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インストール
1
2
3
4
5
groupadd glassfish
useradd -g glassfish glassfish
mkdir /usr/local/glassfish3
chown glassfish:glassfish /usr/local/glassfish3
rpm -ivh /path/to/JDKのrpmファイル

2.クラスタ作成

1
asadmin create-cluster cluster1

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

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

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

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

1
2
3
4
5
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コマンドが使えるか確認

1
2
3
ssh glassfish01 java -version
ssh glassfish02 java -version
ssh glassfish03 java -version

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

1
2
3
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にノードを追加

1
2
3
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.ノードにインスタンスを追加

1
2
3
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の末尾にインスタンス識別子が付加され、
リクエストが振り分けられるインスタンスが固定されます。

1
2
3
4
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起動時のクラスタの自動起動がわかりませんでした。ご存知の方教えて下さい><

1
asadmin start-cluster cluster1

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

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

1.nginxインストール

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

1
2
groupadd nginx
useradd -g nginx nginx

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

1
yum install pcre-devel

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

1
2
3
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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だけです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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ファイルを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<%@ 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を使ってビルドからデプロイまで自動化するのが良いと思います。

1
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の方が設定がわかりやすくパフォーマンスも優れていると思います。