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

mod_perlだけで重い処理を行うにはどうすればよいか#interest_ae自称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入門」を参照すると良いと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください