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'
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 #-------------------
解説
だらだらと長くなってしまうのですが、要点だけ簡単に解説します。
振り分けロジックは
- UNIQUE_CODE モード(ユーザを必ず同一のサーバに割り振る機能)
- TIME モード(時間帯によって割り振るサーバを変える機能)
- 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の中の
ちなみにFORCEオプションはどこからでも叩けるようだとちとまずいので、YAMLの一番下にある「ADMIN_IP」で設定したアドレスからのみ有効となります。この部分はCIDRブロック形式でIPを指定できます。うーん芸が細かい!
mod_proxy_balancerのトリック
最後にmod_proxy_balancerについてですが、詳しい方はすぐにピンとくるかと思いますが、要するにstickysession機能を使って動的にroute制御をおこなってます。stickysessionに見てもらうのはもちろんCookie値です。しかしユーザからのリクエスト時にこの制御用のCookieがある訳ではなく、Apache2::ProxyAllot内での振り分けロジックの結果によって「allot」という名前のCookieを内部的に勝手に発行しています!
えげつないことしてるなぁ、と自分でも思いますが、やってみたところこれはこれで上手に動きます。なんかApacheが可哀想ですが、ザマミロって感じです。快感です。
しかし既知の問題点もありました