mod_perl2でapacheモジュールを作ろう


mod_perlを使ってApache APIのコアな領域にちょっとだけチャレンジしてみようと思います。
ようするにこれのことです↓

はてなダイアリーより
巷ではmod_perlCGIプログラムを高速化するための技術だという解説も時折みられるが、CGIプログラムを高速化させるという点はmod_perlのもつ特徴の一部に過ぎず、実際にはApache APIPerlから利用することで、Apache内部の処理フェーズにフックする処理を実現できる、という点が本質である。

というわけで、早速ハック開始!


今回やりたいことは

  • mod_proxy(もしくはmod_proxy_balancer)に独自の振り分けロジックを導入したい
  • 振り分けロジックはmod_perl2を使ってApacheのリクエスト処理フェーズに細工をすることで実現したい

ということにしてみます。


余談ですが、色々とググってみたところ、
mod_proxy_balancerに独自振り分けロジックを追加できる気がするという、まったくそのまんまのテーマに挑戦されているブログを発見したのですが、こちらは「mod_proxy_balancerのlbmethodをCで実装する」ということらしく、結構難しそうなので、あっさりと諦めてみました!(弱)


さてさて、実際に実装する前にもうちょっとだけ前提条件を設定したいと思います。

  • フロントのWEBサーバは複数台に冗長化されているが、さらにその手前でreverse proxy的なサーバがいて、そいつが振り分け処理を行う
  • 振り分けに使うキーは「Cookieに焼かれたユニークなユーザコード」とし、これを使って「全てのユーザは毎回必ず同一のWEBサーバにバランスされる」ようにします
  • さらにバックエンドには複数のworkerプロセスが待ち構えていて、フロントで受け付けたユーザのリクエスト情報をいろいろ処理する〜〜

と、まぁこんな感じです。
要するに「フロントエンドのサーバたちがユーザごとにリクエストをキレイにまとまておいてくれると、バックエンド処理がラクなんだよねー」というような構成だと思ってください。


それではまずはhandlerのコードからみてみます。「Apache2::ProxyAllot」と名付けてみました。

package Apache2::ProxyAllot;

use strict;
use warnings;
use Apache2::RequestRec ();
use Apache2::RequestIO ();
use APR::Table ();
use Apache2::Const -compile => qw( OK DECLINED );
use CGI::Cookie;

sub handler {
        # requestオブジェクト
        my $r = shift;

        # cookie取得
        my %cookies = parse CGI::Cookie($r->headers_in->{Cookie});
        $cookies{Apache} and my $str = $cookies{Apache}->value() or return Apache2::Const::DECLINED;

        # cookie文字列を数値化
        my $allot;
        for(split(//, $str )){
                $allot += unpack("C*",$_);
        }
        # 任意の数値(ここでは5)で割ったあまりを求める→必ず0から4のいずれかになる
        $allot = $allot % 5;

        # 振り分け先webサーバを決定(192.168.1.0 から 4 のいずれか)
        my $webserver = 'http://192.168.1.' . $allot;
        my $uri = $webserver . $r->uri;

        # ApacheがPROXY経由のリクエストであると思わせるための細工
        $r->proxyreq(1);
        # 新しいURIをリクエストオブジェクトにセット
        $r->uri($uri);
        # mod_proxyのための特別なファイル形式
        $r->filename("proxy:$uri");
        # mod_proxyに後の処理を任せるのでhandlerを指定する
        $r->handler('proxy-server');

        return Apache2::Const::OK;
}

1;

見慣れないとちょっと気持ち悪く思いますが、mod_perl2ではいくつものApacheモジュールを別々にuseしなければなりません。mod_perl1系の時はもっとすっきりしていたんですが、いろいろ紆余曲折(喧嘩?)があってこうなったようです。

コードの中身は見ての通りです。コメントも書いておいたので解説は省略!


でこのハンドラモジュールをApacheのどのフェーズにフックさせるかですが、実は当初はuri_translateフェーズを考えていたのですが、Cookieデータをハンドリングするとなると、その1つ後のHeaderParseフェーズじゃないとだめなことがわかりました。Cookieの処理がはじまるのがHeaderの解析フェーズからなんですね。なるほど。

これがApache側の設定になります。

PerlRequire /var/www/perl/startup.pl
PerlHeaderParserHandler +Apache2::ProxyAllot

PerlRequireで呼び出しているstartup.plは、単に中でuse libでモジュールをおいたディレクトリを読み込んでいるだけです。
大事なのはその次の行です。先ほど上で作ったApacheモジュールをここで設定します。PerlHeaderParserHandlerで指定しているので、これはHederParseフェーズにて呼び出されることになります。


あと忘れてはいけないのはmod_proxyですね。ProxyRequest Onにします。

<IfModule mod_proxy.c>
ProxyRequests On
<Proxy *>
    Order deny,allow
    Allow from all 
</Proxy>
</IfModule>


これで狙いどおりmod_proxyに独自の振り分けロジックを搭載することができました。

実際に動かしてみると、Proxyしてるだけなので当然なんですが、動作は超高速です。よかった。