毎秒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 に設定しても無効です。
2007/04/30(月)Microsoft-IIS がダメダメな件(303 See Otherを無視する件)
●対象 Microsoft-IIS/5.0(Windows 2000系付属)
IE で数々のダメを露呈している MS ですが、案の定IISもダメダメでした。adiaryは、IISサーバ + perlis.dll でも動作することになっているのですが、IISサーバ + perl.exe で動かないという報告がありまして、対応外なのですが調査してみました。どうも IIS の Cookie 処理が怪しいようです。
perlis.dll だと Image::Magick を使えない*1のだそうで。試しに動作させたらメモリを食う暴走して危険でした(汗)。
303 See Otherとは何であるか
原因を説明する前に、adiaryのログイン時の動きを確認しておきましょう。
(1) | adiary/?login | login フォーム |
(2) | adiary/ | パスワード認証をして set-cookie |
(3) | adiary/?login_auth | cookieがきちんとブラウザに保存されているか確認 |
(4) | adiary/ | ログイン後の画面 |
IISだと (3) の時点で「Cookieが有効になっていません」とエラーになります。
HTTPヘッダには 303 See Other というものがありまして、これはHTTP/1.1対応ならば使えます。HTTP/1.1対応かどうかというのは、HTTPクライアントとサーバが両方対応しているときに環境変数 SERVER_PROTOCOL=HTTP/1.1 が設定されます。
adiary(というより Satsuki-system)ではリクエストを redirect するとき、SERVER_PROTOCOL を参照して適切な HTTP ヘッダを返します。というのも、ログインのような POST をリダイレクトするとき、Location ヘッダを出せばいいと思っている cgi 作者もたくさんいらっしゃるようですが、事はそう単純ではありません。
Location ヘッダを同時に出せる(出力してもいい) HTTP/1.1ヘッダ には3種類あります。
ヘッダ | 移動後のメソッド | 意味 |
---|---|---|
301 Moved Permanently | そのまま | コンテンツが永久的に移動した |
302 Found | そのまま | コンテンツの一時的な移動 |
303 See Other | GETに変更 | コンテンツの一時的な移動 |
HTTP/1.1 においては、302で redirect してしまうと、フォームのデータを再度送信されてしまい永久にログインし続ける恐れがあります。クライアントの実装がマチマチですので難しいところですが、HTTP/1.1対応を名乗るクライアントはこのように実装されているハズです。
303 See Otherを無視するIIS
論より証拠、Microsoft-IIS/5.0 の実装を確認してみましょう。HTTP/1.0クライアント。
[console~]$ telnet 192.168.1.1 80
POST /adiary-1.30/adiary.cgi HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
action=login&id=test&pass=test
--------------------------------------------------
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Mon, 30 Apr 2007 06:17:02 GMT
Set-Cookie: session=%00%02%00sid%00aJXNNh2FJGd3%00id%00test; path=/adiary-1.30/;
Content-Type: text/html; charset=EUC-JP;
Locationヘッダは出ていません。これは正しい実装です。続いて HTTP/1.1 の接続です。この場合 SERVER_PROTOCOL=HTTP/1.1 が設定されていますので、cgiは303 See Other を返します。
[console~]$ telnet 192.168.1.1 80
POST /adiary-1.30/adiary.cgi HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Host: 192.168.1.1
action=login&id=test&pass=test
--------------------------------------------------
HTTP/1.1 100 Continue
Server: Microsoft-IIS/5.0
Date: Mon, 30 Apr 2007 06:18:47 GMT
HTTP/1.1 302 Object Moved
Location: http://192.168.17.104/adiary-1.30/adiary.cgi/nabe?login_auth
Server: Microsoft-IIS/5.0
Content-Type: text/html
Connection: close
Content-Length: 183
- 303 をなぜか 302 に書き換えている。
- Locationヘッダはきちんと出力されている。
- Set-Cookieヘッダを出力しない(cgi側ではきちんと出力している)
解決策は?
Microsoft-IIS で cgi 動作させるときは、SERVER_PROTOCOL を HTTP/1.0 と見せかけるしかないでしょう。*2
ほんとIEといい、IISといい、まったくデタラメなソフトばかりです。AN HTTPDの方が幸せなになるんじゃないかなぁ。perlis.dll でも普通に ImageMagick 使えましたし。
- [BUG] Location ヘッダーと組み合わせると CGI で Set-Cookie ヘッダーが無視される(Microsoft)
- RFC2616(日本語訳)
2007/04/26(木)perl の Encode で find_encode は使えるのか?
perl tips - Encodeを速く使う方法によると、find_encoding を使うと、Encode による文字変換が早くなるらしいということで実験してみました。
実験条件
- use utf8 しない(use utf8 はトラブルが多すぎてやってられないので)。
- 変換前も変換後も utf8 フラグは立っていないこと。
- 実験用データとして、42KBのテキストファイル(EUC-JP, 日本語半分ぐらい)を用意。
ソースはこんな感じです。引数に処理したい euc-jp のテキストファイル名を与えます。
use strict; use Benchmark; use Encode; use Encode::Guess qw(euc-jp shiftjis iso-2022-jp); my $euc = join('', <>); my $utf = $euc; Encode::from_to($utf, "euc-jp", "utf8"); my $euc_obj = find_encoding('euc-jp'); my $utf_obj = find_encoding('utf8'); my %test; $test{fromto_u2e} = sub { my $x = $utf; Encode::from_to($x, "utf8", "euc-jp"); }; $test{fromto_e2u} = sub { my $x = $euc; Encode::from_to($x, "euc-jp", "utf8"); }; $test{find1_u2e} = sub { my $x = $euc_obj->encode( $utf_obj->decode($utf) ); }; $test{find1_e2u} = sub { my $x = $utf_obj->encode( $euc_obj->decode($euc) ); }; $test{find2_u2e} = sub { my $x = $utf; Encode::_utf8_on($x); $x = $euc_obj->encode( $x ); }; $test{find2_e2u} = sub { my $x = $euc_obj->decode($euc); Encode::_utf8_off($x); }; $test{encode_u2e} = sub { my $x = $utf; Encode::_utf8_on($x); $x = encode('euc-jp', $x); print $x; }; $test{decode_e2u} = sub { my $x = decode('euc-jp', $euc); Encode::_utf8_off($x); }; timethese(1000,\%test);
結果は次のとおりになりました。
方式 | utf8 to euc | euc to utf8 |
---|---|---|
from_to | 100.26/s | 336.84/s |
find_encoding | 103.92/s | 380.20/s |
find_encoding + _utf_on/off | 357.21/s | 397.93/s |
encode/decode | 304.76/s | 350.68/s |
utf8からutf8(フラグ付き)への変換=utf8文字列の検証が入らないので速くなりましたが、ほかは大して変わりません。use utf8 しない環境では find_encodeを使うほどでもないなぁーという感じもします。
つまり、utf8を他の文字コードに変換する場合は、Encode::_utf8_on() + encode を使うと速い。短い文字列を多量に処理するのでなければ、ほかは大して変わらない。ということだと思います。
というわけで
早速adiaryに組み込んでみよう(笑)
2007/01/13(土)perl の import の働き
use と require と import の関係
perldoc perlmodによると、use というのは次と等価と書かれています。
use Module; use Module LIST;
はそれぞれ、
BEGIN { require Module; import Module; } BEGIN { require Module; import Module LIST; }
と等価になります。
BEGINとは?
BEGIN というのは、perl がそのソースファイルをロード中、発見次第最初に1回だけ実行する構文であるという意味です。少しフォローしておくと、モジュールのロードは、@INCにあるパスを順番に見てロードされますが、lib/ 以下に自作モジュールがあるとき、
push(@INC, './lib'); use Module;
はエラーとなります。先に use (BEGIN内構文)が実行されて、@INCにパスを追加する前にモジュールのロードが発生するからです。
use lib './lib'; use Module;
や、
push(@INC, './lib'); require Module; import Module;
とする必要があります。END { }なんてのもありますが、脱線はこれぐらいにして本題に入りましょう。
import がない場合
モジュールをロードし perl の名前空間に展開するならば、基本的には require すれば足ります。
---test.pm--- package test; sub new { return bless({}, shift); } ---test.pl--- require test; my $obj = new test();
ちなみに require では実行する度に処理されてしまうので無駄になりますから、
use test (); BEGIN { require test; } と等価
とします。import しなくても、ロードしたライブラリを使えるわけです。ではインポートとは何なのか?
import の役割
import は外部ライブラリの関数をあたかも自分の関数のように扱う仕組みです。正確に言えば、外部ライブラリの関数を自分の名前空間内に展開する仕組みが import です。例を見てみましょう。
---test.pm--- use Exporter; #←必須です package test; our @ISA = qw(Exporter); #←必須です our @EXPORT = qw($X &sum); our @EXPORT_OK = qw($Y); our $X = 10; our $Y = 20; sub sum { return ($_[0]+$_[1]); #渡された引数2つを加えて返す処理 } print "in test.pm : X=", \$X, "\n";
呼び出し側はこんな感じです。
use test; # BEGIN {require test; import test;} と等価
our ($X, $Y);
print "X=$X ", \$X, "\n";
print "Y=$Y\n";
print sum(5,3),"\n";
として実行してみます。
in test4.pm X=SCALAR(0x93bc130)
X=20 SCALAR(0x93bc130)
Y=
30
となります。test という名前空間内の変数 $X、関数 sum が、import した側で使えるようになっています。perl 的に正確に言えば、$test::x と &test::sum の示す実体が(内部的な実体のポインタが)それぞれ $MAIN::x、&MAIN::sum にコピーされたことになります。その証拠に、リファレンスをとって実体を調べると、ともに同じ実体である SCALAR(0x93bc130) を示しています。
import される関数は、モジュール側で @EXPORT に代入されているものになります。
import の引数は何?
import は配列引数を取れることになっています。これは、モジュール側で @EXPORT および @EXPORT_OK に代入されているもののうち、どれをインポートするか指定する役割があります。
ですから、さきほどのソースで、
use test qw(&sum $Y);
と変更すると次のような結果になります。
in test4.pm X=SCALAR(0x953e1b8)
X= SCALAR(0x94fd660)
Y=10
30
$Yがインポートされている反面、$X が test.pm 内の実体とは違うものを示していることが分かります。
関連記事
2006/12/27(水)Wikipedia API と JavaScript
Wikipedia APIとは
というふうに、サムネイル作成で使わせて頂いているSimple APIが提供するWikipediaの情報を取得するAPIです。これを adiary で Ajax 風に実装しようと思ったんですが、XMLHttpRequest ではセキュリティ上の問題でクロスドメイン(Simple API提供のWikipedia API)から直接データを取得することができないんですね。
よくよく調べると、JSONPというのがありまして、別にhandlerの名前を指定出来なくていいから対応してもらえないかなーという感じです。
例えば
呼び出すhandler名は固定で、引数も現在のAPIを文字列に変えたものでいいんですけど…。例えば、http://wikipedia.simpleapi.net/api?keyword=Google&output=xml&jsonp=1にアクセスすると、特定の関数をXMLを文字列として戻してくれるとか。
追記
JSONは既に対応済だったらしい(恥)