地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Nginx】multi_acceptを有効化すると何が変わるのかをソースも一緒に追ってみた

概要

nginxの設定でmulti_acceptという設定がありそれをon/offすることで内部的にどう変わっていくのかを追っていく記事。パフォーマンスを求める場面 では必須の設定(?)だと思うのですが実際は内部的にどういう動作となっているのかを追っていきます。ちなみに過去のISUCONの出場者のブログをちらほら見てたのですが多くの人がこの設定をonにしており結構有名なパラメータなのかな?という感じでした

前提知識としてLinuxtcpのlisten-queueとaccept-queueの知識が必要です。

ryuichi1208.hateblo.jp

最初にまとめ

まとめを書こうとしてたのですが公式ドキュメントの説明が一番しっくりくるまとめだったのでそちらを引用しておきます。若干補足すると一度にすべての新しい接続を受け入れますの部分は具体的にワーカープロセスのacceptハンドラの1回の実行を指しておりonにすることで3-wayhandshake済のコネクションをイベントハンドラ実行時に全てaccept(2)してしまおうという機能です。

multi_acceptが無効になっている場合、ワーカープロセスは一度に1つの新しい接続を受け入れます。それ以外の場合、ワーカープロセスは一度にすべての新しい接続を受け入れます。

onにするメリットはnginxがブロッキング処理などで忙しい時にaccept_queueが溢れるくらいのクライアントからの接続が来ている場合にワーカーが多くの接続をaccept(2)するのでtcpdropが発生しにくくなります。ただCPUリソースやメモリリソースなどが逼迫している場合には意味をなさないので(accept(2)してる余裕すらないケース)そこは注意が必要です。

multi_acceptとは

公式から引用します。ちなみにデフォルトoffなので知る人ぞ知る設定なのかなと勘繰ってしまいましたw

内容自体はとてもシンプルでonにすることで一度のmulti_acceptが無効になっている場合、ワーカープロセスは一度に1つの新しい接続を受け入れます。それ以外の場合、ワーカープロセスは一度にすべての新しい接続を受け入れます。eventがkqueueの場合はこの設定は無効化されます。(今回はepollで追っていくので該当はしません)

Syntax:  multi_accept on | off;
Default:    
multi_accept off;
Context:    events

If multi_accept is disabled, a worker process will accept one new connection at a time. 
Otherwise, a worker process will accept all new connections at a time.

The directive is ignored if kqueue connection processing method is used, 
because it reports the number of new connections waiting to be accepted.

nginx.org

イベント駆動エンジン

ざっくりイベント駆動エンジンと対応する関数を書く。enventのハンドラはngx_event_acceptとなる。

  • 1 eventfdを作る
  • 2 eventfdに対してeventをハンドラ付きで書き込む ngx_epoll_process_events()
  • 3 envetfdからeventを読み込む(ngx_epoll_process_events)
  • 4 enventのハンドラを実行する -> 今回はacceptなのでngx_event_accept
  • 5 2に戻る

ten-snapon.com

ソースを追っていく

設定が有効化されると以下のflg変数が立ちます。

typedef struct {
    ngx_uint_t    connections;
    ngx_uint_t    use;

    ngx_flag_t    multi_accept;
    ngx_flag_t    accept_mutex; // これ

    ngx_msec_t    accept_mutex_delay;

    u_char       *name;
} ngx_event_conf_t;

acceptを行うイベント。Nginxはマスターワーカーマルチプロセスモードで動作するため、すべてのワーカープロセスが同時に同じポートをリッスンする場合、ポートで新しい接続イベントが発生すると、各ワーカープロセスは関数ngx_event_accep()を呼び出して確立を試みます。

void
ngx_event_accept(ngx_event_t *ev)
{
    socklen_t          socklen;
    ngx_err_t          err;
    ngx_log_t         *log;
    ngx_uint_t         level;
    ngx_socket_t       s;
    ngx_event_t       *rev, *wev;
    ngx_sockaddr_t     sa;
    ngx_listening_t   *ls;
    ngx_connection_t  *c, *lc;
    ngx_event_conf_t  *ecf;
#if (NGX_HAVE_ACCEPT4)
    static ngx_uint_t  use_accept4 = 1;
#endif

    // イベントハンドラに与えられた時間を使い切ったらループを終了
    if (ev->timedout) {
        if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) != NGX_OK) {
            return;
        }

        ev->timedout = 0;
    }

    // イベントコアモジュールの構成情報を取得
    ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);

    if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {
        ev->available = ecf->multi_accept;
    }

    lc = ev->data;
    ls = lc->listening;
    ev->ready = 0;

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                   "accept on %V, ready: %d", &ls->addr_text, ev->available);

    // acceptループ。クライアントからの接続の処理はここでやってます。
    do {
        socklen = sizeof(ngx_sockaddr_t);

#if (NGX_HAVE_ACCEPT4)
        if (use_accept4) {
            s = accept4(lc->fd, &sa.sockaddr, &socklen, SOCK_NONBLOCK);
        } else {
            s = accept(lc->fd, &sa.sockaddr, &socklen);
        }
#else
        s = accept(lc->fd, &sa.sockaddr, &socklen);
#endif

        // エラー状態の処理。正常ならcontiuneu or returnになります
        if (s == (ngx_socket_t) -1) {
            err = ngx_socket_errno;

            // ソケットが非停止になっていて、 かつ受付け対象の接続が存在しない
            if (err == NGX_EAGAIN) {
                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, err,
                               "accept() not ready");
                return;
            }

            level = NGX_LOG_ALERT;

            // 接続が中止された
            if (err == NGX_ECONNABORTED) {
                level = NGX_LOG_ERR;

            // 1プロセスがオープンできるファイルディスクリプター数の上限に達した
            } else if (err == NGX_EMFILE || err == NGX_ENFILE) {
                level = NGX_LOG_CRIT;
            }

#if (NGX_HAVE_ACCEPT4)
            ngx_log_error(level, ev->log, err,
                          use_accept4 ? "accept4() failed" : "accept() failed");

            if (use_accept4 && err == NGX_ENOSYS) {
                use_accept4 = 0;
                ngx_inherited_nonblocking = 0;
                continue;
            }
#else
            ngx_log_error(level, ev->log, err, "accept() failed");
#endif

            if (err == NGX_ECONNABORTED) {
                // kqueueが有効な場合はループを終わって呼び出し元に戻る
                if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                    ev->available--;
                }

                // 他のacceptキューのsocketのaccpet(2)を試みるためcontinue
                if (ev->available) {
                    continue;
                }
            }

            // プロセスあたりのオープンできるファイル数に到達したらループを抜ける
            if (err == NGX_EMFILE || err == NGX_ENFILE) {
                if (ngx_disable_accept_events((ngx_cycle_t *) ngx_cycle, 1)
                    != NGX_OK)
                {
                    return;
                }

                // accept(2)のmutexロック。今回は割愛
                if (ngx_use_accept_mutex) {
                    if (ngx_accept_mutex_held) {
                        ngx_shmtx_unlock(&ngx_accept_mutex);
                        ngx_accept_mutex_held = 0;
                    }

                    ngx_accept_disabled = 1;

                } else { // accept()時のmutexの確保に失敗した際の待機時間を調整するためのディレクティブ
                    ngx_add_timer(ev, ecf->accept_mutex_delay);
                }
            }

            return;
        }

    // ここが今回の肝でmulti_acceptが有効な場合は次のループ処理が実行される
    } while (ev->available);
}