タグ別アーカイブ: fastcgi

mod_perlだけで重い処理を行うにはどうすればよいか

自称mod_perlエバンジェリストの おがた (@xtetsuji) です。

2012年のYAPCで「mod_perl王にオレはなる!」とか言っておいて、mod_perl業がおろそかになってしまって非常に恐縮です。最近は別の事に熱中していて…;という言い訳は置いといて、そろそろ本腰を入れてmod_perlしていきます。

Twitterを見ていたらこんなツイートがありました。

[tweet https://twitter.com/_sugarsan/status/301911501659320320]

これに対して @mod_perl_info

[tweet https://twitter.com/mod_perl_info/status/302079473338175488]

…;と返しましたが、mod_perlだけでこういう問題を解決するにはどうすればよいでしょう。

まずは system($cmd.’&’) 作戦から。

mod_perl の Perl CGI エミューレート環境 (Apache::Registry, ModPerl::Registry ファミリー) では、コードはある種のコンパイルをされ、特殊な内部パッケージ名の handler という名前のサブルーチンの中のコードとなります。mod_perl は一度メモリ上に作った特殊な内部パッケージの handler というサブルーチンを繰り返し呼び出すことでコストを抑えています。

mod_perlのPerl CGIエミュレート環境ということで、CGI.pm か CGI::Simple を使っていると仮定すると、system($cmd.’&’) されるのは print $cgi->header(); を行って、必要なHTMLなりテキストなりのアウトプットが完了した時点だと思います。そうでなければユーザに完全なレスポンスを返さずに長時間作業に入ることになってしまい、ユーザはブラウザの前でイライラすることでしょう。

これで問題解決…;と言えるかもしれませんが、いくつかの問題もはらんでいます。

  • この system($cmd.’&’); が呼び出されるアクセスが大量にやってきた場合、全ての子の数(たとえばprefork MPMの場合、MaxClientsの数)以上のアクセスが $cmd の長時間処理にかかりっきりになったら、それ以上のHTTP応答ができなくなる。
  • ユーザに返したいレスポンスを全て返した(HTMLであれば”</html>”までとか)としても、ブラウザはそのHTTPリクエストのレスポンスをたぶん待ち続けるはず。ブラウザがHTMLドキュメントの終了を推測できてDOM解析を完了できたり、KeepAlive等のタイムアウト設定で接続が切れることで、表向きは問題無いように見えるけど、処理としてはエレガントではない。

これをmod_perlだけで解決するにはどうすればよいでしょうか。

ネタ元ツイートの方は exec が CORE::exit と同等 (Apacheの子プロセス自体を終了させる) と言及していますが、実際の exec の挙動は一般的に「現在のプロセスを他のプロセスで置き換える」事です。execはexitと違い、mod_perl側が *CORE::GLOBAL::exec をいじるなどのサポートは行っていないので、元のexecそのものの挙動になります。preforkされた該当の子httpdは $cmd で定義されたコマンドに置き換わり、親から見たら子は突然消えたことと同じことになり、子が設定値的に足りなくなれば親が充足してくれます。ただ、以下の問題があります。

  • mod_perlハンドラhandler中で正規の処理を終わらせていないので、クライアントから見たら接続先のHTTPサーバが突然見えなくなるという乱暴な事になる (大きな問題ではないと思いますが)
  • ログ処理フェーズへ子プロセスが進めないので、当然ながらログが書かれない

前者は少々乱暴でも良いとは思いますが、ログが書かれない事を望むウェブ開発者はいないでしょう。

これら全てをmod_perlだけで穏当に解決するにはどうすればよいか。

まずmod_perlというかApacheは、レスポンスを返すフェーズの後続が以下であることを復習しましょう。

  • レスポンスフェーズ
  • ログ処理フェーズ
  • クリーンナップフェーズ
  • 次の処理待ち…;

長時間かかる仕事 $cmd を実行するのであれば、レスポンスフェーズをきちんと終わらせた後、ログ処理フェーズでログを書いてもらい、$cmd の実行はクリーンナップフェーズで行うのが良いでしょう。大量に来るのであればクリーンナップフェーズで exec して子httpd を $cmd にして、親に「子が親離れした」と思わせてもよい。クリーンナップフェーズであれば既にログも書いているので、exec で子が「いなくなっても」問題は無いでしょう。

それは以下のようなコードで実現できると思います。mod_perl の Registry ファミリーを使った Perl CGI エミュレーションスクリプト中を想定。

my $r = $cgi->r # $cgi is instance of CGI.pm or CGI::Simple
    or warn "non mod_perl environment";
if ( !$r ) { # Pure CGI
    system($cmd.'&'); # Pure CGI の場合は諦めてここで実行
} elsif ( $r->can('register_cleanup') ) { # mod_perl1
    $r->register_cleanup(sub { system($cmd.'&'); }); # exec($cmd) でも OK
} elsif ( $r->can('pool') { # mod_perl2
    $r->pool->cleanup_register(sub { system($cmd.'&'); }); # exec($cmd) でも OK
}
else { die "unknown"; }

もしくはmod_perl環境でもわざとレスポンスフェーズの末尾でsystem()をして、それに掛かった時間をログに載せるという手もあります。

my $r = $cgi->r;
my $start_time = time();
system($cmd.'&');
my $end_time = time();
$r->notes->set( cmd_proctime => $end_time - $start_time ) if $r;

Apche の LogFormat ディレクティブで %{cmd_proctime}n を使って参照できます。

$r->notes->set( cmd_proctime => $value ); でなく $ENV{cmd_proctime} = $value; として LogFormat ディレクティブで %{cmd_proctime}e として参照することもできますが、これはmod_perlではバッドノウハウ。この子プロセスが次に受けたリクエストがこのsystem()処理を行なわいスクリプトでも比較的永続的なプロセスである子は環境変数を持ち越してしまうので、前回の値がかかれてしまう。Pure CGIはそもそもApacheノートや環境変数でmod_log_configに値を渡すことはできない(これについて以前FastCGIでどうすべきかを論じた記事がありますが、それがPure CGIでも参考になると思います)。

mod_perlでも、Apacheの処理フェーズなどを駆使してここまで曲芸ができるという例でした。

上記コードは全て知識を元にした推測で書いています。実証実験はしていません。何か問題があればTwitter等でリプライください。

もっと汎用的にこの問題に取り組みたいということであれば、ジョブキューを使いましょう。例えば「第10回 ジョブキューで後回し大作戦―TheSchwartz,Qudo,Q4M(3):Perl Hackers Hub|gihyo.jp …; 技術評論社」や、書籍「モダンPerl入門」を参照すると良いと思います。

Apache上のPerl FastCGIはCustomLogにデータを書くことができるか?ついでにmod_perlでのお話

こんにちは、Apache mod_perl が大好物の おがた (@xtetsuji) です。

そういえば、2012年に開催されるYAPC::Asia Tokyo 2012にトーク「モダンmod_perl入門」を応募しました。この記事が気に入りましたら、ぜひとも「イイね!」お願いします。

mod_perlに偏執的なのも良くないなと思って、最近はFastCGIやPSGI/Plack以降のWAFも勉強しています。Perlから外に出られていないのがまだまだといったところですが…;。

それでも好きが講じて、Twitterを使って日々mod_perlの情報を収集しているのですが、今日こんな会話を見つけました。

この会話、要約するに

  • ApacheのFastCGI(mod_fastcgi)を使ってPerlスクリプトを動作させている
  • PHPのapache_note()関数のようにApacheでデータをログに乗せたい → たぶん LogFormat ディレクティブで “%{Foobar}n” 書式を使って CustomLog ディレクティブに指定したログにデータを書きたい
  • mod_perlは使いたくない → 嫌わないで(´Д⊂ヽ

FastCGIの仕様書や、FastCGIを使ったプログラムを書いたり読んだりしたことはありますが、私はそれほどFastCGIの仕様はわかっていません。ただ、当初これはFastCGIでは無理ではないかと思いました。

  • FastCGIは永続環境であるもののCGIの思想を踏襲しているわけで、Apacheとは独立した仕様であり(Nginxやlighttpdでも動きますし)、Apache HTTPリクエストフェーズでの情報交換を目的としたApache独自の「Apacheノート」にアクセスすることは出来ないだろう
  • FastCGI、今回はApacheのmod_fastcgi自体がこの要望を叶えられないのであれば、各種CGIとなるプログラム言語のライブラリレベルで努力しても、少なくとも普通の実装では無理だろう

環境変数ならと考えてはみたものの、perldoc FCGI を読んでみても実際に試してみても、use FCGI; して得られた my $request = FCGI::Request(); を試行錯誤していじってみても、環境変数をApacheリクエスト処理の後続に位置しているログ処理フェーズに伝えることはできませんでした。…;というかそんなのは当たり前ですね。詳しく説明できない程度の知識しかないのがもどかしいですが、FastCGIはApache本体とは独立したコンテナのようなデーモン(サーバ)を作るわけですから。気の利いたソケット(詳しくないけど言ってみたかっただけ)で両者が繋がっていれば話は変わってくるかもしれませんが、最初から勝算はありませんでしたし、うまくもいきませんでした。

perldoc FCGI をさらに読んでも、環境変数はまだしも、Apacheノートに関する記述は完全にありませんでした。敗北が見えはじめました。

基本に戻ってLogFormatのカスタムログ書式を復習してみました。ApacheのサイトにあるLogFormatのカスタムログ書式のマニュアルを読んでみると、任意データを受け取れそうな “%{Foobar}?” といった書式はこれだけありました。

  • %{Foobar}i:入力ヘッダ “Foobar” の内容
  • %{Foobar}n:Apacheノート “Foobar” の内容 (公式サイト上では「メモ」と書いていますが、この記事では「Apacheノート」で統一します)
  • %{Foobar}o:出力ヘッダ “Foobar” の内容

入力ヘッダはいじりようがない(FastCGIが動作するリクエストフェーズのずっと前の、Apacheのヘッダ解釈フェーズで既に処理されている)ので除外。Apacheノートも、上記考察から除外。

最後に残ったのは出力ヘッダ。「あ、出力ヘッダはFastCGIからApacheを通るし、もしかしたらいけるんじゃね?」と思って試してみました。

custom-output-header.fcgi

#!/usr/bin/perl
# ogata 2012/08/03

use strict;
use warnings;

use FCGI;

my $request = FCGI::Request();

while ( $request->Accept() >= 0 ) {
    print "Content-type: text/plainrn";
    print "X-Tetsuji: Hello! World.rn";
    print "rn";
    print "It is fine.rn";
}

LogFormatの設定

LogFormat "%h %l %u %t "%r" %>s %O "%{Referer}i" "%{User-Agent}i" "%{X-Tetsuji}o"" combined_xtetsuji

手元のApache2+mod_fastcgiにVirtualHostを作って、この combined_xtetsuji を CustomLog で使うようにして、custom-output-header.fcgi を叩いてみました。

以下がそのアクセスログ(IPアドレスは何となく伏せました)

xxx.xxx.xxx.xxx - - [03/Aug/2012:00:08:28 +0900] "GET /custom-output-header.fcgi HTTP/1.0" 200 245 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.57 Safari/537.1" "Hello! World."

お、最後に “Hello! World.” 出てる!

結果的に、出力ヘッダ “%{Foobar}o” を LogConfig ディレクティブに使えば、記録したいデータを出力ヘッダに含めることで、FastCGI でも Apache CustomLog ディレクティブに指定したログにデータが記録できることがわかりました。

…;が、この方法は、わざわざヘッダを見るユーザには記録したいデータが丸見えという明らかな欠点がありますね。これはちょっと…;

Apache FastCGIを使う限り、会話に登場した方が本当に満足する方法があるのか。もしくは上記で良いのか、ちょっとわかりません。私にはこれが限界でした。

あと一つ!mod_perlはPerl CGIの高速化環境として見ても、それほど悪いものではないですよ。FastCGIもmod_perlも永続環境という意味では同じような魅力と問題を合わせ持っていると思います。インフラエンジニアの専門家の方には「Apache自体はスリムであれ」等、鋭いご指摘等があるかもしれませんが。そういったご意見もぜひご教示いただきたいです。

ちなみに、Perl CGIの高速化環境 (PerlHandler Apache::Registry (mod_perl1) / PerlResponseHandler ModPerl::RegistryPrefork (mod_perl2)) のmod_perlでAapcheノートにデータを読み書きする方法は、ざっくり以下です。

  • use CGI; # CGI.pm は Perl CGIの高速化環境下のmod_perl{1,2}をサポートしている
  • my $cgi = CGI->new(); # 普通にインスタンス作成
  • $cgi->r というメソッドが用意されていて、これでmod_perlリクエストオブジェクト(Apache::Request (mod_perl1) / Apache2::RequestRec (mod_perl2)) を取得することができる
  • $cgi->r->notes->get(“BuzzMemo”); # Apacheノート “BuzzMemo” の値を取得
  • $cgi->r->notes->set(“BuzzMemo” => “This is fine”); #  Apacheノート “BuzzMemo” の値を設定

実際にmod_perlハンドラを生で書く場合には幾つかの注意点があったりしますが、今回は割愛(需要あるかな?)。CGI.pmではその部分をなんとなく吸収してくれています。ただひとつ注意点は、CGI.pmのドキュメント perldoc CGI では r というメソッドには一切触れていない、つまり非公開メソッドであること。とても歴史の古いモジュールですので、突然次の日に使えなくなる可能性は限りなく低いとは思いますが、使う場合はその点ご留意ください。

そんな部分も含め、mod_perlの興味深い世界に興味を持っていただいた方は、しつこいですがトーク「モダンmod_perl入門」にぜひ「イイね!」よろしくお願いします!YAPC::Asia Tokyo 2012 会場でのトークや(トーク動画とともに後日公開もされる)資料で、もっと興味深い世界をご紹介します。