毎秒1000リクエスト を捌く超高速CMS「adiary」
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/06/12(月)定義済の関数を調べる
can メソッドの罠
Perlのマニュアルによれば、オブジェクトのメソッドを調べるには、すべてのクラスで継承される UNIVERSAL クラスの can メソッドを用いて
if ($obj->can("function_name")) { $obj->function_name(); } if (MYCLASS->can("function_name")) { MYCLASS->function_name(); }
としろと書いてあります。実際この判定はうまく動き、スケルトンシステム(Satsukiシステム)が今ほど本格的になる前のテンプレートに少し毛が生えた程度のころは、この can メソッドを使って関数呼び出しと変数の判別をしていました*1。
この can メソッドには罠があって、オートローダー(AUTOLOAD 関数/メソッド)が定義されているときcanメソッドは常に true を返します。AUTOLOAD というのは、もともとはPerlの関数を必要なときに動的ロードする仕組みであって、Satsukiシステムでは独自のオートローダーを定義しています*2。さらに、呼び出された関数が未定義のときに呼び出されるという仕組みを用いて、あたかも関数のような動作を動的生成ということも可能です。AUTOLOADは、未定義関数をロードして代理で呼び出すなり、あたかもその関数が存在するように振る舞うなりすればいいということになります。
# オブジェクト内のハッシュを返すメソッド # 呼び出した未定義関数はグローバル変数「our $AUTOLOAD」に入っている。 sub AUTOLOAD { # $AUTOLOAD='MYCLASS::FUNCTION' → $func='FUNCTION' my $func = substr($AUTOLOAD, rindex($AUTOLOAD, '::')+2); my $self = shift; return $self->{$func}; }
canメソッドが正常に働くとき、働かないとき
Satsukiシステムのオートローダーは、
Auth.pm Auth_auto.pm Auth_auto2.pm
という具合に、目的の関数が見つかるまで次々と連番のモジュールをロードしていくという方式で*3、AUTOLOAD内で次々とモジュールをロードしながら、canメソッドを使用して目的の関数が見つかったか調べる方法を取っていました。そして見つからなかったら失敗(die)するように作ってありました。
実際この方式はうまく動作していまして、このことからAUTOLOAD関数が存在するクラス内のcanはそのAUTOLOAD関数内では正常に判別できることがわかります。たぶんperl自身がcanの中でこの辺をこまめに判別しているようです。
AUTOLOADの暴走(無限ループ)
しかし更にややこしい問題が発生しました。SatsukiシステムのDBモジュール*4は、そのAPIが規定されていることが利点となり、どのような下位モジュールに対してもキャッシュを行うDB_cache.pmというものが存在します。この中では、select のみオーバーライドしキャッシュを行い、その他の関数はcanで調べてその関数が存在するときは下位のモジュールの関数を呼び出すという仕組みにしていました。この下位モジュールを呼び出す仕組みでAUTOLOADを使用していたのです。
分かりずらいので図式すると、今このサーバではPostgreSQLおよびこのキャッシュモジュールを使用しています。このときデータベースへのアクセスは、
(キャッシュなり)Diary.pm → DB_pg.pm (キャッシュあり)Diary.pm → DB_cache.pm → DB_pg.pm
となっています。Diary.pm からは、キャッシュがあろうがなかろうが、DBモジュールAPIを満たすものに対するアクセスしか行っていないため何ら違いはありません。
ここで、プログラム中にたまたま Diary.pm から DB_pg.pm に存在しない関数xyz()を読んでしまったのが問題の始まりでした*5。
モジュール | AUTOLOAD内 |
---|---|
DB_cache.pm | DB_pg.pm に xyz() が存在しないのに xyz() を呼び出している |
DB_pg.pm | xyz() のロードに成功していないのに*6、なぜかxyz()が存在すると思って呼びに行く |
結果として「DB_cache.pm → DB_pg.pm → DB_cache.pm → …」という無限ループに陥ってしまいました。DB_cache はその仕組み上、DB_pg.pm を動的に継承してしまうため、DB_pg への関数呼び出しは、例えDB_pg自身からであってもすべて DB_cache を経由します。それでこういうことが起こったわけです。
しかし、どちらのモジュールでも、こういうことを避けるために「canメソッドによって関数が実在するか調べていた」ハズです。調べてみると、どちらのcanも、どんなメソッドに対しても成功していました。ありもしないでたらめな関数でもなんでもif ($obj->can("xxx"))が成立していたわけです。
これが最初に言った、AUTOLOAD関数が定義されているときcanメソッドは常に true を返すという罠です。オブジェクトを動的に継承したために(実際に継承するのではなく、あたかも継承したようにみせかけた*7ために)、自分のクラス外のcanが常に成功したわけです。
解決策
はじめからcanなんて必要なかったんじゃないかというくらい、実に簡単な解決策でした。DB_pg.pm、DB_cache.pmをそれぞれ次のようにしました。
sub AUTOLOAD { (略) if (defined &$AUTOLOAD) { $can=1; last; }
sub can { my ($self, $func) = @_; my $func = $self->{_super_class} . '::' . $func; return (defined &$func); }
そしてたぶん、この方法の方が速く動作します。
注意
ここでは便宜上DB_pg.pm内に AUTOLOAD が存在するとして説明しましたが、実際には、Autoloader.pmからAUTOLOAD関数をインポートしています。
2006/05/29(月)POSTしたものを保存させると……
Content-Type技、色々
データをブラウザで開くのではなく、直接ファイルに保存させたい場合は、
Content-Type: application/xxx-data
という風にしておけば、大抵は未登録のコンテントタイプなので保存指示ウィンドウが出るようになります。
しかし、POST時にこの方法を用いてファイルを保存させようとすると、ファイル名選択ダイアログが出てきません(Firefoxにて確認)。各種blogツールにおいて、エクスポート時のメソッドがなぜGETなのか今まで疑問だったのですが、ようやく謎が解けました。
教訓:POSTメソッド発行時にファイルに保存させようとしてはいけない
おまけ
cgiのファイルなどを実行させずに、そのままのファイル名でブラウザで表示させたい場合は
AddType text/plain .cgi RemoveHandler .cgi
というふうにします(Apache2用)。http://adiary.abk.nu/tools/tdiary2adiary.cgiで使ってる技です。
追記。mod_perlの影響か、RemoveHandler がうまく働かなくなってましたので、次のようにしました。
AddType text/plain .cgi AddHandler default-handler .cgi
Content-Typeを変更せずにファイルとして保存させる
cgiの場合、もっとスマートにファイルとして保存させる方法を発見しました。
Content-Disposition: attachment; filename=export_file_name.txt Content-Type: text/plain; charset=(文字コード);
とすれば良いようです。
2006/05/25(木)タグの認識を正規表現で
一昔前の掲示板などで利用出来るタグを制限できるものがありますが、それと同等のモジュール(自作)をadiaryでは使用しています。属性値を認めると途端にタグの解析が難しくなります。
#!/usr/local/bin/perl use strict; my $inp = join('', <>); while($inp =~ /(<\w([^>"']|[=\s\n]".*?"|[=\s\n]'.*?')*?>)/s) { $inp = $'; print "$1\n"; }
だいたいこれでタグを認識できますが、IE/Firefox/Operaなどで色々確認した結果、壊れた(不正な形式の)htmlタグの解釈は統一されておらず、結局、その辺の仕様まで完全にサポートすることは非常に難しく、そこをつつけば、この手のタグ制限ルーチンの解析を避けて、ブラウザに対し不正なタグを認識させる(XSSする)ことが可能なようです。
2006/04/21(金)multipart/form-data
日記への画像添付に向けて、multipart/form-data の解析ルーチンでも作ろうかと思って調べ始めてみると……
- とりあえずネットで仕様書を検索。RFC1876, RFC2388だということは分かるが……見ても分かりにくいぞ、これは。
- 仕方ないので perl で書かれたサンプル(アップローダ)でも探してみると、CGI.pm を使用したものばかり見つかる。
- しょうがないので、ENCTYPE="multipart/form-data" なフォームで POST してデータを全部ファイルにはき出してみる。実に分かりやすい。
- 結局 boundary を認識しつつ読み出す必要があるので、perl 的にデリミタを書き換えてと思ったものの、例えば 100MB とか大きなファイルを受け取るとき、それだけメモリを食うこととなる。 → 常識的にはNG
- なんかいい方法ないかなぁーと再びネット内のcgiスクリプトを検索。…………見るんじゃなかった(笑)
どうしてネットに転がってる cgi (with perl) スクリプトってのは、有名どころ含め、汚いのばかりかなぁ(苦笑)*1
multipart/form-dataの例
というわけで例をアップしておきます。参考にどうぞ。アップロードしたファイルは、C++のソースです。