nginxのイベント駆動アーキテクチャをソースコードリーディングする

はじめに

昨今、ngx_mrubyの開発ばっかりやっていて、そもそもnginxの挙動をソースレベルで理解できてないなとふと思いたったので、今日はnginxの挙動をソースで追いかけてみたいと思います。

nginxといえば、所謂イベント駆動モデルで、非同期I/Oで実装されておるのですが、それぞれ何のこっちゃ?という方も多いのではないでしょうか?それらをソースコードレベルでどの様に実装されているか理解することで、実装の本質を理解し、自分でも似たようなコードが書けるようになるかもしれません。

またブログ中において、github.comにラインリンク貼ろうとも思ったのですが、nginxだと普通にずれていって無意味だと思ったので、貼るのやめました。

バックトレースから実行スタックを取得する

僕がCのプログラムを読むときはだいたいgdbで直接実行して、backtraceを取得することで呼び出し巡ごとに読むようにしています。今回は#15あたりから#9あたりまで読んでいってみます。

$ gdb ./build/nginx/sbin/nginx
(gdb) set follow-fork-mode child
(gdb) b ngx_mrb_start_fiber
(gdb) run
#0 ngx_mrb_start_fiber (r=0xa369f0, mrb=0xa37fc0, rproc=0xacf8a0, result=0xa37670) at /home/pyama/src/github.com/matsumotory/ngx_mruby/src/http/ngx_http_mruby_async.c:53
#1 0x0000000000535753 in ngx_mrb_run (r=0xa369f0, state=0xa313b8, code=0xbe6710, cached=1, result=0x0)
at /home/pyama/src/github.com/matsumotory/ngx_mruby/src/http/ngx_http_mruby_module.c:850
#2 0x0000000000537c40 in ngx_http_mruby_post_read_inline_handler (r=0xa369f0) at /home/pyama/src/github.com/matsumotory/ngx_mruby/src/http/ngx_http_mruby_module.c:1578
#3 0x000000000048c6b2 in ngx_http_core_generic_phase (r=0xa369f0, ph=0xc13110) at src/http/ngx_http_core_module.c:880
#4 0x000000000048c601 in ngx_http_core_run_phases (r=0xa369f0) at src/http/ngx_http_core_module.c:858
#5 0x000000000048c56e in ngx_http_handler (r=0xa369f0) at src/http/ngx_http_core_module.c:841
#6 0x000000000049c064 in ngx_http_process_request (r=0xa369f0) at src/http/ngx_http_request.c:1952
#7 0x000000000049a9a3 in ngx_http_process_request_headers (rev=0xd2dde0) at src/http/ngx_http_request.c:1379
#8 0x0000000000499d4e in ngx_http_process_request_line (rev=0xd2dde0) at src/http/ngx_http_request.c:1052
#9 0x00000000004987fe in ngx_http_wait_request_handler (rev=0xd2dde0) at src/http/ngx_http_request.c:510
#10 0x000000000047a8cf in ngx_epoll_process_events (cycle=0xa1fed0, timer=60000, flags=1) at src/event/modules/ngx_epoll_module.c:902
#11 0x00000000004684e9 in ngx_process_events_and_timers (cycle=0xa1fed0) at src/event/ngx_event.c:242
#12 0x0000000000477daa in ngx_worker_process_cycle (cycle=0xa1fed0, data=0x0) at src/os/unix/ngx_process_cycle.c:750
#13 0x0000000000474397 in ngx_spawn_process (cycle=0xa1fed0, proc=0x477cc3 <ngx_worker_process_cycle>, data=0x0, name=0x6a34d3 "worker process", respawn=-3)
at src/os/unix/ngx_process.c:199
#14 0x0000000000476b5e in ngx_start_worker_processes (cycle=0xa1fed0, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359
#15 0x0000000000476104 in ngx_master_process_cycle (cycle=0xa1fed0) at src/os/unix/ngx_process_cycle.c:131
#16 0x0000000000430f51 in main (argc=1, argv=0x7fffffffe658) at src/core/nginx.c:382

スタックを読む

ngx_master_process_cycle

実行している大きな処理としては ngx_start_worker_processes でワーカープロセスを起動することと、そのあとはループを回してタイマーで利用する現在時間を更新したり、ワーカープロセスの状態を確認したり(SIGCHILD)、シグナルに準じた処理を実行したりしている。

ngx_start_worker_processes

ngx_spawn_process を実行し、workerプロセスをforkしてhttpdサーバとしての処理を開始する。forkしたプロセスでは ngx_worker_process_cycle を実行する。

ngx_worker_process_cycle

ワーカーが継続する限りは、 ngx_process_events_and_timers が実行される。ちゃんとgraceful意識されてて、なるほどという気持ち。

ngx_process_events_and_timers

nginxのeventfdをpollしてイベントを発火していく処理。

まず ngx_event_find_timer が実行される。nginxのtimerはレッド・ブラック・ツリーで管理されている。そのあたりの実装は ngx_rbtree.c に一通り書かれていてわかりやすい。timerについては ngx_rbtree_insert_timer_value で値を追加し、 ngx_rbtree_insert の後処理でrebalanceされている。

ngx_event_find_timerは最も発火が近いタイマーを取得する。そしてそのタイマーを引数に ngx_process_events を実行する。

ngx_epoll_process_events(ngx_process_events)

ngx_process_events はいくつかのシステム・コールから非同期I/Oを実現する形式を選択出来るのですが、現状はepoll一択なのでepollの例で追いかけます。 ngx_epoll_process_events では epoll_waitを利用して、イベントの発生を待つ。この時のタイムアウトは ngx_event_find_timer の戻り値が入るので、何からかのタイマーの発火の必要性があるまではイベントを待ち続けることになる。

またevent自体は例えばhttpであれば ngx_handle_read_event などにラップされた関数で、イベントを追加し、そのイベントに実処理が記載されたハンドラ渡すことで処理を行っていく。

  1. eventfdを作る
  2. eventfdに対してeventをハンドラ付きで書き込む
  3. envetfdからeventを読み込む
  4. enventのハンドラを実行する
  5. 2に戻る

ざっくりだがnginxのイベント実装はこんな感じで行われている。またこの処理の後に、 ngx_process_events_and_timers でtimerの残り時間を確認し、timerの残り時間がないもの(指定した時間になったtimer)のhandlerを順次実行していく。

ngx_http_wait_request_handler

コネクションをacceptした時に ngx_event_accept を経由して、コールされます。以降のハンドラも似たような感じでhttpの各フェーズで順次登録されたハンドラがイベントで呼び出されていく感じですね。またこれらのハンドラにはモジュールのハンドラを追加することが可能な仕組みになっていてngx_mrubyこのハンドラにmrubyのコードを実行するハンドラを登録することで、mrubyでトラフィックのコントロールを可能にしています。

まとめ

今日は早起きしたがゆえに、nginxのイベント駆動実装を追いかけてみました。これまでに書いたとおり、それぞれのイベントにハンドラを登録してキューイングしていき、それを非同期的に実行していくというのが大枠の実装になっており、あれ・・・これ何かに似てるな?ジョブキューじゃん!!!!1なんて思ったりもしました。

ソフトウェアや、システムのアーキテクチャ、このように似ている、類似点があることが本当に多いので、なにか新しい仕組みを作る時にこのようなコードリーティングして習得した知識が応用できるかもしれないので、まずはひたすらインプットすることが大事だなと思った次第です。