毎秒1000リクエスト を捌く超高速CMS「adiary」
2007/09/26(水)PerlInterpMax の示すもの
daily dayflower などの情報をみて worker MPM な Apache を利用しても、同時に利用可能な Perlインタプリタ(mod_perlプロセス) は PerlInterpMax に制限されるように思っていました。デフォルトではこの値は 5 であり、一見少なく感じられます。
サーバDoS事件
ご存じのとおり本 blog.abk.nu サーバは、以前まで「データベースに接続できないエラー」を吐いてブログが表示されないことが多々発生していました*1。以前の設定値は次のとおりです。
mod_perl : PerlInterpMax 5 PostgreSQL : max_connections = 15
これでも、PostgreSQLへのコネクションが足りなくなりエラーとなっていました。PostgreSQLへのコネクションを保持するデーモンは adiary の他にありません。一体何が起こっていたのでしょうか。
PerlInterpMax の示すもの
ここで実験です。worker MPM(スレッド動作)で動く Apache で"PerlInterpMax 3" に設定し、TCPデーモンとTCP接続をするだけのモジュールを用意して、PostgreSQL への接続と同様にコネクションを(切断することなく)プールさせてみました。この状態で
~$ ab -c 10 -n 100 http://blog.yyy.xx/
として、接続負荷をかけてみます。このときのTCPデーモンの表示は次のようになりました。
[05] Connection from 127.0.0.1 [06] Connection from 127.0.0.1 [07] Connection from 127.0.0.1 [08] Connection from 127.0.0.1 [09] Connection from 127.0.0.1 [10] Connection from 127.0.0.1 [11] Connection from 127.0.0.1 [12] Connection from 127.0.0.1
8本の同時接続です。PerlInterpMax 3 であるのにです。不思議です。ここでもう一度よく PerlInterpMax の説明を読んでみます。
PerlInterpreter を各 httpd スレッド毎に一対一対応させる代わりに,mod_perl では設定自在なインタプリタプールを管理しています。この方式により,必要最小限な数のインタプリタを用意することでメモリ使用量を押さえることができます。
daily dayflower より
どうやらスレッドに対する設定項目であることが読みとれます。そもそもよく考えてみれば、プロセス間でPerlインタプリタを共有することなんて不可能です。この仮説を検証するため、Apache のプロセス数を2つに制限してみました。
<IfModule mpm_worker_module> StartServers 2 ServerLimit 2 </IfModule>
さてもう一度実験です。
[05] Connection from 127.0.0.1 [06] Connection from 127.0.0.1 [07] Connection from 127.0.0.1 [08] Connection from 127.0.0.1 [09] Connection from 127.0.0.1 [10] Connection from 127.0.0.1
今度はきちんと6本になりました。PerlInterpMax(3) * ServerLimit(2) = 6 ですから、計算に合っています。
PerlInterpMax の示すもの
mod_perl はスレッド動作時に 1プロセスごとに Perlインタプリタ をプールして保管します。1プロセスごとにその保管数まで mod_perl スクリプトを実行できます。プールされる実体は Perl 5.6 以降の ithreads そのものです。*2
この1プロセスごとのスレッドプール最大数を決めるのが PerlInterpMax です。worker MPM におけるプロセス数は ServerLimit で制限されます。つまり最大 PerlInterpMax × ServerLimit 個のmod_perl環境が同時に実行/保存されることになります。
それぞれデフォルト値では5、16ですから、80になります。この状況でデータベースへの接続をプールすると、あっと言う間にサーバ資源を食いつぶすことになるので注意が必要です。
注意
PerlInterpMax を小さくするときは、同時に PerlInterpStart も小さくします。PerlInterpStart より小さな値を PerlInterpMax に設定しても無効です。
2006/07/19(水)Perl CGIのキャッシュ環境
FastCGI対応は書き間違えだったのですが、どうせならということで対応してみました(対応版の配布はβ4以降になります)。
Perl CGIのキャッシュ動作比較
気づいたらあれこれ対応しすぎた感じがありますが、せっかくなのでどれが一番速いのか試してみしまた(笑) いずれの設定も全くチューニングも何もしていませんので、参考程度にお願いします。
CPU : Pentium3 800MHz (133*6/Coppermine) OS : FreeBSD 6.1-PRERELEASE Web : Apache 2.2 (prefork) ETC : DBキャッシュON TEST : ab -n100 -c4 http://127.0.0.1/xxx/adiary.cgi/user/
テストしたページには適当な日記が数件表示されています。結果は数回実行して頻出値に近い値を取りました。
動作モード | Req/sec | ms/Req |
---|---|---|
cgi | 2.10 | 477.270 |
speedycgi | 15.19 | 65.825 |
mod_speedycgi | 26.41 | 37.863 |
mod_fastcgi | 21.28 | 46.990 |
mod_fcgid | 21.21 | 47.157 |
mod_perl2*1 | 26.69 | 37.473 |
text-file | 196.7 | 5.084 |
mod_fastcgiとmod_fcgidは差がなくて、mod_perl/mod_speedycgiが一歩前に出てるという感じですね。worker動作(スレッドモデル)となると、対応しているのは mod_perl2 vs mod_fcgid だけ。mod_perl2 はいかんせん導入が面倒くさいので、手軽さでは mod_fcgid の方がよいのかもしれません。
本格的にパフォーマンスを求めたり、高負荷時のメモリ消費量の少なさを考えると mod_perl2 on worker MPM に優る選択肢はないのですが個人では必要ないでしょう。*2
ただ、どれも Apache にモジュールを組み込まないとならないので、お手軽に高速化したい場合はSpeedyCGI(ソースコード)をオススメします。パフォーマンスも(個人で使うには)十分ですし、Apacheからは完全にcgiとして見えるので(プロセスが完全に分離するので)、精神的にもよいです。
誰か同じことをMTの管理画面で試さないかなぁ(笑)
各方式の特徴
せっかくなので、簡単に仕組みと特徴を説明。
Perl/cgi
通常のcgi動作。Apacheが、該当のcgiプログラムを fork し実行します。Perl/cgiでは、cgiファイル(使用ライブラリファイル群)のコンパイル時間がとても長く、度々問題となります。*3以下の方式は、どれもこのコンパイル時間を減らすことで大きな高速化を果たしています。
SpeedyCGI(=PersistentPerl)/mod_speedycgi
Apacheからみたら通常のcgiプログラムですが、スクリプト1行目にPerlではなくSpeedyCGIを起動するための
#!/usr/bin/speedy
を記述することでSpeedyCGIが動作します*4。SpeedyCGIは内部的にPerlを呼び出し、実行終了後もそのプロセスを常駐させ(バックエンドと言う)、続いてリクエストが来たときに空いたバックエンドがあればそれに処理を行わせます。
スクリプト側から見た場合、Apacheプロセスと分離されることを除けば*5、全体的な動作はほとんどmod_perlと同じです。
歴史が浅いのか何なのかいまいちマイナーだけど、導入も容易で、mod_perlでは動作しないスクリプトも動作し、Apacheとは別プロセスになるので使うのも気楽です。SpeedyCGIをインストールしてあるレンタルサーバもみかけます。
mod_speedycgiはSpeedyCGIの管理プログラム起動コストを押さえるためApacheにモジュールとして組み込んだものです。それによりかなり高速化されますが、worker MPMには非対応なので注意が必要です。
FastCGI/mod_fcgid
SpeedyCGI的な考え方をより推し進めて、Apache内部にバックエンドプロセスを管理する機能をモジュールとしてインストールしたものです。SpeedyCGIでは、いくらバックエンドがあるとはいえ結局は別プロセスである SpeedyCGI(/usr/bin/speedyそのもの)を fork して実行する必要がありましたが、FastCGIではバックエンドを直接操作するため fork のオーバーヘッドがありません。
mod_fastcgi(FastCGI)とmod_fcgidの差ですが、前者をスレッド動作のApacheに組み込むのはあまりよろしくないとのことです。また巷の噂によると後者の方が速いとかなんとか。なお、FastCGIをスレッド動作のApacheに組み込んだとしても、CGIプログラム(Perlなど)自体は1プロセス=1クライアントとなります。
cgiスクリプトはFastCGI向けに改造する必要があります。
use FCGI; my $count = 0; my $request = FCGI::Request(); while($request->Accept() >= 0) { print("Content-type: text/html\r\n\r\n", ++$count); }
mod_perl
バックエンドどころか、「Perl自体をApacheに抱え込んでしまえばいい」という荒技的な解決策を提示するのが、mod_perl。荒技なだけに、もっともオーバーヘッドが少ない*6方式です。mod_perlの特徴として、Apacheの内部動作をスクリプト側から事細かに制御できますが、移植性(配布性)を考えたソフトではあまり使うことはありません*7。
mod_perlには、Apache 1.x系向けのいわゆるmod_perl(以下mod_perl1と表記)と、Apache 2.0/2.2系向けのmod_perl2があり全く互換性がありません*8。この互換性が度々問題となって、mod_perl1向け書かれたcgiをmod_perl2で動かそうとすると誤動作して厄介な問題を引き起こします。その代表選手とも言えるものがMovable Type*9。
スクリプト側から見た場合、mod_perl1やスレッド動作でないApache2ならば、およそSpeedyCGIと同じような感じです。ただ、Apache2が真価を発揮する worker(スレッド)動作でのmod_perl2は、Perlスクリプト自体もスレッド動作することで高い効率が得られる一方、カレントディレクトリを指定出来ない(chdir()できない)こと*10、あらゆるスクリプトがマルチスレッドでライブラリ空間(メモリ空間)を共有すること*11が大きなネックになります。後からこの対応を行うことは困難であり、これがmod_perl2向けスクリプトがほとんど存在しない原因になっていると思われます。
余談
Apache2(Prefock) + mod_perl2 のときは、次のようにすると自動で chdir されます(いずれか1つ選択)。
PerlResponseHandler ModPerl::RegistryPrefork PerlResponseHandler ModPerl::PerlRunPrefork
2006/04/15(土)mod_perl で chdir
mod_perl2 (on Apache 2.x)で無理矢理MT(Movable Type)を動かそうとしたり、古いスクリプトをmod_perl2で動かそうとする人たちがいまして。そういうページの中で、mod_perl2はカレントディレクトリをスクリプトのdirに変更しないというのが日本ではなぜか迷信のごとく広まっています。……いやある意味では正しいのですけど。例えばこちらのページでは
PerlFixupHandler "sub { \ chdir('/project/sfo/www/www.sfo.jp/lib/mt'); \ return OK; \ }" PerlResponseHandler ModPerl::Registry
とやって明示的に chdir しています(別にやり玉に挙げているわけではなくて、どこのサイトを見てもおよそこんな感じです)。そもそもなぜ、ModPerl::Registry がスクリプトの場所にカレントディレクトリを変更しないかといえば、worker モデルの場合(Apacheがスレッド動作モデル)の場合、chdir しても途中でカレントディレクトリが変わる可能性がある=あまり意味がないからです。だから chdir を明示的に発行するときは、Apache が prefork(従来のプロセスをforkして並列動作する方式)で動いてることを期待しています。
ここからが本題。mod_perl2のマニュアルによるとmod_perl2にはPrefork専用のハンドラーがあり、
PerlResponseHandler ModPerl::RegistryPrefork PerlResponseHandler ModPerl::PerlRunPrefork
と(どちらか一つを)書けば chdir してくれるそうです。まあ英語のマニュアルだから自分もcgi開発でもなければ読まなかったのですが、日本での普及率をGoogle先生に聞いてみるとこんな結果になります(汗)
というわけで、以降普及することを期待して終わり(笑)