shimada-kの日記

ソフトウェア・エンジニアのブログです

nginxは新規リクエストをどう受け付けるのか

nginxで新規のリクエストが発生した場合の処理をソース上で追いかけてみました。

nginxはワーカープロセスを指定した数だけ起動させてそのプロセス内で非同期にリクエストをさばいていますが複数のワーカープロセスが存在した場合、新規リクエストはどう振り分けられるのか気になったからです。

デバッグログを出力させるためにnginxのソースを本家から落としてきてコンパイルしました(現時点で最新版1.7.1)。

configureの引数に「--with-debg」を付けないとnginx.confでログレベルをdebugにしてもデバッグログは出力されません。

デバッグログを出力可能な状態にしてnginxを起動しブラウザでアクセスすると以下のようなログが出ました。

2014/06/12 23:36:47 [debug] 3932#0: epoll: fd:6 ev:0001 d:0970ABD8
2014/06/12 23:36:47 [debug] 3932#0: accept on 0.0.0.0:80, ready: 0
2014/06/12 23:36:47 [debug] 3932#0: posix_memalign: 096F8C00:256 @16
2014/06/12 23:36:47 [debug] 3932#0: *1 accept: 192.168.80.1:62482 fd:3
2014/06/12 23:36:47 [debug] 3932#0: *1 event timer add: 3: 60000:2309328887
2014/06/12 23:36:47 [debug] 3932#0: *1 reusable connection: 1
2014/06/12 23:36:47 [debug] 3932#0: *1 epoll add event: fd:3 op:1 ev:80002001
2014/06/12 23:36:47 [debug] 3932#0: timer delta: 21126
2014/06/12 23:36:47 [debug] 3932#0: posted events 00000000
2014/06/12 23:36:47 [debug] 3932#0: worker cycle

ログを出力している場所を上から順番に検索しました。1番上のメッセージはngx_epoll_process_eventが出力しているものです。

ngx_epoll_process_events

event/modules/ngx_epoll_module.c

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)

この関数の中でepoll_wait(2)しています。さらにmutexを獲得できた場合は後続の処理を行い最後にunlockするといった実装になっています。ロック取得後はhandler経由でngx_event_acceptを呼び出します。

ngx_event_accept

event/ngx_event_accept.c

void
ngx_event_accept(ngx_event_t *ev)

この関数の中でaccept(2)を実行しています。この関数が2番目と4番目のメッセージを出力しています。ログやソケットの設定をしてngx_add_timerを呼び出した後、handler経由でngx_http_init_connectionを呼び出します。

ngx_event_add_timer

event/ngx_event_timer.h

static ngx_inline void
ngx_event_add_timer(ngx_event_t *ev, ngx_msec_t timer)

この関数はngx_add_timerという命名でevent/ngx_event_timer.hにおいてマクロ化されているためソース上は直接呼び出されません。タイマーの初期化を行っています。タイマーは赤黒木で管理されていました。この関数が実行されて5番目のメッセージが出力されます。

ngx_http_init_connection

http/ngx_http_request.c

void
ngx_http_init_connection(ngx_connection_t *c)

この中でngx_reusable_connectionが呼び出されて6番目のメッセージが出力されます。

ngx_epoll_add_event

event/modules/ngx_epoll_module.c

static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)

この中ではepoll_ctl(2)を呼び出してepollで監視するイベントを追加しています。この関数が実行されることで7番目のメッセージが出力されます。

ngx_process_events_and_timers

event/ngx_event.c

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)

この関数の中で8番目と9番目のメッセージが出力されます。この関数の中でngx_process_eventsが実行されることで一番最初のngx_epoll_process_eventsが呼び出されます。一番最初のngx_epoll_process_eventsはngx_event_actions_t構造体に登録されていて、process_eventsというシンボルで透過的に呼び出される仕組みになっています。

event/ngx_event.h

#define ngx_process_changes  ngx_event_actions.process_changes
#define ngx_process_events   ngx_event_actions.process_events
#define ngx_done_events      ngx_event_actions.done

#define ngx_add_event        ngx_event_actions.add
#define ngx_del_event        ngx_event_actions.del
#define ngx_add_conn         ngx_event_actions.add_conn
#define ngx_del_conn         ngx_event_actions.del_conn

#define ngx_add_timer        ngx_event_add_timer
#define ngx_del_timer        ngx_event_del_timer

実際の呼び出しの部分はマクロで定義されています。

ngx_worker_process_cycle

os/unix/ngx_process_cycle.c

static void
ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)

最後10番目のメッセージを出力しているのがngx_worker_process_cycleです。この関数内は無限ループになっていて、ワーカープロセスの本体です。実はこの関数の中でngx_process_events_and_timersを呼び出しています。

そもそも、ngx_worker_process_cycleを呼び出しているのはmasterプロセスが呼び出すngx_start_worker_process経由で呼び出されたngx_spawn_procesの中でfork(2)した子プロセスです。

ngx_start_worker_processes

os/unix/ngx_process_cycle.c

static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)

この関数の中で設定ファイルで指定されているワーカープロセスの数だけループを回してngx_spawn_processを呼び出しています。その際に引数でngx_worker_process_cycleの関数ポインタを渡しています。

ngx_spawn_process

os/unix/ngx_process.c

ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
    char *name, ngx_int_t respawn)

この中でfork(2)して第2引数で受け取ったprocを子プロセスが実行しています。第2引数はngx_worker_proces_cycleです。

まとめ

各ワーカープロセスはngx_worker_proces_cycleの中でepollでイベントを監視し、最初にmutexを獲得できたプロセスがaccept(2)を実行します。

最近のカーネルでは複数の子プロセスが同時にaccept(2)を実行してもエラーにはならないはず*1ですが、このような実装になっている理由は公式ドキュメント*2で下記のように書いてあります。

If accept_mutex is enabled, worker processes will accept new connections by turn. Otherwise, all worker processes will be notified about new connections, and if volume of new connections is low, some of the worker processes may just waste system resources.

「アクセスがあった時にすべてのワーカープロセスを起こすのはシステムリソースの無駄遣いである」という設計思想を読み取ることができます。