mod_perl 2.0.2 へのマイグレーション

mod_perl 2 が Stable リリースになって気がつけば半年以上経った様子。はてなではこれまで mod_perl 2 は mod_perl 2.0RC-4 (1.99) とかを使ってましたが、ぼちぼち 2.0 にちゃんと移行した方がいいかなと、重い腰を上げつつ作業してます。

現在、mod_perl には互換性のない三つのバージョンが存在してます。

1.0 は Apache 1.3 の API に対応している mod_perl、1.99 と 2.0 は Apache 2.0 API に対応している mod_perl です。Apache 2.0 がそれまでのバージョンとの API の互換性を捨ててアーキテクチャの見直しが行われたのをきっかけに、mod_perl後方互換性を捨ててスクラッチから書き直された結果の産物です。

1.99 というのは、mod_perl 2.0 が Stable リリースされる直前までのバージョンで、言うなればベータ版。ベータが取れるときにいくつかの API が変更になったり、名前空間Apache から Apache2 に変更されたりといった経緯があって、その両者はだいたい同じなんだけど互換性がありません。なので、1.99 を使ってたアプリケーションが動いてるサーバーを 2.0 にアップデートするとそのアプリケーションは動かなくなってしまいます。

例えば Catalyst とかは Catalyst-Engine-Apache の中で、MP19、MP20、MP13 と三つの Engine を用意してその差異を吸収していたりします。

さて、みなさんご存じの CGI.pm は、CGI という名前の割には mod_perl で動くと mod_perlAPI を使ったりするようにソースが書かれているんですが、その最新版には

if (exists $ENV{MOD_PERL}) {
  # mod_perl handlers may run system() on scripts using CGI.pm;
  # Make sure so we don't get fooled by inherited $ENV{MOD_PERL}
  if (exists $ENV{MOD_PERL_API_VERSION} && $ENV{MOD_PERL_API_VERSION} == 2) {
    $MOD_PERL = 2;
    require Apache2::Response;
    require Apache2::RequestRec;
    require Apache2::RequestUtil;
    require Apache2::RequestIO;
    require APR::Pool;
  } else {
    $MOD_PERL = 1;
    require Apache;
  }
}

というコードがあります。mod_perl のバージョンを評価して、対応するバージョンのモジュールをロードしています。しかし、Catalyst の場合とは違って 1.99 には対応してくれてません。少しまえのバージョンまでは 1.0 と 1.99 に対応したコードになってましたが、ここ最近のアップデートで上記のコードに入れ替わりました。なので、CGI.pm を使ってる mod_perl 1.99 なコードは、CGI.pm のバージョンアップと共に動かなくなってしまうという罠があったりします。

1.99 は mod_perl のサイトにダウンロードのリンクがないし、この CGI.pm の事情なども見るに、もうおまえらいい加減 2.0 使えよなっていう開発コミュニティの意向なのかなあという感じがして、2.0.2 へ移行しようと作業をしているというわけであります。ついでなので、CGI.pm とか使ってる悪しき習慣をやめて Apache2::Request を使うようにとかフレームワークの改良もしてます。

さて、いざ作業しようと思ったわけですが実は僕は mod_perl API を直接使うときは 1.0 ばかりいじってて、 mod_perl 2.0 にはあんまり詳しくないけどフレームワークが抽象化してくれてるのをいいことにその辺がおざなりになっていました。のでこの正月にその辺りのドキュメントを漁っていました。

1.0 → 2.0 では、それほど大きく変わっていなくて苦労というほどのものはなかったのですが、1.0 のときと同じノリでやってるとはまったりする箇所はやっぱりあります。例えば 1.0 のときは

package Sandbox::Hello;
use strict;
use warnings;
use Apache::Constants qw(OK);

sub handler ($$) {
    my ($class, $r) = @_;
    $r->send_http_header('text/html');
    $r->print('Hello, World!');
    return OK;
}

1;

と、割と深いことを考えなくても handler() を定義して $rAPI を使って終了という具合でしたが 2.0 では $r に相当するオブジェクトのメソッドが複数のモジュールに分散していて、その対応をまず把握する必要があります。

  • Apache2::RequestIO
  • Apache2::RequestRec
  • Apache2::RequestUtil
  • Apache2::Server
  • Apache2::ServerUtil

この辺。それぞれのモジュールをロードすると、$r で扱える API が増えるので、それを使ってプログラミングしていきます。あと、send_http_header がなくなってるとか、OO でハンドラを書きたければ attribute で method を指定しなさいとかという細かい変更もあります。結果 Hello, World は

package Sandbox::Hello;
use strict;
use warnings;

use Apache2::ReqeustRec();
use Apache2::RequestIO();
use Apache2::Const -compile => 'OK';

sub handler : method {
    my ($class, $r) = @_;
    $r->content_type('text/plain');
    $r->print('Hello, World!');
    return Apache2::Const::OK;
}

1;

という案配になります。

このメソッドの変更とか、所望のメソッドがどこのモジュールに定義されてるかとかは ModPerl::MethodLookup を使って調べられるようになっていて

$ perl -Mmod_perl2 -MModPerl::MethodLookup -e print_method send_http_header
'send_http_header' is not a part of the mod_perl 2.0 API
use 'content_type' instead. To use method 'content_type' add:
        use Apache2::RequestRec ();

こんな感じでコマンドラインから都度調査しつつマイグレーションしていく形になります。ちなみに、API とモジュールの対応とかめんどくさかったら startup.pl で

use ModPerl::MethodLookup;
ModPerl::MethodLookup->preload_all_modules();

とすることもできます。

The function preload_all_modules() preloads all mod_perl 2.0 modules, which implement their API in XS. This is similar to the mod_perl 1.0 behavior which has most of its methods loaded at the startup.

だそうです。

という具合でぼちぼちやって、既存のアプリケーションはだいたい動くようになりました。

これで mod_perl 2 で追加された機能、例えば(Apache::Filter とか使わないでもOKな)I/O フィルタリングやハンドラのフック箇所の指定機能とかも安心して使えるし、サーバーやモジュールのバージョンアップにも気を遣わなくて済みそうです。prefork と worker でどれぐらい差がでるかのベンチマークとかもやってみたい。

10分で完了、mod_perl 2.0 で Hello, World!

10分で、といいながらたぶん mod_perl と Apache2 をビルドするのに 10 分以上かかるという罠。まあいいや。以下のやり方で LinuxMacOSX どちらでもちゃんと動くと思われます。

まず、mod_perl 2.0 のインストール。DSO でもいいけど、ここでは Apache にスタティックに組み込みます。

  • インストールディレクトリは /usr/local/httpd_mp2 に。
  • MPM は prefork。perl を thread 有効でビルドしてるなら mpm=worker でもいいと思います。
$ wget http://perl.apache.org/dist/mod_perl-2.0-current.tar.gz
$ wget http://www.apache.org/dist/httpd/httpd-2.0.55.tar.gz
$ tar zxvf mod_perl-2.0-current.tar.gz
$ tar zxvf httpd-2.0.55.tar.gz
$ cd mod_perl-2.0.2
$ perl Makefile.PL ?
MP_USE_STATIC=1 ?
MP_AP_PREFIX=../httpd-2.0.55 ?
MP_AP_CONFIGURE="--prefix=/usr/local/httpd_mp2 --with-mpm=prefork"
$ make
$ make test
$ sudo make install

mod_perlApacheコンパイルは一緒にやってくれるので、mod_perl、ウェブサーバー共にインストールはこれで完了。*1

次に、Apache の設定。highperformance.conf をコピーして書き換えます。

$ cd /usr/local/httpd_mp2/conf
$ sudo cp httpd.conf httpd.conf.default
$ sudo cp highperformance.conf httpd.conf
$ sudo vi httpd.conf

内容の方は、こんな感じで。

Listen 80
ServerRoot /usr/local/httpd_mp2
DocumentRoot /usr/local/httpd_mp2/htdocs

User  nobody
Group nobody

MaxClients       150
StartServers     5
MinSpareServers  5
MaxSpareServers  10
MaxRequestsPerChild 100
ErrorLog logs/error_log
TransferLog logs/access_log
HostnameLookups Off

<Directory />
    Options FollowSymLinks
    AllowOverride None
</Directory>

次に、mod_perl の設定を追記します。

  • localhost の 80 番にアクセスしたら Sandbox::Hello ハンドラが動くように。
  • startup.pl の置き場所は適当に。
NameVirtualHost *:80
<VirtualHost *:80>
    ServerName powerbook.bloghackers.net:80

    PerlModule mod_perl2
    PerlRequire /Users/naoya/perlhacks/Apache2/startup.pl
    PerlModule Sandbox::Hello
    <Location />
        SetHandler perl-script
        PerlResponseHandler Sandbox::Hello
    </Location>
</VirtualHost>

conf で指定した場所に startup.pl を作ります。中では、とりあえずライブラリパスの設定をするだけ。アプリケーションが大きくなるにつれてこのファイルも育っていくことでしょう。

#!/usr/local/bin/perl
use strict;
use warnings;
use lib qw (/Users/naoya/perlhacks/Apache2/lib);

1;

startup.pl で指定した lib ディレクトリの中に Sandbox/Hello.pm を作って、Hello, World! なコードを書く。

package Sandbox::Hello;
use strict;
use warnings;
use Apache2::RequestRec;
use Apache2::RequestIO;
use Apache2::Const -compile => 'OK';

sub handler : method {
    my ($class, $r) = @_;
    $r->content_type('text/plain');
    $r->print('Hello, World');
    return Apache2::Const::OK;
}

1;

これで apachectl start して Apache を立ち上げる。http://localhost/ にアクセスして Hello, World! が出力されれば完了です。

Apache2::Request を使いたくなったら

$ sudo perl -MCPAN -e shell
cpan> install ExtUtils::XSBuilder
cpan> look Apache2::Request
# perl Makefile.PL --with-apache2-apxs=/usr/local/httpd_mp2/bin/apxs
# make
# make test
# make install

という感じでインストールして、httpd.conf に

LoadModule apreq_module modules/mod_apreq2.so

を追加します。

これで、

...
use Apache2::Reqeust;
...

sub handler : method {
    my ($class, $r) = @_;
    my $apr = Apache2::Request->new($r);
    my $mode = $apr->param('mode');
    ...
}

という具合にクエリパラメータが拾えるようになります。

Enjoy!

*1:バックスラッシュが ? になっちゃってます。読み替えてね。