更新:$Date:: 2013-12-13 00:06:44 +0900#$, $Rev: 156 $
Webサービス提供の際、フロントに複数のApacheサーバを置いて、その後ろにクラスタ構成のTomcatを置いてセッション共有のアプリサーバとする際のやり方。Apacheはリバースプロキシとして動き、Tomcatへの中継はmod_proxy_ajpを用いる(mod_jkではない)。なお、こういうページを作っておいてアレですが、私はTomcat嫌いです。メンドイから。
あんまりデカい例を出しても分かりにくいので、フロントに2台のApache(Webサーバ)、後ろに2台のTomcat(アプリサーバ)という冗長化した構成を考える。
ユーザからのリクエストについて、画像ファイルなど静的ファイルは前段のApacheが返す。そして特定のパスが叩かれた場合には、Apacheはアプリサーバへ中継してその結果をユーザに返す。つまりApacheはリバースプロキシとして動く。
以下の作業は、CentOS 6.4、Apache 2.2.15、Tomcat 7.0.39のセットで試しました。
以下のような要件で作ってみる。
この構成でのアクセス例:
http://my.example.org/contents/menu.html ---> Apacheが静的ファイルを返却 http://my.example.org/webapp/auth/login ---> Tomcatに中継してアプリサーバの結果を返却
後ろのTomcatにリクエストを中継するため、Apacheはリバースプロキシとして動作する必要がある。そのため、Proxyモジュールのmod_proxy, mod_proxy_ajp, mod_proxy_balancerが必要になる。Apacheのhttpd.confにLoadModuleされているか確認。なお、CentOS標準rpmのhttpdならば最初からこう設定されているので、何もしなくて良い。
LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_ajp_module modules/mod_proxy_ajp.so LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
次に、実際にどのパスが叩かれたら後ろのTomcatに振るかを設定する必要がある。これを直接httpd.confに書いてしまうと、ちょっとの変更のたびに全体のコンフィグをいじることになってスジが悪いので、外部ファイルにしよう。
Include conf.d/proxy_ajp.conf
上記の内容をhttpd.confに追記して、実際のファイルはApacheインストールディレクトリのconf.d/proxy_ajp.confとして設置することにする。
conf.d/proxy_ajp.confに書く内容は以下のような感じ。最初に、不用意なProxyとならないためにProxyRequests Offとする。
ProxyRequests Off ProxyPass /webapp/ balancer://webapp/ stickysession=JSESSIONID <Proxy balancer://webapp> BalancerMember ajp://192.168.1.3:8009/webapp route=jvm1 loadfactor=10 BalancerMember ajp://192.168.1.4:8009/webapp route=jvm2 loadfactor=10 </Proxy> <Location /balancer-manager/> SetHandler balancer-manager Order Deny,Allow Deny from all Allow from 192.168.1.0/24 </Location>
まず2行目、ProxyPassでどのパスにリクエストがあったらアプリサーバに振るかを記述する。ここでは、http://www.example.org/webapp/myapl/hoge などにアクセスがある場合を想定しており、/webapp/ へのアクセスを引っかけている。実際の処理はbalancer://で書くので後述。
また2行目最後のstickysession=JSESSIONIDは、セッションがある場合にstickyを効かせて同一のアプリサーバに振るよう指定している。これにより、同一セションは同一のアプリサーバに振られるようになる。
続いて、<Proxy balancer://webapp>の行。ここで後ろのTomcatにどう振るかを具体的に書いている。ApacheとTomcatの連携にはAJPプロトコルを用いているので、ajp://と書いて該当のポート番号8009で振っている。Tomcatのserver.xmlを見ると、以下のような行が見つかるはずですね。
<!-- Define an AJP 1.3 Connector on port 8009 --> <Connector port="8009" enableLookups="false" protocol="AJP/1.3" redirectPort="8443" />
次に書いている"route"というのは、Tomcatのserver.xmlに書く"jvmRouteの値(Tomcatの設定で後述)。これは何かと言うと……実際にブラウザに食わされたCookieを覗いてみると分かるけど、Tomcatで吐かれるクッキーのJSESSIONIDは以下のように、本来のセッションIDにピリオドでjvmRouteを連結したものになっている。
Set-Cookie: JSESSIONID=5F06491A3952130A723D3B8XXXXXXXXX.jvm2; Path=/webapp/myapl
Apacheのmod_proxy_balancerは、このJSESSIONIDのピリオドの後ろの値を見て振り先を決めているわけだ。そのため、このApacheの設定とTomcatの設定を合わせる必要がある。注意。
最後のloadfactorは、振る割合を決める。この例なら同じ値だから1:1で振られるけど、例えば1と9にしておけば片方には10%しか振られない。
これ以外にも色々設定できるので、詳しくはApache - mod_proxyを参照。なお、デフォルトのretry値が60になっているので、これで、バックエンドのTomcatに接続できない時はそのサーバに以後60秒は振らないようになる(60秒後にリトライして、まだダメならまた振らない)。こうして、Tomcatプロセス停止時にいちいち手で切り離す必要がなくなる。
なお、Proxy balancerを書くときは、最後にスラッシュは含めない。つまり以下のように書くのは誤り。
<Proxy balancer://webapp/> BalancerMember ajp://192.168.1.3:8009/webapp/ route=jvm1 loadfactor=10 BalancerMember ajp://192.168.1.4:8009/webapp/ route=jvm2 loadfactor=10 </Proxy>
しかし誤りといいつつ、おそらくこう書いても、ほとんどの場合は正しく動くとは思う……というのも、Tomcatの方のアクセスログ(/usr/local/tomcat/logs/localhost_access_log.YYYY-MM-DD.txt)を見てみると分かるけど、この場合は以下のように出ているはずだ。
xx.xx.xx.xx - - [05/Jan/2013:22:00:24 +0900] "GET /webapp//servlets/hoge HTTP/1.1" 200 347
見ての通り、ApacheからTomcatに振るパス /webapp の後ろのスラッシュが連続してしまっている。だからProxy balancerの最後にスラッシュを付けてはいけない。もっとも、URLとしてスラッシュが連続しても特に問題無く動くアプリが大多数だと思うので、こういう設定になっていて問題無いならばまぁいいかも(私も某所でこの誤った設定のまま放置しているサーバが…… (^^;)。
balancerを使うと、現在の振り分け状況を http://servername/balancer-manager で見ることができる。当然、外部に公開するのはよろしくないので内部アクセス以外をForbiddenにしている。
先の例ではProxyPassで前段のApacheから後ろのTomcatに振るパスを記述したが、これは次のようにLocationを使って書いても良い。……というか、こっちの方が一般的な書き方かも。
ProxyRequests Off <Proxy balancer://webapp> BalancerMember ajp://192.168.1.3:8009/webapp route=jvm1 loadfactor=10 BalancerMember ajp://192.168.1.4:8009/webapp route=jvm2 loadfactor=10 </Proxy> <Location /webapp/> ProxyPass balancer://webapp/ stickysession=JSESSIONID ProxyPassReverse balancer://webapp/ stickysession=JSESSIONID </Location> <Location /balancer-manager/> SetHandler balancer-manager Order Deny,Allow Deny from all Allow from 192.168.1.0/24 </Location>
まぁお好きな方で。ちなみに、オライリーのTomcatハンドブックでは、こちらのLocationを使った書き方になっています。
これについては、Tomcat 7 インストール の記事を参照。
さきほどのApacheの設定ファイル中、BalancerMemberで指定したrouteの値を設定する。具体的には、Tomcatのserver.xmlの以下行を変更すれば良い。
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
ここでは、apl1をjvm1、apl2をjvm2とする。各サーバごとに違う値を指定しないといけないので、server.xmlは一台でマスターを作ってそれをコピーしてばらまく……という運用はしない方が良い。過去、何度かこれでハマったので、4台あったら4台にログインして手でなおすこと、として落ち着いてしまった。
クラスタ構成(セッション情報を共有する)環境を作成する。具体的には、ログイン画面ではapl1に振られて、次のマイページに移動した時はapl2サーバに振られた……という場合でも同じセッションIDで動作できるようにしたい。前段のApacheでstickysessionしているのだが、ログインした後にそのアプリサーバが壊れて切り離された場合とか、その辺を考えるとやはり必要だろう。
クラスタリングを有効にするにはserver.xmlのClusterのコメントアウトを外し、何点か修正する。この際、デフォルトのCluster要素は<Engine>直下にあるのだが、この位置で以下の例をやろうとするとFarmWarDeployerが次のようなエラーを吐いてしまい上手くいかない。
Feb 07, 2013 10:26:51 PM org.apache.catalina.ha.deploy.FarmWarDeployer start SEVERE: FarmWarDeployer can only work as host cluster subelement!
このため、以下の例はserver.xmlのもうちょっと下の方、<Host>要素内に書くようにする。
さてTomcatのクラスタリングは大変ややこしい仕組みで実現されており、マルチキャスト通信により自分で参加クラスタを判断してmember参加するという感じになっている。<Cluster>の中に実際にどう書くかは、公式ページのドキュメントClustering/Session Replication HOW-TOに例が載っている。
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8"> <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/> <Channel className="org.apache.catalina.tribes.group.GroupChannel"> <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.104" port="50001" frequency="500" dropTime="3000"/> <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="192.168.1.3" port="4000" autoBind="100" selectorTimeout="5000" maxThreads="6"/> <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter"> <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/> </Sender> <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/> <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/> </Channel> <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/> <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/> <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer" tempDir="/tmp/war-temp/" deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/" watchEnabled="false"/> <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/> <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/> </Cluster>
修正した方がいいのは、まず<Membership>のaddressとport。マルチキャスト通信でクラスタを組むため、この「addressとport」のセットでクラスタがグルーピングされる。つまり、デフォルトでは "228.0.0.4" と 45564/udp (マルチキャストなのでUDP) が指定されているが、このデフォルト値セットを使っていると後から隣で別のサービスを立ち上げられたときに(それはあなたとは限らない、全然違う部署の人の、全然違う物理サーバかも)、かち合ってしまう可能性が非常に高い。だからこの値はなんでもいいから(もちろんIPアドレスはマルチキャスト通信の範囲で)変えておくことが推奨される。ここではIPアドレスを228.0.0.104、ポートを50001にしてみた。
また、<Receiver>のaddressはデフォルトでは"auto"になっているが、NICを何枚も持っているサーバだとこんな設定じゃはっきり言って不安である。素直に、使おうとしているNICのアドレスを記述しちゃった方がいい。なおこうすることにより、やっぱりserver.xmlのマスターファイルを1つ作ってそれを全台に撒く……というようなことはできなくなるが、私は仕方ないので1台1台手で直す運用にしている。
<Receiver>のportは、実際にセッション共有をおこなうレシーバのポートである。デフォルトでは4000番だが、サーバ内で他のアプリとポート番号がバッティングした場合は、autoBind回数だけ1ずつプラスして試してくれる。つまりこの設定だと、4000-4099までで自動設定してくれるので、ほとんどの場合は気にしなくて良い。
なおセッションの共有を行う際は、クラスタ内のサーバは全てNTPで正確に時間を合わせておかないといけない。時間がズレているとうまくセッション共有ができなくてハマることがある、気をつけよう。
Tomcatのクラスタリングは、マルチキャスト通信でやりとりすると先ほど書いた。が、複数のNICを持つ場合などは、どのインタフェースでマルチキャストを扱うか、明示的にルーティングを指定しないといけない。
マルチキャストをどのNICで投げるかのルーティングを、/etc/sysconfig/network-scripts/route-eth1 などに書いてやる。eth0を使うなら、もちろんroute-eth0に。
224.0.0.0/4 dev eth1
設定を終わったら、network-scriptsを反映するためにnetworkのrestartが必要となる。この際、sshなどでリモート接続している場合は通信が不安定になることがあるためコンソールから作業するのが望ましい(リモートからnetwork restartしても、普通は大丈夫である。が、もし記述に間違いがあった場合はいきなりネットワーク不通となり、ssh接続も切れてにっちもさっちもいかなくなるため、ネットワーク周りの再起動はコンソールから作業するのが基本)。
# cd /etc/init.d # ./network restart
反映後、netstat -nrして、マルチキャストのルーティングが追加されていることを確認する。
$ netstat -nr Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 10.211.55.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1 169.254.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth1 224.0.0.0 0.0.0.0 240.0.0.0 U 0 0 0 eth1 0.0.0.0 10.211.55.1 0.0.0.0 UG 0 0 0 eth1
このように、224.0.0.0/4にはeth1を使うように設定されているか確認。
設定が終わったら、Tomcatを再起動してログを見ながら、隣のTomcatを上げたり落としたりするとこんな感じのログが出てクラスタリングできていることが分かる。
2012/01/26 13:31:11 org.apache.catalina.cluster.tcp.SimpleTcpCluster memberAdded 情報: Replication member added: org.apache.catalina.cluster.mcast.McastMember[tcp://192.168.1.3:4000,catalina,192.168.1.3,4000, alive=3] 2012/01/26 13:32:14 org.apache.catalina.cluster.tcp.SimpleTcpCluster memberDisappeared 情報: Received member disappeared: org.apache.catalina.cluster.mcast.McastMember[tcp://192.168.1.3:4000,catalina,192.168.1.3,4000, alive=497027136]
また、セッション共有のマルチキャスト通信がちゃんと出来ているかは、tcpdumpでmulticastを見てみると良い。デフォルトならば500ミリ秒に1回、すなわち1秒に2回飛んでいるはず。
# /usr/sbin/tcpdump -i eth1 -n ip multicast tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 01:36:00.054028 IP 192.168.1.3.50001 > 228.0.0.104.50001: UDP, length 77 01:36:00.554770 IP 192.168.1.3.50001 > 228.0.0.104.50001: UDP, length 77 01:36:01.055657 IP 192.168.1.3.50001 > 228.0.0.104.50001: UDP, length 77 01:36:01.556332 IP 192.168.1.3.50001 > 228.0.0.104.50001: UDP, length 77
Apache 2.2系ならmod_proxy_ajpは標準ライブラリなので、こっちの方が楽です。
確かに、論理的にはどちらか片方だけやれば、ログインしてあちこち叩いて……というのは実現できる。しかし信頼性のためには必要。私がこの構成にしているのは、まずstickysessionするのは単にログを見る時あちこち見なくていいから(1台のアプリサーバのログで追えるから楽チン)。で、クラスタしているのはユーザがログイン中にアプリサーバが壊れて切り離されても、そのまま動作継続できるから。
あまり信頼性が求められないサービスで、「いったんやり直してください」出してオシマイ程度でいいなら、クラスタは必要無い。
コネクタの設定で、tomcatAuthenticationをfalseに変える。
<!-- Define an AJP 1.3 Connector on port 8009 --> <Connector port="8009" tomcatAuthentication="false" enableLookups="false" protocol="AJP/1.3" redirectPort="8443" />
これで、httpServletRequestクラスのgetRemoteUser()メソッドでログインIDが取れるようになる。この設定を入れないとNULLが返ってハマる……。
はい。私もそう思います。