この記事は「nginx Advent Calendar 2022」の6日目の記事です!
nginxのアーキテクチャは親プロセス+子プロセスのアーキテクチャで子プロセス(以下ワーカプロセス)がクライアントとのやり取りを行っています。
初期化処理
- 親プロセスが起動する
- socketを作成しconfに書かれている内容になるようにsetsockopt(2)を実行していく
- ワーカープロセスを生成する
という流れで実行していきます。この後はイベント駆動エンジンでイベントの発生のたびにコールバックを呼び出していく処理となっています。今回はepollでの場合を見ていきます。
accept(2)のthundering herd問題
大昔のカーネルはaccept(2)を複数プロセスから呼び出すとすべての寝ているプロセスを起こすという処理になっていました。この問題は現在は解消されています。
しかしこれはaccept(2)を複数プロセスからブロックしている場合になります。epoll->acceptのような場合には今も変わらずepollはすべてのプロセスを起こすので全プロセスでacceptを実行しEAGAINが帰ってくるという処理が走ってしまいました。これはコンテキストスイッチが発生するので余計な処理であることは変わりません。
void ngx_event_accept(ngx_event_t *ev) { s = accept4(lc->fd, &sa.sockaddr, &socklen, SOCK_NONBLOCK); 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(2)済みならEAGAINが帰って来て関数をreturnする "accept() not ready"); return; }
acceptの実行はコンテキストスイッチを伴います。なのでこの対策としてnginxは過去にユーザ空間でシリアライズするようなaccept_mutextという機能を提供してデフォルトでオンになっていました。(現在はデフォルトでオフになっていました。)
オフになった理由としてはカーネルにSO_REUSEPORTやEPOLLEXCLUSIVEと言った機能が提供されるようになりユーザ空間でやるよりもより効率よく処理を行えるようになったからだと思われます。SO_REUSETPORTの性能測定はnginxも公式で行った結果が公開されていますがたしかに性能向上しているようです。これはどういうものかというと複数プロセスでlistenキューを別々に持つことでカーネルが割り振りを行いイベントの発生を1プロセスに限定させることが出来るというものでした。この機能をいれることでクライアントから接続時にngx_event_accept()は1プロセスのみが処理を行うようになります。この時点でthundering herdの対策として問題なしと言えそうです。
SO_REUSEPORTは取りこぼす
ワーカープロセスが終了処理をしている最中であってもカーネルはlistenキューに新規接続を積み続けます。listenキューはプロセスごとにあるのでプロセスが終了したらlistenキューにあったクライアントについては接続がエラーとなるか再送するかというような動きとなります。これを対策するのがEPOLLEXCLUSIVEという機能です。
一方、 EPOLLEXCLUSIVE は、 listen キューは一本のまま、その listen キューを待っている複数プロセスの epoll_wait() を全部起こすのではなく、1つ以上(1つとは限らない)だけ起こすという機能です。 graceful shutdown 中は epoll_wait() をしていないはずなので、 listen キューに来たリクエストは他のワーカープロセスの epoll_wait() に通知され、取りこぼしがありません。
とのことです。プロセスAが終了している途中はepoll_waitは実行していないのでクライアントの接続はプロセスA以外に割り振られることでプロセスAは安心して終了できるというもののようです。仮にacceptキューにのみ入ったコネクションがある場合でもepoll_waitにより救われるので取りこぼしはなくなるはず?