更新:$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に中継してアプリサーバの結果を返却

Apacheの設定

httpd.confの記述

後ろの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として設置することにする。

ProxyPassとbalancer

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>

ProxyPass

まず2行目、ProxyPassでどのパスにリクエストがあったらアプリサーバに振るかを記述する。ここでは、http://www.example.org/webapp/myapl/hoge などにアクセスがある場合を想定しており、/webapp/ へのアクセスを引っかけている。実際の処理はbalancer://で書くので後述。

また2行目最後のstickysession=JSESSIONIDは、セッションがある場合にstickyを効かせて同一のアプリサーバに振るよう指定している。これにより、同一セションは同一のアプリサーバに振られるようになる。

Proxy balancer

続いて、<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としてスラッシュが連続しても特に問題無く動くアプリが大多数だと思うので、こういう設定になっていて問題無いならばまぁいいかも(私も某所でこの誤った設定のまま放置しているサーバが…… (^^;)。

Location /balancer-manager/

balancerを使うと、現在の振り分け状況を http://servername/balancer-manager で見ることができる。当然、外部に公開するのはよろしくないので内部アクセス以外をForbiddenにしている。

ProxyPassとbalancer その2

先の例では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の設定

Tomcat 7のインストール

これについては、Tomcat 7 インストール の記事を参照。

jvmRouteの設定

さきほどの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とTomcatの連携には、mod_jkっていうのをよく見るけど……

Apache 2.2系ならmod_proxy_ajpは標準ライブラリなので、こっちの方が楽です。

Apacheでstickysessionしているのに、Tomcatでさらにセッション共有する意味がわからない

確かに、論理的にはどちらか片方だけやれば、ログインしてあちこち叩いて……というのは実現できる。しかし信頼性のためには必要。私がこの構成にしているのは、まずstickysessionするのは単にログを見る時あちこち見なくていいから(1台のアプリサーバのログで追えるから楽チン)。で、クラスタしているのはユーザがログイン中にアプリサーバが壊れて切り離されても、そのまま動作継続できるから。

あまり信頼性が求められないサービスで、「いったんやり直してください」出してオシマイ程度でいいなら、クラスタは必要無い。

Tomcatに振るパスで、ApacheのBASIC認証を効かせたい

コネクタの設定で、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が返ってハマる……。

Tomcatってなんか分かりにくい

はい。私もそう思います。


プログラミングメモ

▲HOME

▲ABOUT ME