unicornを読んだので備忘録。非公式ですがミラーのリポジトリは defunkt/unicornです。

bin/unicornでは引数をパースしつつ、Unicorn.builderでRackアプリを返すLambdaを作成し、そのLambdaを引数にUnicorn::HTTPServer#start, #joinを実行します。

config.ruが指定されている場合は、inner_appにはconfig.ruの内容でビルドしたRackアプリがセットされます。

最終的にmiddleware => inner_appという順番で実行されるRackアプリが作成されるLambdaを返すことになります。RackアプリではなくLambdaを返している意図としては遅延評価をするためで、Lambda作成時点でRailsアプリを読み込むのではなくLambdaをcallした時点で初めてRailsアプリが読み込まれます。

Unicorn::HTTPServer#startのself.pidではpidファイルにプロセスIDを書き込んでいます。

preload_appがtrueの場合はbuild_app!が実行されます。build_app!はLambda#callを呼んでおり、Railsアプリを読み込みます。この時点ではマスタープロセスが実行しているので、子プロセス生成時には子プロセスでRailsアプリをロードすることなく即座に子プロセスで処理を開始することができます。

bind_new_listeners!メソッドでは指定したアドレス(UNIXドメインソケット or TCPソケット)で待ち受けます。

spawn_missing_workersでWorkerを作成します。このプロセスがHTTPリクエストをハンドリングすることになります。

まず、指定の数だけWorkerを増やします。worker_spawnやforkで子プロセスが生成される前にbefore_forkが呼び出されます。unless pidの部分は子プロセスはpidがnilになるので、子プロセスのみがunlessの中を実行することになり、after_forkとworker_loopが実行されます。親プロセスではpidをキーにUnicorn::Workerのインスタンスを保持します。

worker_loopでは初期化をしつつHTTPリクエストのハンドリングを行っています。

readersにはUnicornのソケットとWorkerのインスタンスが入ります。Workerのインスタンスはto_ioメソッドを実装しており、IO.selectの引数に入れることができます。これによってワーカがマスタープロセスからシグナルを受信できるようにしています。

IO.selectを使っているためThundering Herdが生じます。Unicornのリファレンスではこれに関して以下のように記述しています。

要約するとこんな感じです

  • Thundering Herdになる可能性があるけど、ActionDispatch以外はselect/acceptしかしていないのでアプリケーションには影響を与えないはず
  • Thundering Herd自体の影響もそこまで無い。プロセス数はサーバリソースに依存するが、1コアあたり2〜4プロセスぐらいになる。

ちなみに、Chris’s Wiki :: blog/unix/AcceptDoesNotThunderの記事だと256プロセスでも20〜60ms程度のオーバーヘッドと書いてあったりします。

process_clientではHTTPのハンドリングをしています。HTTPリクエストのパースはRagelを使ってC拡張を書いていて、Unicorn::HttpRequest#readのところで使っています。それ以外の部分はRackの仕様に従ってハンドリングをしています。

デーモン化はUnicorn::Launcher.daemonize!でダブルフォークの方法でデーモン化しています。

IO.pipeを使っているのはgrandparentプロセスのライフサイクルコントロールのためで、joinメソッド内でパイプを通じてgrandparentプロセスにデータが送られたのを契機にプロセスがexitするようになっています。