概要
nginxの設定でmulti_acceptという設定がありそれをon/offすることで内部的にどう変わっていくのかを追っていく記事。パフォーマンスを求める場面 では必須の設定(?)だと思うのですが実際は内部的にどういう動作となっているのかを追っていきます。ちなみに過去のISUCONの出場者のブログをちらほら見てたのですが多くの人がこの設定をonにしており結構有名なパラメータなのかな?という感じでした
前提知識としてLinuxのtcpのlisten-queueとaccept-queueの知識が必要です。
最初にまとめ
まとめを書こうとしてたのですが公式ドキュメントの説明が一番しっくりくるまとめだったのでそちらを引用しておきます。若干補足すると一度にすべての新しい接続を受け入れます
の部分は具体的にワーカープロセスのacceptハンドラの1回の実行を指しておりonにすることで3-wayhandshake済のコネクションをイベントハンドラ実行時に全てaccept(2)してしまおうという機能です。
multi_acceptが無効になっている場合、ワーカープロセスは一度に1つの新しい接続を受け入れます。それ以外の場合、ワーカープロセスは一度にすべての新しい接続を受け入れます。
onにするメリットはnginxがブロッキング処理などで忙しい時にaccept_queueが溢れるくらいのクライアントからの接続が来ている場合にワーカーが多くの接続をaccept(2)するのでtcpのdropが発生しにくくなります。ただ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.
イベント駆動エンジン
ざっくりイベント駆動エンジンと対応する関数を書く。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に戻る
ソースを追っていく
設定が有効化されると以下の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); }
オンにするメリットとかデメリットとか
synbacklogが大きくなってしまっているケースではオンにしておくことである程度軽減される。デメリットはupstreamに流したく無い量も受け付けてしまうのできちんと流量制御を実装する必要がありそうということ。nginxのacceptで止まってくれてるおかげで実は負荷が高くなっているのが局所的に抑えられていたみたいなケースもありそうな気がする。
バックエンドがどれくらいの処理を行えるかを計測してその後にrate limitなどを実装。そしてこのパラメータのチューニングするかどうかの判断のような流れになるかな