すごいヘビーな負荷を受けているPSGIアプリケーションで「なんでこれで負荷があがるの?」的な現象があったので二つほどTipを。ちなみにこれは 2013/03/06時点での話なので、もしこれをあなたが大分将来に読んでいるのなら、状況に変更がないかちゃんと確認すること!

まずこのお話の前提:mod_perlなアプリをPSGIに移行したかった。アプリはmod_perlハンドラで書かれているので、Apache::RequestをPlack::Requestに書き換えたり、ハンドラ部分をオブジェクトにしてキレイにするくらいで、基本的な構造は何も変えてない(←ここポイント)。あとはApache側とか設定をもりもりいじって、PSGIファイルを書いて、Starletでデプロイして、パフォーマンスが30%くらい悪くなった。さて、犯人は誰でしょう?

まずアプリケーションを組む側が「やっちまったなぁ?」な件:Plack::Builder::mount()を多用しすぎると大分ペナルティがある。 例えば、/foo => MyApp::Handler::Foo, /bar => MyApp::Handler::Barみたいなマッピングで処理を移譲する場合、mountでこう書けなくもないけど、

builder {
enable ...; # ミドルウェア
mount "/foo" => MyApp::Handler::Foo->new;
mount "/bar" => MyApp::Handler::Bar->new;
....
};

これはmount()することによって関数コール一個分余計に呼ぶし、それを何回も繰り返すので当然・・・遅い。今回は元々独立したハンドラが一杯あって面倒くさかったからこう書いちゃったけど、負荷が高いとよくないので、こう書いたほうがよい。

# pseudocodeだからこのまま使うなよ!
my %handlers = (
"/foo" => MyApp::Handler::Foo->new,
"/bar" => MyApp::Handler::Bar->new,
....
);
sub handle_psgi {
my $env = shift;
my $handler = $handlers{ $env->{PATH_INFO} };
if (! $handler) {
return [ 404, [], [] ];
}

$handler->handle_request($env);
}

builder {
enable ...; # ミドルウェア
enable ...;
\&handle_psgi;
}; 

ベンチはkazeburo先生のこれとかを参考に。まぁ当然の結果だけど、約2倍も処理速度がはやくなるね!

この変更をする際に、PSGIファイルから呼ばれる基本のディスパッチ関数をオブジェクトに埋め込んでいたのを一つの関数にまとめた。メソッドコールのオーバーヘッドもいらない。

次はPlack::Requestの話。今回の案件ではほぼ全てのリクエストにクエリストリングがついてきて、それをパースしないといけないんだけど、Plack::Request->query_parametersが重い。思いの外重い。

俺がランチミーティングをしてたらまたkazeburo先生が色々あさってきてくれて、query_parametersの中でuri()を2回呼んでて、なおかつこの結果はキャッシュされてない。URIオブジェクトの生成を愚直に2度行っている事と、クエリストリングをパースしたいだけなのにそもそもURIオブジェクトの生成とか必要ないだろ!ってことで以下のようなパッチを当てると5倍から10倍速くなる:

Optimize ->query_parameters
 
これは現時点で最新版のPlack(1.0016)には入ってない。

ここまで見て、Devel::NYTProfの結果にはもう最適化できそうなところが何も無かった、というところまで落とし込めた。ふー、よかったね!

・・・と思ったんだけど、よくよく考えるとこのクエリのパースってCのほうが絶対速いしCPU使わないよね・・・。ってことでCPANをQueryStringで検索したらそれらしき物がなかったのでXSで書いた:Text::QueryString

ふー、やったぜ、やれやれ。と思っていたらIRCでURL::Encode(::XS)の作者に「それ、俺のモジュールでできるし、URL::Encode::XSを使えば爆速だぜ!」って言われたのでベンチしてみたらText::QueryStringが負けたorz 多分URIデコードの部分だと思う・・・。ともあれ、すでにそういうものが存在するのであれば別にいいや、ってことでText::QueryString 0.03で別にモジュールあるよ!ってドキュメントを書いてオワコンにして、以下のようにURL::Encodeでモンキーパッチしちゃうことに:
use Plack::Request;
use URL::Encode; # URL::Encode::XSを入れておく事

{
no strict 'refs';
*Plack::Request::query_parameters = sub {
my $self = shift;
my $env = $self->env;
my $query = $env->{'plack.request.query'};
if ($query) {
return $query;
}
$env->{'plack.request.query'} =
Hash::MultiValue->new(@{URL::Encode::url_params_flat($env->{'QUERY_STRING'})});
};

これは割とマイクロな最適化だったけど、少しでもCPU使用率減らしたかったので、まぁまぁよいかな、と。

ちなみに結局アプリをホストしているサーバー単位ではApache(mod_perl 1.3)ベースの時に比べると+10%くらいの負荷ペナルティでPSGI化ができた。

というわけでまとめると、Apacheはやっぱり基本的な所では高性能。リクエスト関連のパースはやっぱりガチガチに作り込まれてるだけあってすごく低い負荷でやってのける。Perlだけでそれを代替すれば当然作り込まれたCには必ず負ける。だからPlackにちょろっと移行しただけだとやはり負荷の問題がでてくる・・・だけどそれさえもいくつかの良識的な最適化、優秀なオペレーションエンジニアとの協同作業、ある程度の試行錯誤を許してくれるデプロイ手順の確立、そして「まー、Perl遅いならCで書けばいいや」(もしくはCベースのモジュールで代替しちゃえ)という割り切りで確実・着実にApacheにもほとんどひけをとらないパフォーマンスを発揮できる、ということでした。

以上本日のPlackパフォーマンスTipsでした。