【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_* 这个命名,我们可以想象到该事件状态是通过红黑树来维护的。我知道它是一种平衡二叉树,但是具体细节我一无所知,我要去调查一下…

bannerAds