minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

Tomcat5には、標準でクラスタリング機能が付いています。
クラスタリングとは、簡単に言えば複数のサーバを同時に動かす技術のことです。
ただし、外部からはあたかも単一のサーバにアクセスしているような動作をします。
これによって、一つのサーバがダウンしてもシステムは正常に動作することが保証されます。
さらに、サーバを増やすことによって簡単にシステムパフォーマンスを上げることが出来ます。

今回はこの機能について調べてみます。
ちなみに、クラスタリング機能を使うにはJDK1.4以上が必要になります。

複数のTomcatを同一マシン上で起動する

本来、クラスタリング機能の各サーバ(ノードと呼ばれる)は
別々のマシン上で動作するものですが、ここでは説明を簡単にするために
同一マシン(さらに同一ディレクトリ)から2つのTomcatを起動することにします。
今回使用したのはTomcat5.0.27(Windows版)です。

まずは、それぞれのserver.xmlを生成します。
デフォルトの conf/server.xml ファイルの複製を、同一ディレクトリ内に
server1.xml, server2.xml という名前で作成します。

まずは、両ファイルから Cluster 要素をコメントインして下さい。
これによりクラスタリング機能が有効になります。
そして、今回は同一マシンから2つのTomcatを起動するので
使用するポート番号を変える必要があります。

/Server#port
/Server/Service/Connector#port (2箇所)
/Server/Service/Engine/Host/Cluster/Reciever#tcpListenPort

以上、4箇所のポート番号を変更します(ただしserver2.xmlのみ)。
一律に10000足せばいいでしょう(8080→18080のように)。

これで準備は完了です。
$TOMCAT_HOME/bin ディレクトリから以下を入力します。

catalina start -config conf/server1.xml
catalina start -config conf/server2.xml

これで同一マシン上から2つのTomcatが実行されます。
以下、server1.xml で起動した方をTomcatAとします。
server2.xml で起動した方はTomcatBとします。

TomcatAへアクセスするためのURLは http://localhost:8080/
TomcatBへアクセスするためのURLは http://localhost:18080/ となります。

サンプルアプリケーションを展開する

クラスタリング機能を試す簡単なサンプルアプリケーションを用意しました。
counter-test.zip

これを $TOMCAT_HOME/webapps 上で展開します。
ファイル構成は以下のようになります。

counter-test/index.jsp
counter-test/WEB-INF/web.xml

index.jsp の内容は以下の通りです。

<%@ page contentType="text/html;charset=EUC-JP" %>
<%
    Integer count = (Integer)session.getAttribute("count");
    if (count != null) {
        count = new Integer(count.intValue() + 1);
    } else {
        count = new Integer(1);
    }
    session.setAttribute("count", count);
    System.out.println(session.getId() + " : " + count);
%>
<html>
<head>
<title>カウンター</title>
</head>
<body>
セッションID : <%= session.getId() %><br>
現在のカウンタ数 : <%= count %><br>
</body>
</html>

内容はいたって簡単です。
セッションからカウンタ値を取得して、1を足して画面に表示します。
そして1増やしたカウンタ値をセッションに格納します。
同時に、標準出力(Tomcatコンソール)にセッションIDとカウンタ値を出力します。
セッションが存在しない場合(初回アクセス)はカウンタ(値は1)を生成します。

web.xml の内容は以下の通りです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">
<web-app>
  <distributable />
</web-app>

distributable 要素がポイントです。

今回は同一マシン(かつ同一ディレクトリ)にあるTomcatを使いますが、
別マシン(または別ディレクトリ)にあるTomcatを実行するときには
このアプリケーションをそれぞれの TOMCAT_HOME ディレクトリ以下に格納する必要があります。

サンプルアプリケーションを実行する

ではまず、TomcatAからサンプルアプリケーションにアクセスします。

http://localhost:8080/counter-test/

セッションIDと現在のカウンタ数が表示されるはずです。
この状態からブラウザのリロードを行うと、カウンタ数が増えていきます。
これは、セッションに格納したカウンタ数を毎回1ずつ増やしているからです。

ここで、一旦ブラウザを閉じてみます。
タブブラウザを使用している人は、タブを閉じるだけでなくブラウザごと閉じて下さい。
その後再び同じURLにアクセスすると、カウンタ数はまた1に戻ってしまいます。
そして、セッションIDは先程表示されていたものと異なる値になっているはずです。

セッションとは?

セッションについて詳しく延べるのは避けますが、
とりあえずブラウザを閉じない間はセッション(セッションID)が保持されるので
カウンタ数は次々と増えていきます。
しかし、一旦ブラウザを閉じるとセッションIDが保持されなくなるので
再び画面にアクセスした際には新しいセッション(およびセッションID)が
割り振られるという事になるのです。

セッションIDを明示的に指定する

通常セッションIDはブラウザによって自動的に管理されますが、
手動で指定してやることも可能です。
まず、先程と同じように画面にアクセスして数回リロードを行って下さい。

この段階で、画面に表示されているセッションIDをメモ(コピペでOK)しておきます。
そしてブラウザを閉じ、またブラウザを開きます。
次に、URL欄に以下のように打ち込みます。

http://localhost:8080/counter-test/;jsessionid=XXX

XXX の部分は、先程メモしておいたセッションIDで置き換えます。

するとどうでしょう。
カウンタ数は先程から引き継がれているはずです。
これは、先程まで使用していたセッションを明示的に使うことを指定したからです。

そのためのURL記述法が ;jsessionid=XXX になります。
これは、通常のURLの後ろに付ける形になります。
ただしクエリー文字列を使用する場合には、URLとクエリー文字列の間に付ける必要があります。
例えば、

test.jsp;jsessionid=XXX?key=value

のようにします。

なお、間違ったセッションIDを記述するとそれは無視され
通常通りにセッションが新しく生成されます。

ようやく本題

さて、いよいよ本題のクラスタリング処理を試してみます。
Tomcatを2つ起動して、TomcatAから画面にアクセスします。

http://localhost:8080/counter-test/

ここでリロードを数回して、セッションIDをメモします。

ブラウザを閉じたら、以下のURLにアクセスします。

http://localhost:18080/counter-test/;jsessionid=XXX

これはTomcatBのアプリケーションにアクセスすることを意味します。
この場合でも、カウンタの値が保持されることを確認して下さい。

通常、異なるTomcatではセッションを共有しないので
TomcatAで使用していたセッション(セッションID)をTomcatBで使うことは出来ません。
しかし、クラスタリング機能を使うと
セッションは各Tomcat上で共有されます。
従って、TomcatBからTomcatAのセッションを使えるようになります。

クラスタリングの仕組み

では、どのようにこの機能が実装されているのでしょうか。
深く追っていくと大変なので(笑)、簡単に調べてみました。

まずTomcatAを起動すると、このサーバ上にクラスタが生成されます。
さらに「メンバーシップ」というサービスも起動されます。
このサービスはクラスタ内の各ノード(ここではTomcatプロセス)を管理します。

次にTomcatBを起動すると、先程と同じようにクラスタやメンバーシップが起動されます。
ここで若干の変化があります。
先程起動したメンバーシップを検知して、それぞれのメンバーシップは互いに
相手のメンバーシップを登録します。

これにはマルチキャスト通信の技術が使われています。
そのため、2つの設定ファイル(serverX.xml)で
Membershipのアドレス(mcastAddrとmcastPort)は統一しなければなりません。

セッションの共有

いずれかのTomcat上でセッションが生成されると、その情報は
メンバーシップを通じて各Tomcatサーバに通知されます。
これによって、全てのTomcat上では常に同一のセッション情報が共有されます。

クラスタリングを使うアプリケーションの作成方法

server.xmlにクラスタリング情報を記述しただけでは、
通常のアプリケーションはクラスタリングを使用しません。

クラスタリング処理は毎回HTTPリクエスト/レスポンスを加工します。
そしてセッションが生成/変更されるとその情報を各サーバに送信します。
つまり、通常よりも多くのオーバーヘッドが掛かるのです。
そのため、クラスタリング処理はデフォルトでは有効になっていません。
アプリケーション毎に使用を許可する必要があります。

アプリケーション毎の指定

方法は簡単です。
アプリケーションの web.xml に以下の一文を追加します。

<distributable />

これによって、このアプリケーションでクラスタリング処理が有効になります。

全アプリケーション共通の設定

全てのアプリケーションでクラスタリング処理を有効にすることも可能です。
その場合、conf/server.xml 内の Manager 要素内に同様の1文を追加します。

セッションオブジェクト

そして、セッションに格納するオブジェクトも一定のルールに従う必要があります。
それは、java.io.Serializable を実装することです。
クラスタリング処理は、セッションオブジェクトを一旦byte配列に変えて
各サーバに送信します。そして受信側は、受け取ったbyte配列からJavaオブジェクトを生成します。
これを行うには、各オブジェクトがシリアライズ(直列化)可能になっている必要があるのです。

String や Integer など、Javaの基本クラスは大体シリアライズ可能になっているので
問題はありませんが、自分で作成したクラスのインスタンスを
セッションオブジェクトとして使う際にはその宣言文に
implements Serializable を付加するのを忘れないようにしましょう。

ちょっとした注意点

最近(?)のTomcatでは、Tomcatをシャットダウンするときに
全セッション情報がファイルとして保管されます。
そして再びTomcatを起動した際にはこの情報が有効になります。
つまり、ユーザがセッション継続中にサーバを再起動しても
問題なく処理を続けられるという事です。

クラスタリング処理を有効にしたアプリケーションでは、この処理が行われません。
セッションは各サーバで共有されているので、
サーバをシャットダウンしてもセッション情報は保管されません。
その代わりに、再びサーバを起動した際には既に起動しているTomcatからセッション情報を受け取ります。

しかし、全てのTomcatをシャットダウンしてしまうと
セッション情報を格納する場所が無くなります。
このとき、セッション情報は全て消滅しまうので注意しましょう。

クラスタリング処理を行うアプリケーションでは、常に一つ以上のTomcatを
起動しておくことを心掛ければこのような事態に陥らないで済むはずです。

Apache2との連携

さて、今までの例はあくまで調査に過ぎません。
実際にはTomcatAがダウンしたときに
わざわざURLを手動で書き換える事などしたくはないはずです。

通常はApacheなどのHTTPサーバと連動させて、一つのポートアクセス(通常は80番)から
TomcatAとTomcatBに処理を分散させるようにします。
これを実現するには、mod_jk2 というApacheモジュールを使います。

http://jakarta.apache.org/site/binindex.cgi
から「JK 2」のバイナリをダウンロードして下さい。
これを、Apache2ホームディレクトリ上で展開します。

httpd.confの修正

以下の1文を追加(もしくはコメントアウト)します。

LoadModule jk2_module modules/mod_jk2.so

workers2.propertiesの作成

mod_jk2のバイナリを展開すると、conf/workers2.properties.sample
というファイルが作成されているはずです。
このファイルを conf/workers2.properties に複製します。
そして以下の1文を最後に追加します。

[uri:/counter-test/*]

まずはこの段階で、ApacheとTomcatの連携をしてみましょう。
TomcatAを起動させ、その後Apacheを起動します。
以下のURLを入力します。

http://localhost/counter-test/

カウンタ画面が表示されれば成功です。
このとき、80番(デフォルトポート番号)へのリクエストが
mod_jk によって Tomcat に接続されたことになります。
リロード動作によって、カウンタが増加することを確認して下さい。

Tomcat2台に処理を振り分ける

workers2.propertiesに以下の文を追加します。

[channel.socket:localhost:18009]
port=18009
address=127.0.0.1

[ajp13:localhost:18009]
channel=channel.socket:localhost:18009

TomcatAとTomcatBを起動させ、最後にApacheを起動します。
そして先程と同じように、

http://localhost/counter-test/

にアクセスします。
何回かリロード動作を行って、カウンタが増加することを確認して下さい。
一見先程と同じように見えますが、実は2台のTomcatに処理が振り分けられています。

今度はTomcatAとTomcatBのコンソール画面を見ながら
リロード処理をしてみて下さい。
両方のコンソールに交互に出力がされているはずです。
クラスタリング処理が有効になっているので、
セッションは両方のTomcatサーバで同期が取られています。

ここで、TomcatA(TomcatBでも可)をシャットダウンしてみます。
そしてリロードを数回行います。
先ほどとは異なり、毎回TomcatBの方に処理が振られることを確認して下さい。