GlassFishクラスタ構成を作った時のメモです。

バージョンは、
OS: Scientific Linux 6.3 x86_64
nginx: 1.2.6
GlassFish: 3.1.2.2
Java: Java SE 7u11
いずれも今日時点の最新安定バージョンです。

アプリはJavaEE6で作ってます。
ステートフルなアプリなので、スティッキーセッションとセッションレプリケーションが必要です。

 

構成はこんな感じ。
DAS(ドメイン管理サーバ)がクラスタを管理します。
今回はGlassFishクラスタの話なので、「nginxとDASの冗長化は?」というツッコミは無しでお願いします:)

GlassFishのレプリケーション方式はインメモリレプリケーションです。
昔のGlassFishではHigh-Availability Database (HADB)という方式があったようですが、
もう無くなったみたいですね。

レプリケーションについてはこのページがわかりやすいです。
Clustering in GlassFish Version 3.1

では、クラスタ構成の手順です。
このブログエントリーを参考にさせて頂きました。ありがとうございます!
Glassfish 3.1の自己増殖クラスタを試す

1.ノード準備
※この手順だけはDASではなく、各ノードで行います。

  • glassfishユーザ作成(GlassFishはglassfishユーザで実行する事にします)
  • GlassFishインストールディレクトリ作成
  • GlassFishインストールディレクトリのオーナーをglassfishに変更
  • Javaインストール
groupadd glassfish
useradd -g glassfish glassfish
mkdir /usr/local/glassfish3
chown glassfish:glassfish /usr/local/glassfish3
rpm -ivh /path/to/JDKのrpmファイル

2.クラスタ作成

asadmin create-cluster cluster1

※EC2環境で稼働させる場合、マルチキャストが使えないため以下のように明示的にノードを列挙してください。
詳しくはこのページを参照。
Discovering a Cluster When Multicast Transport Is Unavailable
@yoshioteradaさんに教えて頂きました。ありがとうございます!

※同一ノードにインスタンスを複数立ててクラスタを複数作る場合は、ポートが被らないように注意しましょう。

asadmin create-cluster --properties GMS_DISCOVERY_URI_LIST=tcp'\\:'//glassfish01,tcp'\\:'//glassfish02,tcp'\\:'//glassfish03:GMS_LISTENER_PORT=9090 cluster1

3.DASからノードへのssh接続設定

echo "AS_ADMIN_SSHPASSWORD=ノードのglassfishユーザのパスワード" > /tmp/password
asadmin --passwordfile=/tmp/password --interactive=false setup-ssh --sshuser glassfish --generatekey=true glassfish01
asadmin --passwordfile=/tmp/password --interactive=false setup-ssh --sshuser glassfish --generatekey=true glassfish02
asadmin --passwordfile=/tmp/password --interactive=false setup-ssh --sshuser glassfish --generatekey=true glassfish03
rm /tmp/password

4.DASからsshでノードのjavaコマンドが使えるか確認

ssh glassfish01 java -version
ssh glassfish02 java -version
ssh glassfish03 java -version

5.ノードにGlassFishをインストール

asadmin install-node --installdir /usr/local/glassfish3 --sshuser glassfish --sshkeyfile ~/.ssh/id_rsa glassfish01
asadmin install-node --installdir /usr/local/glassfish3 --sshuser glassfish --sshkeyfile ~/.ssh/id_rsa glassfish02
asadmin install-node --installdir /usr/local/glassfish3 --sshuser glassfish --sshkeyfile ~/.ssh/id_rsa glassfish03

6.DASにノードを追加

asadmin create-node-ssh --nodehost glassfish01 --sshuser glassfish --sshkeyfile ~/.ssh/id_rsa --installdir /usr/local/glassfish3 glassfish01
asadmin create-node-ssh --nodehost glassfish02 --sshuser glassfish --sshkeyfile ~/.ssh/id_rsa --installdir /usr/local/glassfish3 glassfish02
asadmin create-node-ssh --nodehost glassfish03 --sshuser glassfish --sshkeyfile ~/.ssh/id_rsa --installdir /usr/local/glassfish3 glassfish03

7.ノードにインスタンスを追加

asadmin create-instance --node glassfish01 --cluster cluster1 instance1
asadmin create-instance --node glassfish02 --cluster cluster1 instance2
asadmin create-instance --node glassfish03 --cluster cluster1 instance3

8.スティッキーセッションのためのプロパティ設定
この設定を行うと、Cookieの末尾にインスタンス識別子が付加され、
リクエストが振り分けられるインスタンスが固定されます。

asadmin create-system-properties --target instance1 INSTANCE=i1
asadmin create-system-properties --target instance2 INSTANCE=i2
asadmin create-system-properties --target instance3 INSTANCE=i3
asadmin create-jvm-options --target cluster1 -DjvmRoute=\${INSTANCE}

9.クラスタを起動
※OS起動時のクラスタの自動起動がわかりませんでした。ご存知の方教えて下さい><

asadmin start-cluster cluster1

GlassFishの設定はここまで。
次はnginxのインストールと設定です。

nginxは標準でスティッキーセッションの機能がありませんが、
モジュールを作ってくれている方がいらっしゃるので、ありがたく使わせて頂きます。

1.nginxインストール

ソースからインストールするので、ユーザを作ります。

groupadd nginx
useradd -g nginx nginx

依存ライブラリのインストール

yum install pcre-devel

ソースと追加モジュールのダウンロード

cd /usr/local/src
wget http://nginx.org/download/nginx-1.2.6.tar.gz
svn checkout http://nginx-sticky-module.googlecode.com/svn/trunk/ nginx-sticky-module-read-only

インストール ※各種パスとモジュールの指定は適宜読み替えてください。

tar xvf nginx-1.2.6.tar.gz
cd nginx-1.2.6
./configure \
  --prefix=/var/www \
  --user=nginx \
  --group=nginx \
  --sbin-path=/usr/local/sbin/nginx \
  --conf-path=/etc/nginx/nginx.conf \
  --error-log-path=/var/log/nginx/error.log \
  --http-log-path=/var/log/nginx/access.log \
  --pid-path=/var/run/nginx/nginx.pid \
  --lock-path=/var/lock/subsys/nginx.lock \
  --with-http_ssl_module \
  --with-http_realip_module \
  --with-http_gzip_static_module \
  --with-http_stub_status_module \
  --add-module=../nginx-sticky-module-read-only
make
make install

2.設定ファイル修正
nginxのconfigureの時に–conf-pathで指定したパスに設定ファイルが出来ています。
以下、設定例です。
※スティッキーセッションに必要な設定は、upstreamのstickyだけです。

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log debug;

pid        /var/run/nginx/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" '
                      '$upstream_response_time';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;

    tcp_nopush      off;

    keepalive_timeout  65;

    gzip  off;

    server_tokens off;

    upstream cluster1 {
        sticky;
        server glassfish01:28080 max_fails=2 fail_timeout=10m;
        server glassfish02:28080 max_fails=2 fail_timeout=10m;
        server glassfish03:28080 max_fails=2 fail_timeout=10m;
    }

    server {
        listen       80;

        proxy_set_header Host               $host;
        proxy_set_header X-Real-IP          $remote_addr;
        proxy_set_header X-Forwarded-Host   $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;

        error_page  404              /404.html;
        error_page  500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location / {
            root   /var/www/html;
            index  index.html index.htm;
        }

        location /favicon.ico {
            root   /var/www/html;
        }

        location /warファイルコンテキスト名/ {
            proxy_pass    http://cluster1;
        }
    }
}

以上で環境は出来ました。
動作確認してみます。

1.確認用ソース
以下をsession.jspという名前で保存して、warファイルを作成します。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.net.InetAddress" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>
<%@ page import="javax.servlet.http.Cookie" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
<!--
body
{
	font-family: 'MS ゴシック';
	font-size: 9pt;
}

table
{
	border-collapse: collapse;
	border-left: 1px;
	border-right: 1px;
	border-top: 1px;
	border-bottom: 1px;
	border-style: solid;
	border-color: #000000;
}

table.border
{
	border-left: 0px;
	border-right: 0px;
	border-top: medium solid;
	border-bottom: 0px;
	border-color: #000000;
}

td
{
	border-left: 0px;
	border-right: 1px;
	border-top: 0px;
	border-bottom: 1px;
	border-style: solid;
	border-color: #000000;
	margin: 2px;
	padding: 2px;
}

td.title-left
{
	background-color: #a3bacd;
	font-weight: bold;
	text-align: left;
}

td.title-center
{
	background-color: #a3bacd;
	font-weight: bold;
	text-align: center;
}
-->
</style>
</head>
<body>
<h1><font color="red">Session Info</font></h1>
<table align="center" border="0" width="800">
	<tr>
		<td class="title-left" width="120">Server</td>
		<td><%= InetAddress.getLocalHost().getHostName() %></td>
	</tr>
	<tr>
		<td class="title-left" width="120">Session ID</td>
		<td><%= session.getId() %></td>
	</tr>
	<tr>
		<td class="title-left" width="120">Created on</td>
		<td>
<%
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
			out.print(sdf.format(new Date(session.getCreationTime())));
%>
		</td>
	</tr>
	<tr>
		<td class="title-left" width="120">Request Count</td>
		<td>
<%
			if (session.getAttribute("count") == null) {
				session.setAttribute("count", new Integer(0));
			}
			int count = ((Integer)session.getAttribute("count")).intValue() + 1;
			session.setAttribute("count", new Integer(count));
			out.print(count);
%>
		</td>
	</tr>
</table>

<br/>
<table border="0" width="100%" class="border">
	<tr>
		<td width="80%"></td>
	</tr>
</table>

<h1><font color="red">Cookie Info</font></h1>
<table align="center" border="0" width="800">
	<tr>
		<td class="title-center">Name</td>
		<td class="title-center">Value</td>
		<td class="title-center">Domain</td>
		<td class="title-center">Path</td>
		<td class="title-center">MaxAge</td>
		<td class="title-center">Comment</td>
		<td class="title-center">Secure</td>
		<td class="title-center">Version</td>
	</tr>
<%
	Cookie[] cookies=request.getCookies();
	if (cookies != null) {
		for (int i=0; i<cookies.length; i++) {
			Cookie cookie = cookies[i];
			out.print("<tr>"  + "\r\n");
			out.print("<td>"  + cookie.getName()    + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getValue()   + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getDomain()  + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getPath()    + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getMaxAge()  + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getComment() + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getSecure()  + "</td>" + "\r\n");
			out.print("<td>"  + cookie.getVersion() + "</td>" + "\r\n");
			out.print("</tr>" + "\r\n");
		}
	}
%>
</table>

</body>
</html>

2.デプロイ Jenkinsを使ってビルドからデプロイまで自動化するのが良いと思います。

asadmin --host=glassfish00 --port=4848 --passwordfile=/path/to/.glassfishpasswd deploy --target=cluster1 --contextroot=warファイルコンテキスト名 --force=true --property java-web-start-enabled=false:preserveAppScopedResources=true /path/to/warファイル

3.ブラウザで確認
http://nginxホスト名/warファイルコンテキスト名/session.jsp
を表示して、リクエストの度に「Request Count」の値がカウントアップされる事、
「JSESSIONID」の値が変わらない事を確認します。

4.GlassFishのWeb管理コンソールから、テスト中のセッションを処理しているノードを停止します。

5.もう一度ブラウザで確認
http://nginxホスト名/warファイルコンテキスト名/session.jsp
を再度表示して、「Request Count」の値が初期化されずにカウントアップされる事、
「JSESSIONID」の値はインスタンス識別子の部分だけ変わる事を確認します。

以上で完了です。

 

最後に
nginx + GlassFishの組み合わせは情報が少なかったです。
昔からApacheを使うのが当たり前ではありますが、(マニュアルにも載ってるし)
個人的には、nginxの方が設定がわかりやすくパフォーマンスも優れていると思います。