自信のないタイトルは1年前に「2011年には流石にリリースされると思います」と書いてしまった反省からです。
リリースに関わっているわけでもないのに根拠のない予言をするものではありません。更にさかのぼること3年前には、Apache2.4カウントダウン?のタイトルで記事を書いています。もはや狼少年状態です。
Apache2.4の新機能の中で意外にフィーチャーされていませんが、個人的な注目はevent MPM(とAsynchronous support)です。いわゆる非同期I/O動作のイベントドリブンなmpmです。非同期I/Oのイベントドリブンと聞くと、nginxと同じ動作?と思う人もいるかもしれませんが、動作モデルは異なります。
Apacheを知っている人は、event mpmがバージョン2.2から存在するのを知っているかもしれません。バージョン2.2では実験的(experimental)mpmでしたが、バージョン2.4でexperimentalが取れました。それどころかUnix系ではデフォルトmpmになりました(経緯としては、一度、simpleと呼ばれる類似の非同期I/Oベースのmpmが作成されデフォルトになって、その後、event mpmに集約されました)。
event mpmの前提知識として、v2.2のデフォルトのworker mpmの動作を知っておいたほうが良いのですが、その辺は3年前の記事を参考にしてください。この記事のsimple mpmの説明の大枠はevent mpmにも当てはまります。
event mpmはマルチプロセスにもできますが、まずは単一プロセスの前提で説明します。
event mpmには単一のリスナスレッドと複数のワーカースレッドが存在します。リスナスレッドのエントリポイントはlistener_thread()です。listener_thread()はネットワーク待ちのイベントループです。イベントループはapr_pollset_poll()でブロックします(タイマーありのブロック)。apr_pollset_poll()はOSに応じてepoll_wait(2)やpoll(2)やselect(2)の呼び出しにつながります。
このapr_pollset_poll()で、accept(2)待ちのソケット(いわゆるlistenソケット)とaccept(2)が返したソケット(いわゆるconnectionソケット。以下接続ソケット)の両方を監視します。コードの骨格は次になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// リスナスレッドのコードの骨格 static void * APR_THREAD_FUNC listener_thread(apr_thread_t * thd, void *dummy) { 省略... for (;;) { 省略... rc = apr_pollset_poll(event_pollset, timeout_interval, &num, &out_pfd); 省略... // apr_pollset_poll()が返したソケットの処理ループ while (num) { 省略... // CSD(client socket descriptor):HTTP処理中のソケット if (pt->type == PT_CSD) { 省略... // ソケットのステートに応じた処理の振り分け(リクエスト読み込み中やレスポンス書き出し中など) switch (cs->pub.state) { case CONN_STATE_CHECK_REQUEST_LINE_READABLE: 省略... case CONN_STATE_WRITE_COMPLETION: 省略... // ワーカースレッドに拾ってもらうためのキューに突っ込む rc = push2worker(out_pfd, event_pollset); 省略... case CONN_STATE_LINGER_NORMAL: case CONN_STATE_LINGER_SHORT: // 処理済みのソケットを閉じる 省略... } } // TCP接続を受け付けたソケット else if (pt->type == PT_ACCEPT) { // accept(2)して接続ソケットをCSDとしてapr_pollset_poll()の監視対象にする } out_pfd++; num--; } 省略... } 省略... } |
クライアントから接続を受け付けると、listenソケットに対してaccept(2)処理をします。accept(2)が返した接続ソケットをapr_pollset_poll()の監視対象にします。クライアントからHTTPリクエストが飛んでくると、この接続ソケットがreadableとしてapr_pollset_poll()から返ります。最初のステートはCONN_STATE_CHECK_REQUEST_LINE_READABLEなので、リクエスト行の読み込みをします(上記コード抜粋を参照)。リクエスト行読み込みの処理など、実際のHTTP処理はリスナスレッドではなくワーカースレッドが行います。リスナスレッドは読み込み可能になった接続ソケットを内部的なキューに入れるだけで実処理はしません。
ワーカースレッドのエントリポイントはworker_thread()です。コードの骨格は次のように単純です。キューにソケットが突っ込まれるまでブロックします。言うまでもありませんが、ブロック中にCPUは消費しません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// ワーカースレッドのコードの骨格 static void *APR_THREAD_FUNC worker_thread(apr_thread_t * thd, void *dummy) { 省略... // 事実上の無限ループ while (!workers_may_exit) { 省略... // リスナスレッドがキューに突っ込んだソケットを取り出すまでブロック rv = ap_queue_pop_something(worker_queue, &csd, &cs, &ptrans, &te); 省略... rv = process_socket(thd, ptrans, csd, cs, eq, process_slot, thread_slot); } 省略... } |
process_socket()はソケットのステートに応じたHTTP処理をします。細かい部分を無視すれば、リクエスト行の読み込み、リクエストヘッダの読み込み、リクエストボディの読み込み、レスポンス出力の4段階のステート遷移です。HTTP接続がkeep-aliveであれば、レスポンス出力後に次のリクエスト行読み込みにステート遷移します。
worker mpmの場合、このステート遷移の間、ワーカースレッドがひとつのHTTP処理、つまりひとつのクライアントに占有されます。event mpmの場合、ワーカースレッドは占有されないので、複数のHTTP接続を並行的に処理できます。
こうなるとHTTP処理中のソケット読み書きがブロッキングかノンブロッキングかが気になります。答えを書くと、リクエスト行の読み込みとレスポンス書き出しはノンブロッキング、リクエストヘッダとリクエストボディの読み込みはブロッキングです。つまり、クライアントが遅かったり(これはあまりない)、ネットワークが遅かったり(これはありそう)、悪意的に小さなバイト列を散発的に送りつけるリクエストがあると、ワーカースレッドがリクエストの読み込みでブロックする可能性があります。要はワーカースレッドが占有されます。これが問題になる場合はRequestReadTimeoutディレクティブのタイムアウトを短くするのが防衛策になります。
Apacheが非同期I/Oベースになると、nginxとの性能比較が気になります。個人的見解ですがApacheがnginx同等のスループット性能には達しないだろうと思っています。誰かがベンチマーク比較していないか探しましたが見つかりませんでした。Apache2.2時の比較でボロ負け結果は見つかりますが。
比較のためにnginxの動作を簡単に説明すると、単一スレッドがリスナとワーカーの役割を兼ねて、かつすべてのソケット読み書き処理がノンブロッキングになるアーキテクチャです(acceptすらaccpet4でノンブロッキングにする徹底さです。accept可能になったソケットがブロックしうるのかは知りませんが)。こういう言い方はUnixはLinuxみたいなものですと言うみたいで一部の人を不快にさせそうですが、要はNode.jsと同じアーキテクチャです。
event mpmのApacheはworker mpmより平行性が上がったとは言え、依然としてワーカースレッドの数が平行性の制約であることに変わりありません。ワーカースレッドの数を増やすには次のふたつの方法があります。
- プロセス当たりのワーカースレッドを増やす
- プロセスを増やす
プロセス当たりのワーカースレッドの数は32bit OSではメモリの制約で通常1000のオーダーです(OSにも依ります。Linuxのデフォルトはスレッドひとつに仮想メモリ2Mバイト割り当てるのでスレッド数1000のオーダーでプロセスの仮想メモリが2Gバイトのオーダーになります)。64bit OSであればこの辺の制約が外れるので、スレッド数のオーダーを上げられます。スレッド数のオーダーを上げた時はスレッド間のタスク切り替えのコストが課題になります。この課題は10年前ぐらいから言われていて、その後、カーネル側の改良のニュースなども時々聞いた記憶もあるので(最近の動向はウォッチしていません)、もしかした1万スレッドぐらい余裕な世界になっているかもしれません。
後者のプロセスを増やすのはApache的には保守的な手段です。一見、問題ないようですが、複数プロセスが同じポートのソケットに一斉にaccept(2)かけた時の性能問題を昔(いつだったか思い出せないぐらい大昔)見た記憶があります。こちらも凄腕のカーネルハッカーたちが解決済みの問題かもしれません。
他のApache2.4の新機能の解説は気が向いたらやります。
井上
http://blog.matsumoto-r.jp/?p=1812
nginxには勝てないだろうと安易に書いてごめんなさい。
井上
http://blog.zhuzhaoyuan.com/2012/02/apache-24-faster-than-nginx/
nginxのほうが速い反論ベンチマーク結果が現れました。