【Apache】有关prefork的讨论
摘要
预分叉(prefork)的讨论。从对预分叉是什么的讨论开始扩展。
从一开始就是TCP通信的流程。
要在Linux环境下实现TCP客户端/服务器通信,服务器端需按照以下步骤等待通信连接。
socket() -> bind() -> listen() -> accept()
建立套接字 -> 绑定 -> 监听 -> 接收
如果是运行在单一进程中的TCP服务器,那么上述步骤就足够了。但是当客户端变成多个时,在同时连接时,客户端1可以连接,但客户端2无法连接。
为了支持多个客户端的同时连接,可以使用fork()系统调用。最简单的方法是在父进程中进行监听(listen),生成子进程并让子进程继续处理后续操作。以下是一个简单的示例。通过这种方式,由于子进程的生成,单一进程的TCP服务器所面临的问题就得以解决。
// listenまでは親でやっておきループ内で子プロセスを生成し通信を行う。
for (;;) {
len = sizeof(client);
sock = accept(sock0, (struct sockaddr *)&client, &len);
pid = fork();
if (pid == 0) {
n = read(sock, buf, sizeof(buf));
if (n < 0) {
perror("read");
return 1;
}
write(sock, buf, n);
close(sock);
return 0;
}
暂时只需赶紧写出一个大致内容,所以详细情况省略了。
在接受(accept)之后进行分叉(fork),套接字(socket)会在父子进程之间共享吗?
sock = accept(sock0, (struct sockaddr *)&client, &len);
pid = fork();
if (pid == 0) {
n = read(sock, buf, sizeof(buf));
if (n < 0) {
perror("read");
return 1;
}
这部分代码在fork之后轻松地使用了父进程打开的socket,对此产生了疑问。答案在下面的文章中非常清楚明了。简单来说,打开文件描述符(例如文件偏移量等)在fork之后是被共享的。也就是说,子进程可以使用父进程接受的socket进行通信。(需要注意的是,这只是共享的,而不是每个子进程都创建了自己的socket)。顺便说一下,在我进行了一些调查后发现,对于Ruby而言,在调用exec时会关闭fd。
当使用 man 命令或者 fork(2) 进行搜索时会找到相关信息。
子プロセスは親プロセスが持つ オープンファイルディスクリプタの集合のコピーを引き継ぐ。 子プロセスの各ファイルディスクリプタは、 親プロセスのファイルディスクリプタに対応する 同じオープンファイル記述 (file description) を参照する (open(2) を参照)。 これは 2 つのディスクリプタが、ファイル状態フラグ・ 現在のファイルオフセット、シグナル駆動 (signal-driven) I/O 属性 (fcntl(2) における F_SETOWN, F_SETSIG の説明を参照) を共有することを意味する。
使用fork模型的TCP服务器所产生的问题
在这里发生fork(2)耗费很高的成本。虽然fork适用于CoW,但进程的创建本身是非常繁重的工作。请参考以下文章了解更详细的信息。
那么我们将讨论一种在文章主题中也提到的使用prefork的方法,虽然生成线程和事件驱动等方式很流行。如果fork的成本很高,我们可以先进行fork,然后在客户端到来时使用预先生成的套接字来提供服务。大致的流程如下。
-
- ソケットを親プロセス で生成
-
- forkして子プロセスを作る
- 子プロセスがそれぞれaccept待ちでblockする(処理が正常に完了した場合、受け付けたソケットの 記述子である非負整数を返します。)
使用这种方法可以创建一个状态。在此状态下,阻塞在第三个accept的子进程只会唤醒其中一个进程来处理客户端的到来,并且唤醒后的进程可以进行后续的通信。此时客户端的最大并发连接数将等于fork的数目。如果想要有1000个用户连接,就需要1000个进程。这涉及到C10k问题。
Nginx在处理大量访问时只需要少量的进程(内部只有一个线程)来处理,它不是通过prefork或线程,而是通过事件驱动来进行处理。简单来说,每个worker会监视事件并进行逐个处理,以减少上下文切换等问题。因为这与主题无关,所以就先说epoll(2)非常强大然后停止吧。
在同一个套接字上同时从多个进程进行accept是安全的吗?
总而言之,听起来应该没问题。虽然我没有看过源代码,但似乎内核会为此处理好。据说旧版本的内核会出现雷雨效应等问题。(似乎某些应用程序还会在应用层实现acceptmutex,旧版本的apache似乎就是通过这种方式进行排他处理的。)
额外选项1:Gracefull shutdown的实现具体是怎样的?
如果要定义”优雅关闭”,可以采用以下方式
-
- 停止指示後に、新しい接続を受付しない
- 残った処理中の接続が完了するのを待ってから、プロセスを安全に停止する
如果在TCP层面操作,我们的任务是关闭监听套接字,不再接受新的连接。
子进程根据收到的信号来实现安全停止并结束处理。
看起来是这样的。以Nginx为例(非常简化地描述了一下)。
static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{
for ( ;; ) {
// SIG_QUITを受け取った子プロセスは以下に入る
if (ngx_quit) {
ngx_quit = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
"gracefully shutting down");
ngx_setproctitle("worker process is shutting down");
if (!ngx_exiting) {
ngx_exiting = 1; // 終了フラグを立てる
ngx_set_shutdown_timer(cycle); // shutdownタイマーを設定する
ngx_close_listening_sockets(cycle); // リスニングソケットをcloseする
ngx_close_idle_connections(cycle); // アイドルコネクションをcloseする
}
}
// 終了フラグが立っているので以下に入る
if (ngx_exiting) {
if (ngx_event_no_timers_left() == NGX_OK) { // ngx_event_no_timers_leftはアクティブな接続がある限りはOKにならない
ngx_worker_process_exit(cycle); // 終了関数を呼び出す
}
}
}
}
如果ngx_event_no_timers_left没有监视到活动连接,则进程将终止。阅读时我意识到,如果在后端应用程序的某个地方出现问题,Nginx本身的处理也没有设置超时,那么它将永远无法运行。总的来说,这是一个应该由应用程序方面解决的问题,不过迟早会遇到类似的情况。ngx_event_no_timers_left就是这样的一个点。它会检查事件树,并且如果没有未处理的事件,就会向调用者返回OK。这次,如果收到OK,Nginx进程将正常终止,以实现Graceful shutdown。
ngx_int_t
ngx_event_no_timers_left(void)
{
ngx_event_t *ev;
ngx_rbtree_node_t *node, *root, *sentinel;
sentinel = ngx_event_timer_rbtree.sentinel;
root = ngx_event_timer_rbtree.root;
// イベントを管理している木がroot = sentinel nodeならOKを返す
if (root == sentinel) {
return NGX_OK;
}
for (node = ngx_rbtree_min(root, sentinel);
node;
node = ngx_rbtree_next(&ngx_event_timer_rbtree, node))
{
ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));
// cancel不可能状態のイベントがあれば終了させない
if (!ev->cancelable) {
return NGX_AGAIN;
}
}
return NGX_OK;
}
根据 ngx_rbtree_* 这个命名,我们可以想象到该事件状态是通过红黑树来维护的。我知道它是一种平衡二叉树,但是具体细节我一无所知,我要去调查一下…