mod_proxy_balancerにしてみる

昨日つくったApacheモジュールですが、その後いろいろ調べてみたら、まだまだ改善の余地があるっていうか、大幅に見直した方が良いんでないかい?と思い出したので、もうちょっとしつこく検討してみます。


昨日つくったやつの主な流れを再度整理すると、

  • あらかじめmod_proxyでフォワード型のproxy機能を準備しておく
  • 続いてApache2::ProxyAllot(←昨日つくったやつね)もPerlHeaderParserHandlerとして事前に準備しておく
  • ユーザからは普通にリクエストを受け付けてよい(proxy設定とか無しでいいです)
  • 受け付けたリクエストはApacheのHeder解析フェーズでProxyAllotにとっ捕まります
  • ここで捕捉されたリクエストは、バックエンドのどのサーバに振り分けるのかを勝手に決められます
  • さらに後段でmod_proxyを騙す(?)ためにproxy経由のリクエストだったかのように細工を施します
  • なにも知らないApacheくんは応答フェーズのタイミングでmod_proxyを使って上手に振り分け処理を行ってくれます

というようなあこぎな手法でした。

あこぎで回りくどい割にはmod_proxy_balancerは使えてないので、バックエンドのどれかが落ちると、どうしようもなかったりします。
それに振り分けロジックも「ユーザのCookieで云々」ってやつだけしか実装していないので、ちょっと使い道が限られてしまうなぁ、と思いました。


というわけで、mod_proxy_balancerに対応!さらに振り分けロジックも3パターンに増強させてみました!

まずはソースと設定ファイルを晒してみます。

package Apache2::ProxyAllot;

use strict;
use warnings;
use CGI::Cookie;
use Apache2::RequestRec ();
use Apache2::RequestIO  ();
use Apache2::Connection ();
use APR::Table          ();
use Apache2::Const -compile => qw( OK DECLINED );
use YAML qw 'LoadFile';
use Net::CIDR::Lite;
use Data::Dumper;
# yaml confファイル用のグルーバル変数
our $conf;

# ハンドラ
sub handler {

    my $r = shift;
    my $allot;
    my $yaml = $r->dir_config('config_yaml');
    $conf ||= LoadFile($yaml);

    # UNIQUE_CODE モード(ユーザを必ず同一のサーバに割り振る機能)
    if ( $conf->{TYPE} eq 'UNIQUE_CODE' ) {
        my $allot_num = $conf->{UNIQUE_CODE}->{allot_num};
        my %cookies   = parse CGI::Cookie( $r->headers_in->{Cookie} );
        $cookies{Apache} and my $str = $cookies{Apache}->value()
          or return Apache2::Const::DECLINED;
        for ( split( //, $str ) ) {
            $allot += unpack( "C*", $_ );
        }
        $allot = $allot % $allot_num + 1;
    }

    # TIME モード(時間帯によって割り振るサーバを変える機能)
    elsif ( $conf->{TYPE} eq 'TIME' ) {
        my $hour = [ localtime(time) ]->[2];
        my @allot_array;
        while ( my ( $key, $value ) = each( %{ $conf->{TIME} } ) ) {
            if ( $key =~ /(\d+)-(\d+)/ ) {
                my $from = $1;
                my $to   = $2;
                if ( $hour >= $from && $from <= $to ) {
                    @allot_array = @$value;
                }
            }
            elsif ( $key =~ /^(\d+)$/ ) {
                if ( $hour == $1 ) {
                    @allot_array = @$value;
                }
            }
        }
        @allot_array = @{ $conf->{TIME}->{other} } unless @allot_array;
        my $i = int( rand( $#allot_array + 1 ) );
        $allot = $allot_array[$i];
    }
    # URL モード(URLのパスによって割り振るサーバを変える機能)
    elsif ( $conf->{TYPE} eq 'URL' ) {
        my $uri = $r->uri;
        my @allot_array;
        while ( my ( $key, $value ) = each( %{ $conf->{URL} } ) ) {
            if ( $key =~ /\/(.+)\// ) {
                my $regex = $1;
                if ( $uri =~ /$regex/ ) {
                    @allot_array = @$value;
                }
            }
        }
        @allot_array = @{ $conf->{URL}->{other} } unless @allot_array;
        my $i = int( rand( $#allot_array + 1 ) );
        $allot = $allot_array[$i];
    }

    # FORCE オプション(強制的に割り振るサーバを指定する機能)
    my $args = $r->args;
    if ( $args =~ /__force__=(\d+)$/ ) {
        my $force = $1;
        my $ip    = $r->connection->remote_ip;
        my $cidr  = Net::CIDR::Lite->new;
        for ( @{ $conf->{ADMIN_IP} } ) {
            $cidr->add_any($_);
        }
        $allot = $force if $cidr->find($ip);
    }

    # この時点でallotが未定義だったらDECLINEDでreturn
    return Apache2::Const::DECLINED unless $allot;

    # mod_proxy_balancer の stickysession のためにallot情報をCookie値として追加
    $allot = 'x.' . $allot;
    my $allot_cookie = new CGI::Cookie( -name => "allot", -value => $allot );
    $r->headers_out->add( 'Set-Cookie' => $allot_cookie );

    # return OK
    return Apache2::Const::OK;
}

1;

mod_proxy_balanser関連

ProxyPass / balancer://TEST/ stickysession=allot
<Proxy balancer://TEST/>
        BalancerMember http://backend.webserver_01 route=1
        BalancerMember http://backend.webserber_02 route=2
        BalancerMember http://backend.webserber_03 route=3
        BalancerMember http://backend.webserber_04 route=4
        BalancerMember http://backend.webserber_05 route=5
</Proxy>

Apache Handler関連

PerlRequire /var/www/perl/startup.pl
PerlHeaderParserHandler +Apache2::ProxyAllot
perlSetVar config_yaml '/var/www/perl/config.yaml'

YAML

TYPE: TIME
#-------------------
TIME:
  2-5:
    - 1
    - 2
    - 3
  12:
    - 1
    - 2
    - 3
    - 4
    - 5
  other:
    - 1
    - 2
    - 3
    - 4
#-------------------
UNIQUE_CODE:
  allot_num: 10
  cookie_string: Apache
#-------------------
URL:
  /img/:
    - 1
  /search/:
    - 2
  other:
    - 3
    - 4
    - 5
#-------------------
ADMIN_IP:
  - 192.168.1.0/24
#-------------------

解説

だらだらと長くなってしまうのですが、要点だけ簡単に解説します。

振り分けロジックは

  1. UNIQUE_CODE モード(ユーザを必ず同一のサーバに割り振る機能)
  2. TIME モード(時間帯によって割り振るサーバを変える機能)
  3. URL モード(URLのパスによって割り振るサーバを変える機能)

の3通りにしてみました。
上のYAMLがそれらの設定ファイルです。YAMLには全モードのサンプルが乗ってますが、実際にはTYPEというところでどれか1つだけモードを指定して使います。

例えばTYPE=TIMEとするとTIMEモードが有効になります。上記のYAMLの例だと2時から5時がバックエンドのサーバ1,2,3に割り振って、12時には1,2,3,4,5の全部に、その他の時間帯は1,2,3,4に割り振る、ということになります。

UNIQUE_CODEモードは昨日のブログに書いたままのモノですが、割り振る数(allot_num)とユーザコードを格納しているCookieの名称(cooikie_string)をYAMLで設定するようになってます。

URLモードは。。。(説明書くのがめんどくなってきた)見たまんまです。正規表現で評価した結果で割り振り先がかわります。


あとおまけでFORCEオプションというのも準備しました。

よくバランサーの上から「ピンポイントで中の1台だけを外から叩きたい、けど叩けない!」みたいなもどかしい思いをしたことないですか?uriパラメータのお尻に「__force__=1」とか「__force__=3」のように付加してあげることで、1や3のサーバを狙い撃ちできます。なお、ここで言っている1とか3の数値はhttpd_confの中のディレクティブ内のrouteの数値と対応してます。
ちなみにFORCEオプションはどこからでも叩けるようだとちとまずいので、YAMLの一番下にある「ADMIN_IP」で設定したアドレスからのみ有効となります。この部分はCIDRブロック形式でIPを指定できます。うーん芸が細かい!

mod_proxy_balancerのトリック

最後にmod_proxy_balancerについてですが、詳しい方はすぐにピンとくるかと思いますが、要するにstickysession機能を使って動的にroute制御をおこなってます。stickysessionに見てもらうのはもちろんCookie値です。しかしユーザからのリクエスト時にこの制御用のCookieがある訳ではなく、Apache2::ProxyAllot内での振り分けロジックの結果によって「allot」という名前のCookieを内部的に勝手に発行しています!

えげつないことしてるなぁ、と自分でも思いますが、やってみたところこれはこれで上手に動きます。なんかApacheが可哀想ですが、ザマミロって感じです。快感です。


しかし既知の問題点もありました

  1. Header解析フェーズで内部的に発行してしまったCookieの消し方がわからん。。。応答フェ−ズでユーザにデータを返却する前に消しておきたいなぁ。
  2. たぶんチョー基礎的なことなんだと思うんですが、mod_proxyとか使う場合に飛ばし先のサイトが相対パスで画像はってあるとうまく表示されません。どうしたものか。。教えてエラい人!!