4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Apache】preforkの話

Last updated at Posted at 2021-01-06

概要

preforkの話。preforkってなんだっけって話から広げていく

そもそもTCP通信のフロー

LinuxでTCPクライアント/サーバで通信するにはサーバ側は以下の手順を踏んで通信を待ち受ける。

socket() -> bind() -> listen() -> acceps()

単一プロセスで動く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すると共有される。つまりacceptしたsocketを子プロセスが使って通信が可能ということ。(あくまでも共有であって子プロセスがそれぞれソケットを作っているわけではないので注意)。ちなみにいろいろ調べてて見つけたのだがRubyの場合はexec呼び出し時にfdをcloseするらしい。

manとかでfork(2)とかで調べるとでてくる。

子プロセスは親プロセスが持つ オープンファイルディスクリプタの集合のコピーを引き継ぐ。 子プロセスの各ファイルディスクリプタは、 親プロセスのファイルディスクリプタに対応する 同じオープンファイル記述 (file description) を参照する (open(2) を参照)。 これは 2 つのディスクリプタが、ファイル状態フラグ・ 現在のファイルオフセット、シグナル駆動 (signal-driven) I/O 属性 (fcntl(2) における F_SETOWN, F_SETSIG の説明を参照) を共有することを意味する。

forkモデルのTCPサーバで起きる問題

ここで起きるのがfork(2)のコストの高さです。forkはCoWが効くとは言えプロセスの生成自体は大変な作業です。詳しくは以下の記事をry

じゃあどうするかスレッドを生成する方法やイベント駆動なんかが流行ってますが記事の主題でもあるpreforkを使う方法を取り上げます。forkのコストが高いなら先にforkさせておいてクライアントが来ても生成しておいたsocketで賄おうという考え方です。ざっくり流れとしては

  • ソケットを親プロセス で生成
  • forkして子プロセスを作る
  • 子プロセスがそれぞれaccept待ちでblockする(処理が正常に完了した場合、受け付けたソケットの 記述子である非負整数を返します。)

といった状態を作ります。この時3番目のaccept待ちでブロックしている子プロセスはクライアントが来ると複数のうちひとつのプロセスだけを起床させ起床したプロセスがその後の通信を行うことができるといった仕組みです。この時クライアントの最大同時接続数はforkした数となります。1000ユーザ接続させたいなら1000のプロセスが必要です。この辺は巡り巡ってC10k問題とか言われているやつです。

Nginxなんかだと少ないプロセス(内部的にも1スレッド)で大量のアクセスを捌いていますがpreforkとかスレッドではなくイベント駆動で処理をしています。ざっくりいうとworkerごとにイベントを監視して都度処理を行うことでコンテキストスイッチなんかの問題を軽減しています。本題とは何も関係ないのでとりあえずepoll(2)が凄すぎるって感じで辞めておきます。

同一のソケットに複数プロセスからacceptして大丈夫なの?

結論としては大丈夫らしいです。ソースを読んだわけではないですがこの辺はカーネルがよしなにやってくれるらしいです。古いカーネルだとthundering herdなんかも起きていたらしいです。(acceptmutexをアプリで実装してるケースもあるらしく古いapacheなんかはその方式で排他を行っていたらしい)

おまけ① Gracefull shutdownってどういう実装なの?

グレースフルシャットダウンの定義を以下とするなら

  • 停止指示後に、新しい接続を受付しない
  • 残った処理中の接続が完了するのを待ってから、プロセスを安全に停止する

TCPレイヤでやることとしたらlisten socketをcloseする。 -> 新規接続を受付しない

子プロセスは親から受けたシグナルを元に処理完了で終了するように実装する -> 安全停止

といった流れになる。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がactiveな接続を監視してなければプロセスが終了する。読んでいて気づいたがこれはバックエンドのアプリのどこかで刺さった場合はNginx自体のこの処理もTimeoutを設定してなければ永遠に動かない実装の模様。そもそもそんなのアプリ側でなんとかしろよって話だけどいつかハマりそう。ngx_event_no_timers_leftはこの辺。イベントツリーを確認して未処理イベントが無ければOKを呼び出し元に返す。今回はOKが帰ってきたらNginxのプロセスが正常終了する流れなのでそれを持ってGracefull 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_*っていう名前からも想像できるようにイベントの状態は赤黒木で持っている模様。平衡二分木の一種なのは知ってるけど詳細は何も分からんので調べよう。。。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?