かなり今更感の漂う内容ではありますが、意外と情報が分散していたり、Apache2.4系を考慮した場合に足りていない内容があったのでこのエントリで一度まとめてみようと思います。
CGIを使うようなシステムでそれなりにアクセスが集中するサーバ、例えば日々のピーク時のApacheのbusyワーカー数が1000になるようなサーバで、かつ、それを処理可能なマシンスペックのサーバであることを前提にしています。
ApacheのMPMとCGI実行アーキテクチャの復習
ApacheでCGIを使う場合には、MPMとCGI実行アーキテクチャの組み合わせは大きく分けて以下の2つに分ける事ができます。
- worker(event) + mod_cgid
- prefork + mod_cgi
Apacheの2.4系から特にworker(event) + mod_cgidのモデルが推奨されているようです。また、2.4系ではpreforkであってもデフォルトではmod_cgidが動作するようになっています。ですが、このまま使用すると状況によってはマシンの性能を十分に生かせない事があります。それらについて、まずはCGIを実行するためのアーキテクチャの違いからお話します。
mod_cgiとmod_cgidのアーキテクチャの違い
まずは簡単に復習ですが、mod_cgiはリクエストのあったCGIをApacheでプールしている複数のプロセスで分担してCGIを実行します。それに対して、mod_cgidはApache起動時にシングルスレッドのhttpdプロセスを起動させておき、CGIにリクエストがあった場合はリクエストを受けたhttpdプロセスとmod_cgid専用のシングルスレッドのhttpdプロセスがプロセス間通信を行い、実際のCGI実行はmod_cgid専用のプロセスで実行されます。通常このプロセスは一つです。
workerやeventといったマルチスレッドかつマルチプロセスのハイブリッド型MPMを使う場合はmod_cgidを使う事が推奨されています。その理由は以下のエントリやApacheのマニュアルに書かれています。
Unix オペレーティングシステムの中には、マルチスレッドのサーバから プロセスを fork するのが非常にコストの高い動作になっているものがあります。 理由は、新しいプロセスが親プロセスのスレッドすべてを複製するからです。 各 CGI 起動時にこのコストがかかるのを防ぐために、mod_cgid は子プロセスを fork して CGI スクリプトを実行するための 外部デーモンを実行します。 主サーバは unix ドメインソケットを使ってこのデーモンと通信します。
マルチスレッドのプログラムで、「自スレッド以外のスレッドが存在している状態」でfork*1を行うと、さまざまな問題を引き起こす可能性があります。「問題」の典型例としては、子プロセスのデッドロックが挙げられます。問題の詳細を把握しないまま、マルチスレッドのプログラムで不用意にforkするのはやめましょう!
上記のエントリのように、マルチスレッドのプロセスをガンガンfork()するのは安全でなかったり、コストの問題があったりするので、Apacheでは基本的にworkerやeventをMPMで使う場合にmod_cgidとの組み合わせで使用する事を推奨しているわけです。
マシンの性能向上に伴いmod_cgidプロセスがボトルネックに
しかし、実は状況によってmod_cgidにはスケールの面で問題があります。というのも、上記の仕様上どれだけhttpdのスレッドやプロセスをプールしていたとしても、CGIへのアクセスはシングルスレッドのmod_cgidプロセスで処理が一旦行われます。アクセス数の増加に合わせてマシンの性能を向上させ、CPUのコアの数も10以上になる事もざらにあるようなこの時代にも関わらず、です。
その状況で、メモリ効率や性能のためと思ってworkerやeventモデルとmod_cgidを合わせて使っていると、マシンスペックが向上してCPUコアが増え、大量のリクエストを受け付けられるようになっていくほどに、CGIの処理をシングルスレッドで担当しているmod_cgidプロセスが忙しくなります。そして、ついには一つのコアをCPU100%で占有してしまいます。
その結果、他に沢山コアがあるにも関わらず、mod_cgidのプロセスがボトルネックとなってその他のhttpdプロセスが待たされる状況が発生するのです。
つまり、CGIを使う状況によってはマシンスペックやアクセスパターンを考慮した場合、メモリ効率や静的コンテンツ配信の性能向上に効いてくるだろうという考えから、むやみにworkerやeventといったMPMを使わずに、あえて従来のprefork + mod_cgiを使う事も考えられるという話になります。
prefork+mod_cgiをチューニング
ではそういう状況ではどういうチューニングをするべきかを述べます。この組み合わせだと、CGIを沢山使う状況では、マシンの性能が向上しマルチコアの数が増えても、上述した通りmod_cgiは複数のマルチプロセスそれぞれがfork()してCGI実行を担当するので、効率良くマルチコアを使う事ができると考えられます。
この状況でのチューニングとしては、僕は以下の戦略でチューニングするようにしています。以降はApache2.4系でのディレクティブ名で記述します。
- 日々のピーク時のbusyワーカーの2倍を
MaxRequestWorkers(旧MaxClients)
に設定する StartServers
、MinSpareServers
、MaxSpareServers
、MaxRequestWorkers
の数を同じにする- mod_bumpy_lifeを使ってサーバプロセスの破棄のタイミングを分散する
SpareServersやMaxRequestWorkers(旧MaxClients)を同じにする話はこれまでも散々検討されてきた内容で、この点については以下の記事で詳細に解説されています。
また、MaxRequestWorkers
をピーク時のbusyワーカーの約2倍に設定しておけば、大抵の場合は効率良くリクエストを処理できるはずです。そのためには当然、そのプールしておくhttpdプロセスの数とSharedメモリを考慮した物理メモリ、さらには十分なCPUのコア数を載せておく必要があります。
さらに、上記の設定を同じ値にしておくと、MaxRequestsPerChild
で設定した値のタイミングでhttpdプロセスがほとんど同時に全て破棄されてしまうので、その瞬間の性能が大きく低下してしまう可能性があります。その対策として、mod_bumpy_lifeを使う事で、httpdプロセスが破棄されるタイミングをばらしておいて、大きな性能低下が発生しないようにしておきましょう。
実際に、CGIプロセスの生成・破棄と、httpdの小サーバプロセスの生成・破棄のコストは大きく違っています。実験的に1リクエストに必ずCGIプロセスの生成・破棄を発生した場合と、httpdの子サーバプロセスの生成・破棄を発生させた場合とでは、数十倍性能に違いが出る事を以前確認しました。
ですので、StartServers
、MinSpareServers
、MaxSpareServers
、MaxRequestWorkers
の数を同じにすることでコストの高いhttpdの子サーバプロセスのfork()を極力防止し、かつ、同時に複数のhttpd子サーバプロセスが破棄されないようにする必要があります。詳細は以下のエントリや論文を参考にして下さい。
worker+mod_cgidとprefork+mod_cgiのベンチマーク
それでは上記の考え方に従ってチューニングを行い、CPUコア24個でメモリ32GBの環境でベンチマーク比較を行いました。それぞれApache 2.4のworkerとpreforkの設定は以下になります。(MaxRequestWorkersだとシンタックスハイライトにならないので遺憾ながらMaxClientsで書いています)
worker + mod_cgidの設定
LoadModule mpm_worker_module modules/mod_mpm_worker.so LoadModule cgid_module modules/mod_cgid.so <IfModule worker.c> ServerLimit 16 StartServers 16 ThreadLimit 128 ThreadsPerChild 128 MinSpareThreads 2048 MaxSpareThreads 2048 MaxClients 2048 MaxRequestsPerChild 40000 </IfModule>
prefork + mod_cgiの設定
LoadModule mpm_prefork_module modules/mod_mpm_prefork.so LoadModule cgi_module modules/mod_cgi.so <IfModule prefork.c> ServerLimit 2048 StartServers 2048 MinSpareServers 2048 MaxSpareServers 2048 MaxClients 2048 MaxRequestsPerChild 40000 </IfModule>
これらに対して、hello.cgiという単純なCGIに対してweighttpで同時接続数300総接続数100万でベンチマークをしました。
hello.cgi
# curl example.jp/hello.cgi <html><head><title>hello</title></head><body><p>hello world</p></body></html>
weighttpによる同時接続数300総接続数100万
# weighttp -k -c 300 -n 1000000 example.jp/hello.cgi
worker + mod_cgidのベンチマーク結果
CPUの使用状況
ベンチマークの途中でCPUの使われ方を簡単に見てみると、pid31659であるmod_cgidのプロセスのCPU使用率が100%に張り付いており、それがオーバーヘッドとなって各CPUのidle値が高く、遊んでいるCPUがかなり多い状態になっています。mod_cgidはclone()システムコールが大量に発生し、システムCPUの使用時間が支配的になっている状態でした。
top - 16:03:23 up 15 days, 4:08, 6 users, load average: 0.22, 0.10, 0.04 Tasks: 493 total, 2 running, 488 sleeping, 0 stopped, 3 zombie Cpu0 : 18.9%us, 10.5%sy, 0.0%ni, 69.9%id, 0.0%wa, 0.0%hi, 0.7%si, 0.0%st Cpu1 : 15.4%us, 8.4%sy, 0.0%ni, 76.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu2 : 10.9%us, 6.0%sy, 0.0%ni, 83.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu3 : 4.0%us, 64.9%sy, 0.0%ni, 31.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu4 : 5.6%us, 37.2%sy, 0.0%ni, 57.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu5 : 5.3%us, 3.3%sy, 0.0%ni, 91.4%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu6 : 8.0%us, 19.1%sy, 0.0%ni, 72.2%id, 0.0%wa, 0.0%hi, 0.7%si, 0.0%st Cpu7 : 3.4%us, 9.7%sy, 0.0%ni, 86.6%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu8 : 1.0%us, 2.7%sy, 0.0%ni, 96.0%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu9 : 0.7%us, 2.3%sy, 0.0%ni, 96.7%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu10 : 0.3%us, 2.0%sy, 0.0%ni, 97.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu11 : 0.3%us, 3.3%sy, 0.0%ni, 96.4%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu12 : 3.6%us, 4.3%sy, 0.0%ni, 92.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu13 : 17.5%us, 44.4%sy, 0.0%ni, 38.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu14 : 18.8%us, 48.0%sy, 0.0%ni, 33.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu15 : 8.0%us, 15.7%sy, 0.0%ni, 76.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu16 : 10.3%us, 29.7%sy, 0.0%ni, 60.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu17 : 13.0%us, 28.8%sy, 0.0%ni, 58.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu18 : 2.3%us, 8.0%sy, 0.0%ni, 89.3%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu19 : 20.9%us, 37.4%sy, 0.0%ni, 41.8%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu20 : 15.1%us, 34.9%sy, 0.0%ni, 50.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu21 : 6.9%us, 28.0%sy, 0.0%ni, 65.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu22 : 1.7%us, 9.7%sy, 0.0%ni, 88.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu23 : 0.3%us, 1.3%sy, 0.0%ni, 98.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Mem: 32809072k total, 5593952k used, 27215120k free, 243008k buffers Swap: 16777208k total, 0k used, 16777208k free, 4258232k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 31659 apache 20 0 246m 46m 680 R 99.7 0.1 0:15.32 httpd 2746 nscd 20 0 1256m 5840 1796 S 13.6 0.0 14:40.55 nscd 30643 root 20 0 96492 9316 716 S 7.3 0.0 0:01.19 weighttp 348 apache 20 0 2703m 56m 5276 S 3.6 0.2 0:00.66 httpd 31661 apache 20 0 2703m 55m 4616 S 3.6 0.2 0:00.65 httpd 32471 apache 20 0 2703m 55m 4616 S 3.6 0.2 0:00.62 httpd 31662 apache 20 0 2703m 55m 4652 S 3.3 0.2 0:00.61 httpd 31665 apache 20 0 2703m 56m 5276 S 3.3 0.2 0:00.62 httpd 31667 apache 20 0 2703m 55m 4616 S 3.3 0.2 0:00.60 httpd 31683 apache 20 0 2703m 55m 4616 S 3.3 0.2 0:00.62 httpd 31741 apache 20 0 2703m 55m 5312 S 3.3 0.2 0:00.62 httpd
ベンチマークの結果
# weighttp -k -c 300 -n 1000000 exmaple.jp/hello.cgi weighttp - a lightweight and simple webserver benchmarking tool starting benchmark... spawning thread #1: 300 concurrent requests, 1000000 total requests progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 563 sec, 458 millisec and 904 microsec, 1774 req/s, 537 kbyte/s requests: 1000000 total, 1000000 started, 1000000 done, 999845 succeeded, 155 failed, 0 errored status codes: 999845 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 310377231 bytes total, 222409549 bytes http, 87967682 bytes data
mod_cgid専用のプロセスで待たされたせいか、リクエストは155 failedし、性能は1774 req/sという結果になりました。
prefork + mod_cgiのベンチマーク結果
CPU使用状況
mod_cgiではmod_cgidのようにオーバーヘッドとなるhttpdプロセスが存在しないため、各CPUは20%前後のidleで効率良く全てのCPUを80%近く使用できている事が分かります。上述した思惑通りの動きをしています。
top - 16:15:19 up 15 days, 4:20, 6 users, load average: 84.23, 19.72, 6.71 Tasks: 1720 total, 30 running, 1687 sleeping, 0 stopped, 3 zombie Cpu0 : 20.6%us, 64.5%sy, 0.0%ni, 14.6%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu1 : 19.2%us, 62.9%sy, 0.0%ni, 17.9%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu2 : 18.9%us, 60.3%sy, 0.0%ni, 20.5%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu3 : 18.9%us, 57.9%sy, 0.0%ni, 22.8%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu4 : 18.1%us, 55.9%sy, 0.0%ni, 25.3%id, 0.0%wa, 0.0%hi, 0.7%si, 0.0%st Cpu5 : 17.0%us, 54.3%sy, 0.0%ni, 28.3%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu6 : 20.1%us, 65.3%sy, 0.0%ni, 14.2%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu7 : 18.6%us, 63.5%sy, 0.0%ni, 17.3%id, 0.0%wa, 0.0%hi, 0.7%si, 0.0%st Cpu8 : 19.6%us, 61.1%sy, 0.0%ni, 18.9%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu9 : 19.0%us, 58.2%sy, 0.0%ni, 22.2%id, 0.0%wa, 0.0%hi, 0.7%si, 0.0%st Cpu10 : 17.9%us, 56.5%sy, 0.0%ni, 25.2%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu11 : 17.6%us, 54.8%sy, 0.0%ni, 27.2%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu12 : 18.1%us, 57.6%sy, 0.0%ni, 23.4%id, 0.3%wa, 0.0%hi, 0.7%si, 0.0%st Cpu13 : 17.4%us, 59.7%sy, 0.0%ni, 22.0%id, 0.3%wa, 0.0%hi, 0.7%si, 0.0%st Cpu14 : 18.2%us, 58.1%sy, 0.0%ni, 23.4%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu15 : 19.7%us, 56.6%sy, 0.0%ni, 23.0%id, 0.0%wa, 0.0%hi, 0.7%si, 0.0%st Cpu16 : 18.7%us, 56.1%sy, 0.0%ni, 24.9%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu17 : 19.0%us, 54.1%sy, 0.0%ni, 26.6%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu18 : 18.2%us, 59.6%sy, 0.0%ni, 21.9%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu19 : 19.3%us, 58.7%sy, 0.0%ni, 21.6%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu20 : 18.2%us, 60.1%sy, 0.0%ni, 21.1%id, 0.3%wa, 0.0%hi, 0.3%si, 0.0%st Cpu21 : 19.1%us, 57.8%sy, 0.0%ni, 22.8%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu22 : 19.7%us, 56.7%sy, 0.0%ni, 23.3%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu23 : 16.8%us, 57.2%sy, 0.0%ni, 25.7%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Mem: 32809072k total, 6643652k used, 26165420k free, 243056k buffers Swap: 16777208k total, 0k used, 16777208k free, 4627828k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2746 nscd 20 0 1256m 5844 1796 R 27.0 0.0 15:54.52 nscd 16886 root 20 0 96492 9320 716 S 14.6 0.0 0:02.68 weighttp 15873 apache 20 0 246m 47m 1680 S 2.6 0.1 0:00.39 httpd 15912 apache 20 0 246m 47m 1680 S 2.6 0.1 0:00.40 httpd 15974 apache 20 0 246m 47m 1684 S 2.6 0.1 0:00.39 httpd 15977 apache 20 0 246m 47m 1684 S 2.6 0.1 0:00.39 httpd 16015 apache 20 0 246m 47m 1680 S 2.6 0.1 0:00.39 httpd 16051 apache 20 0 246m 47m 1680 S 2.6 0.1 0:00.39 httpd 16128 apache 20 0 246m 47m 1680 S 2.6 0.1 0:00.38 httpd
ベンチマーク結果
# weighttp -k -c 300 -n 1000000 example.jp/hello.cgi weighttp - a lightweight and simple webserver benchmarking tool starting benchmark... spawning thread #1: 300 concurrent requests, 1000000 total requests progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 303 sec, 679 millisec and 13 microsec, 3292 req/s, 985 kbyte/s requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 306598163 bytes total, 220827929 bytes http, 85770234 bytes data
全てのCPUをを満遍なく使う事により、worker + mod_cgidの1774req/sの約2倍程の性能である3292req/sという結果が得られました。
MPMとCGIモジュール | 1秒間のリクエスト処理数(req/s) |
---|---|
worker + mod_cgid | 1774 |
prefork + mod_cgi | 3292 |
また、メモリの使われ方も、このベンチマークの範囲内ではworkerが5593952k usedであったのに対しpreforkでは6643652k usedとやはりpreforkの方が多くなっています。ですが、CoW等の影響によりプロセスの数の差ほどには差がないように思います。この辺りは、CGIの実装やアクセスパターンによって変わると思うので、ここで深くは言及しないことにします。
prefork+mod_cgiにもう少し負荷をかけてみる
さらに、prefork型にもう少し高い負荷をかけてみて、その性能を評価してみます。 同時接続数1000で総接続数1000万ぐらいでいってみましょう。すると以下のような結果が得られました。
# weighttp -k -c 1000 -n 10000000 example.jp/hello.cgi
CPU使用状況
もはやロードアベレージが1000近くなっていますが、コアが24個あることを考えるとシングルコアではロードアベレージ40程度とも考えられるのであまり気にしないでおきます。このような負荷をかけて、大体それぞれのCPUが80%前後使っている事がわかります。
top - 01:11:25 up 15 days, 13:16, 4 users, load average: 991.46, 989.26, 889.42 Tasks: 3483 total, 30 running, 3450 sleeping, 0 stopped, 3 zombie Cpu0 : 21.3%us, 63.2%sy, 0.0%ni, 15.2%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu1 : 19.4%us, 62.9%sy, 0.0%ni, 17.4%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu2 : 19.3%us, 60.5%sy, 0.0%ni, 19.9%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu3 : 18.1%us, 58.4%sy, 0.0%ni, 22.9%id, 0.3%wa, 0.0%hi, 0.3%si, 0.0%st Cpu4 : 17.8%us, 56.1%sy, 0.0%ni, 25.5%id, 0.3%wa, 0.0%hi, 0.3%si, 0.0%st Cpu5 : 16.8%us, 55.0%sy, 0.0%ni, 28.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st Cpu6 : 21.5%us, 63.8%sy, 0.0%ni, 14.4%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu7 : 19.9%us, 63.5%sy, 0.0%ni, 16.3%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu8 : 19.7%us, 60.3%sy, 0.0%ni, 19.7%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu9 : 18.6%us, 58.0%sy, 0.0%ni, 22.8%id, 0.0%wa, 0.0%hi, 0.6%si, 0.0%st Cpu10 : 17.5%us, 56.8%sy, 0.0%ni, 25.3%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu11 : 16.8%us, 55.9%sy, 0.0%ni, 27.0%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu12 : 19.3%us, 57.0%sy, 0.0%ni, 23.4%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu13 : 19.4%us, 57.3%sy, 0.0%ni, 23.0%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu14 : 19.5%us, 57.2%sy, 0.0%ni, 23.0%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu15 : 20.1%us, 56.2%sy, 0.0%ni, 23.3%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu16 : 17.2%us, 57.0%sy, 0.0%ni, 25.2%id, 0.0%wa, 0.0%hi, 0.6%si, 0.0%st Cpu17 : 17.8%us, 55.9%sy, 0.0%ni, 26.0%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu18 : 19.3%us, 57.9%sy, 0.0%ni, 22.5%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu19 : 19.6%us, 58.0%sy, 0.0%ni, 22.1%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu20 : 19.1%us, 58.6%sy, 0.0%ni, 22.0%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu21 : 19.0%us, 57.9%sy, 0.0%ni, 22.8%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu22 : 18.4%us, 57.3%sy, 0.0%ni, 24.1%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Cpu23 : 18.5%us, 57.6%sy, 0.0%ni, 23.6%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st Mem: 32809072k total, 13668324k used, 19140748k free, 244612k buffers Swap: 16777208k total, 0k used, 16777208k free, 10148824k cached PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2746 nscd 20 0 1784m 6204 1796 R 28.9 0.0 27:05.90 nscd 4369 root 20 0 116m 28m 716 S 16.8 0.1 5:46.81 weighttp 32273 root 20 0 16020 4184 964 R 4.8 0.0 1:57.66 top 1165 apache 20 0 246m 47m 1752 S 1.0 0.1 0:06.56 httpd 1177 apache 20 0 246m 47m 1748 S 1.0 0.1 0:06.45 httpd 1180 apache 20 0 246m 47m 1952 S 1.0 0.1 0:06.51 httpd 1186 apache 20 0 246m 47m 1744 S 1.0 0.1 0:06.35 httpd 1189 apache 20 0 246m 47m 1744 S 1.0 0.1 0:06.55 httpd 1191 apache 20 0 246m 47m 1752 S 1.0 0.1 0:06.34 httpd 1192 apache 20 0 246m 47m 1748 S 1.0 0.1 0:06.52 httpd 1201 apache 20 0 246m 48m 2312 S 1.0 0.2 0:06.50 httpd 1203 apache 20 0 246m 48m 2316 S 1.0 0.2 0:06.54 httpd
ベンチマーク結果
# weighttp -k -c 1000 -n 10000000 example/hello.cgi weighttp - a lightweight and simple webserver benchmarking tool starting benchmark... spawning thread #1: 1000 concurrent requests, 10000000 total requests progress: 10% done progress: 20% done progress: 30% done progress: 40% done progress: 50% done progress: 60% done progress: 70% done progress: 80% done progress: 90% done progress: 100% done finished in 3069 sec, 170 millisec and 722 microsec, 3258 req/s, 974 kbyte/s requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx traffic: 3063400835 bytes total, 2207144720 bytes http, 856256115 bytes data
かなりのCGI負荷にも関わらず、取りこぼす事なく3258req/sでているのは頼もしいですね。一つ上のベンチマークとほぼ同等の性能が出ています。
おそらくworkerだとこうはいかず、mod_cgidで待たされてタイムアウトとなる状況が多発し取りこぼす事が増えると思われます。また、メモリ面もこれだけ負荷をかけて4GB増加ぐらいなので、32GBマシンだとメモリ面では十分余裕があると思われます。
まとめ
以上のように、ApacheでCGIを使用する場合には、マシンの性能向上やCGIに対する大量のアクセスを考慮した場合に、workerやeventを使わずに従来型のpreforkとmod_cgiを使った方がスケールする状況があるということが分かりました。
余談ですが、性能の低いサーバ、例えばシングルコアのメモリ2GBといった環境では、mod_cgidがボトルネックにならずほとんど性能差が得られませんでした。ですので、このエントリでかいたようなbusyワーカーのピーク時の数やサーバマシンの性能が高い状況においては、preforkを採用するという選択肢も生じてくるという話でした。
特にCGIの実行可能な高集積Webバーチャルホスティング環境ではprefork+mod_cgiを使った方が良いでしょう。