毎秒1000リクエスト を捌く超高速CMS「adiary」
2008/11/27(木)Shibuya.pm#10 閲覧中
ここでみかけたので、Shibuya Perl Mongersテクニカルトーク#10をみてます(現在進行形)。ブラウザひとつで遠方のWorking groupがみられるのは便利。*1
たまたまですが、Perlアプリケーションフレームワークの話題。知らないだけでいくつかあるらしい。そして考えることはみんな一緒らしい。*2
こういうの見てると、adiaryとそのアプリケーションフレームワークである Satsuki-system のメンテ時間が取れないのが悔しくなってきますね。高速性と可搬性ならまけないのに(苦笑)
2008/02/08(金)2行に渡るメールヘッダの正しい処理
問題発生
ある方より、メール投稿利用時に長い日本語タイトルを付けると、途中に半角スペースが入ってしまうというバグ報告を受けました。
Subject: =?ISO-2022-JP?B?GyRCPmFHKz5hRys+YUcrPmFHKz5hRys+YUcrPmFHKz5hGyhC?= =?ISO-2022-JP?B?GyRCRys+YUcrPmFHKz5hRys+YUcrPmFHKz5hRys+YUcrPmFHKz5hGyhC?=
のようにMIMEエンコードが長くなり空白が入ったときに、この空白がデコードされてもそのまま残ってしまうことが原因です。やっつけならこの空白を除去するだけで良いのですが、やっつけプログラムは最低なのできちんと調査してみました。
長いメールヘッダの規定
RFC 2822「Internet Message Format」には次のように規定されています。
2.1.1. 行の長さの制限
それぞれの行の文字はCRLFを除いて、決して998文字以下でなければならず(MUST)、78文字以下であるべきである(SHOULD)。
2.2.3. 長いヘッダフィールド
それぞれのヘッダフィールドは(略)単一論理行である。しかしながら便宜のため、文字数制限のために、ヘッダフィールドの一部であるフィールドボディは複数行に分割して表現でき、これをfoldingと呼ぶ。どこであれfoldingのホワイトスペース(WSPではない)があるとき、WSP(SPACE/0x20またはTAB/0x09)の前にCRLFを挿入してよい。例えば、ヘッダフィールド:
Subject: This is a testは以下のように表現できる。
Subject: This is a test(中略)
このfoldingされたヘッダフィールドの複数行表現を1行表現にする過程をunfolding と呼ぶ。unfolding はWSPがすぐ後に続くあらゆるCRLFを単に削除することでなされる。
つまりRFCの規定に従い、最初に例示したメールヘッダは次のように復元されます。
Subject: =?ISO-2022-JP?B?(略)?= =?ISO-2022-JP?B?(略)?=
ここまでは正しい復元過程であり、やっつけ実装として考えた「単純に複数行ヘッダの2行目以降の先頭空白の除去」は誤りであることがわかります。実際、長い英字タイトルに1文字だけ空白を入れた「aa(略)aaa bbb(略)」というメールを送信したところ
Subject: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaa ←※実際にはここまで1行
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
というように78文字の制限を超えてメールヘッダが作られました。単純な先頭空白除去では、このような改行位置が半角の文になっているメールを受信した際に問題が発生します*1。そもそもRFCの規定で「CRLFを単に削除する」となっているのですから、そこは正しく従わねばなりません。
MIMEの規定
では「長い日本語タイトルの途中に空白が入る問題」はどこに実装の誤りがあるのでしょうか。これについて、RFC 2047「非ASCIIテクストのためのメッセージヘッダ拡張」を参照します。
6.2. 'encoded-word' のディスプレイ
複数の 'encoded-word' を含むある特定のヘッダフィールドを表示する時は、隣接する 'encoded-word' を分離するあらゆる 'linear-white-space' は無視される。
'encoded-word'は、いわゆるMIMEエンコードのことです。linear-white-space はRFC 822により次にように定義されています。
HTAB = <ASCII HT, horizontal-tab> ; (文字コード:9) LWSP-char = SPACE / HTAB ; semantics = SPACE linear-white-space = 1*([CRLF] LWSP-char) ; semantics = SPACE [CRLF] は CRLF があってもなくても良い。(0回以上1回未満の繰り返し) 1*(element) はelementの少なくとも1つ以上の繰り返し。
まとめると「=?ISO-2022-JP?B?(略)?=」ブロックと「=?ISO-2022-JP?B?(略)?=」ブロックの間にある、"CR LF SPACE(又はTAB)"の1つ以上の繰り返しは削除すべしということになります。
正しいメールヘッダ復元の実装
Perl正規表現で次にように行います。
$hl = (1つのヘッダフィールド(1行または複数行)) while ($hl =~ /(.*?=\?ISO-2022-JP\?B\?[A-Za-z0-9\+\/=]*\?=)(?:(?:\r\n)?[\t ])+(=\?ISO-2022-JP\?B\?[A-Za-z0-9\+\/=]*\?=.*)/i) { $hl="$1$2"; } $hl =~ s/=\?ISO-2022-JP\?B\?([A-Za-z0-9\+\/=]*)\?=/ &ベース64デコード($1) /ieg;
ベース64デコード関数は別に用意してください。(たとえばこれ)
(?:(?:\r\n)?\r\n[\t ])+
上記が、linear-white-spaceに相当します。ただ、メールデータを読み込む状況によっては \n \r \r\n のいずれになってプログラムに渡されるかわかりませんので、若干厳密な実装ではなくなりますが、
$hl =~ s/\r\n|\r/\n/g;
しておいてから、
(?:\n?[\t ])+
の方が良いと思います。さらに言えば "RFC 2822" の規定を先に適用して、先に改行コードを除去してから、MIMEエンコード間の空白除去を行っても動作としては変わりません。それをベースにした adiary の実装を最後に示します。
$hl =~ s/\x00//g; my @buf; # MIMEデコードをしてエスケープ表記"\0 num \0"に置き換え $hl =~ s/=\?[\w\-]+\?B\?([A-Za-z0-9\+\/=]*)\?=/ push(@buf, $self->base64decode($1)); "\x00$#buf\x00"; /ieg; # RFC 2047 $hl =~ s/\x00[\t ]+\x00/\x00\x00/g; # エスケープ復元 $hl =~ s/\x00(\d+)\x00/$buf[$1]/g;
2008/01/15(火)Apache 2.2/worker で ServerLimit が効かない?
PerlInterpMaxを解析後、正しくApacheに ServerLimitを設定しているにも関わらず、またデータベース接続エラーが。ps ax すると、httpd(Apache)のプロセスが9個ぐらい起動してました(汗*1
あれバグ? それとも仕様??
検索してみても、同様の報告しかみつかりませんでした*2。調べてみると
MaxRequestsPerChild 1000
を越えたときに、古いプロセスがうまく殺せていないようです。mod_perl が影響しているかどうかは不明です。とりあえず、MaxRequestsPerChild 0 にして様子をみようと思います。
推測
MySQL環境で同様の設定もしても再現しないことから、おそらく PostgreSQL で Connection を永続させているため、ネットワークコネクションがすべて切れない(Apacheがコネクション=クライアント接続中)と勘違いしてプロセスが Kill されないせいと思われます。*3
PostgreSQL への接続に UNIX ドメインソケットを使っているせいでタイムアウトの設定ができない(汗