Hazelcastには、HazelcastWMというモジュールがあって、HazelcastのDistributed MapとServlet Filterを使ってセッションクラスタリングを行うことができます。
Http Session Clustering with HazelcastWM
http://www.hazelcast.com/docs/3.1/manual/single_html/#HttpSessionClustering
使用方法はいたって簡単、web.xmlにHazelcastWMが提供するServlet FilterとHttpSessionListenerを適用することです。Distributed Mapの設定を行いたい場合は設定ファイルが必要になりますが、ちょっと試してみる分には不要です。
なお、通常のHttpSessionが使用するCookie以外に、もうひとつHazelcastが使用するCookieが追加されます。
今回は、これを使ってTomcatで簡単なセッションクラスタリングをしてみようと思います。さすがに、このテーマでHazelcast × JBoss ASはねぇ…。
- セッションのデータは、分散して持つ(レプリケーションではない)
- HazelcastのClient APIを使用することができ、その場合はTomcat内からセッション情報そのものを外部に追い出すことができる
といった感じです。少し、面白い組み方ができるかもしれません。
ちょっと気になるのは、Servlet Filterで強引に組み込む形になるのでアプリケーションサーバにがっちりインストールするタイプのものと比べると、やや不安?
では、試してみましょう。
準備と説明
まずは、依存関係の定義を行います。
libraryDependencies ++= Seq( "org.eclipse.jetty" % "jetty-webapp" % "9.1.0.v20131115" % "container", "org.eclipse.jetty" % "jetty-plus" % "9.1.0.v20131115" % "container", "javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided", "com.hazelcast" % "hazelcast-wm" % "3.1.3" )
Jettyが入っているのは、WARファイルを作るためにxsbt-web-pluginを使用しているためです。また、HazelcastのClient APIを使用する場合は、hazelcast-clientモジュールを追加してください。
Client APIについては、後述します。
では、セッションが共有されていることを確認するための、簡単なServletを作ってみます。
src/main/scala/org/littlewings/hazelcast/sessionclustering/SharedSessionServlet.scala
package org.littlewings.hazelcast.sessionclustering import java.io.IOException import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse, HttpSession} @WebServlet(Array("/*")) class SharedSessionServlet extends HttpServlet { @throws(classOf[IOException]) @throws(classOf[ServletException]) override protected def doGet(req: HttpServletRequest, res: HttpServletResponse): Unit = { val session = req.getSession val counter: Integer = session.getAttribute("counter").asInstanceOf[Integer] val nextVal: Integer = if (counter == null) 1 else counter + 1 session.setAttribute("counter", nextVal) res.getWriter.println(s"Counter = $nextVal") } }
そして、web.xmlにHazelcastWMが提供するクラスを書いていきます。
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="hazelcast-session-clustering" version="3.0"> <filter> <filter-name>hazelcast-filter</filter-name> <filter-class>com.hazelcast.web.WebFilter</filter-class> <!-- init-paramで設定する --> </filter> <filter-mapping> <filter-name>hazelcast-filter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>REQUEST</dispatcher> </filter-mapping> <listener> <listener-class>com.hazelcast.web.SessionListener</listener-class> </listener> </web-app>
まず大枠は、こんな感じでServlet Filter(WebFilter)とHttpSessionListenerの定義を書きます。SessionListenerは、セッションを破棄した際に、Distributed Mapを破棄するために存在します。
そして、filterタグの中にはinit-paramで、WebFilterの設定をしていきます。順次、書いていきましょう。
map-name。セッション管理で使用する、Distributed Mapの名前を指定します。
<!-- Name of the distributed map storing your web session objects --> <init-param> <param-name>map-name</param-name> <param-value>my-sessions</param-value> </init-param>
HazelcastInstance#getMapで指定する名前ですね。
sticky-session。動きに大きく影響するので、あとでまた触れます。前段にロードバランサなどがいる場合に、セッションスティッキー前提にするかどうかです。
<!-- How is your load-balancer configured? stick-session means all requests of a session is routed to the node where the session is first created. This is excellent for performance. If sticky-session is set to false, when a session is updated on a node, entry for this session on all other nodes is invalidated. You have to know how your load-balancer is configured before setting this parameter. Default is true. --> <init-param> <param-name>sticky-session</param-name> <param-value>true</param-value> </init-param>
デフォルトはtrueで、セッションスティッキー前提です。
cookie-name。HazelcastWMが使用する、Cookieの名前です。
<!-- Name of session id cookie --> <init-param> <param-name>cookie-name</param-name> <param-value>hazelcast.sessionId</param-value> </init-param>
cookie-domain。HazelcastWMで使うCookieのドメインを指定します。
<!-- Domain of session id cookie. Default is based on incoming request. --> <!-- <init-param> <param-name>cookie-domain</param-name> <param-value>.mywebsite.com</param-value> </init-param> -->
今回は、コメントアウトしてデフォルトのリクエストを受けたホスト名(普通のCookieのデフォルトと同じ)としました。
cookie-secure。Secure Cookieにするかどうかで、デフォルトはfalse。Non Secureです。
<!-- Should cookie only be sent using a secure protocol? Default is false. --> <init-param> <param-name>cookie-secure</param-name> <param-value>false</param-value> </init-param>
cookie-http-only。Cookieに、HttpOnlyを付与するかどうかで、デフォルトはfalse。付与しません。
<!-- Should HttpOnly attribute be set on cookie ? Default is false. --> <init-param> <param-name>cookie-http-only</param-name> <param-value>true</param-value> </init-param>
debug。trueにすると、デバッグログが出力されます。デフォルトはfalseです。
<!-- Are you debugging? Default is false. --> <init-param> <param-name>debug</param-name> <param-value>true</param-value> </init-param>
config-location。Hazelcastの設定ファイルを指定します。省略すると、デフォルトのhazelcast-default.xmlが使用されます。
<!-- Configuration xml location; * as servlet resource OR * as classpath resource OR * as URL Default is one of hazelcast-default.xml or hazelcast.xml in classpath. --> <!-- <init-param> <param-name>config-location</param-name> <param-value>/WEB-INF/hazelcast.xml</param-value> </init-param> -->
今回は、デフォルトを使用します。
instance-name。指定すると、すでにHazelcastInstanceが存在する場合は、そちらを使用します。
<!-- Do you want to use an existing HazelcastInstance? Default is null. --> <init-param> <param-name>instance-name</param-name> <param-value>default</param-value> </init-param>
use-client。trueにすると、すでに存在するHazelcastクラスタに、Client APIで接続します。hazelcast-clientモジュールが必要です。
<!-- Do you want to connect as a client to an existing cluster? Default is false. --> <init-param> <param-name>use-client</param-name> <param-value>false</param-value> </init-param>
今回は、サーバ自身がクラスタに参加します。
client-config-location。HazelcastClientの設定ファイルを指定します。
<!-- Client configuration location; * as servlet resource OR * as classpath resource OR * as URL Default is null. --> <!-- <init-param> <param-name>client-config-location</param-name> <param-value>/WEB-INF/hazelcast-client.properties</param-value> </init-param> -->
shutdown-on-destroy。アンデプロイ時に、HazelcastInstanceをシャットダウンするかどうかで、デフォルトはtrueでシャットダウンします。
<!-- Do you want to shutdown HazelcastInstance during web application undeploy process? Default is true. --> <init-param> <param-name>shutdown-on-destroy</param-name> <param-value>true</param-value> </init-param>
設定項目しては、こんな感じです。あとは、こんなfilter-mappingにしておいてください。
<filter-mapping> <filter-name>hazelcast-filter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>REQUEST</dispatcher> </filter-mapping>
で、とりあえずこの設定でWARファイルを作って
> package
Tomcatにデプロイします。WARファイルの名前は、
hazelcast-session-clustering.war
とします。
なお、Tomcatは2つ用意します。それぞれ、
tomcat1 tomcat2
としましょう。Tomcat2は、ポートを1,000ずらしています。
では、2つとも起動します。
$ tomcat1/bin/startup.sh $ tomcat2/bin/startup.sh
それでは、動作確認してみましょう。
なお、今はsticky-session=trueです。
sticky-session=true
init-paramでsticky-sessionをtrueにした場合、前述の通りセッションスティッキーが前提になります。その挙動を確認してみましょう。
まずは、Tomcat1にアクセスしてみます。Cookieを保存するように、オプションを指定して。
$ curl -o - -c sid -b sid http://localhost:8080/hazelcast-session-clustering/ Counter = 1
1って返りました。
この時のCookieは、こんな感じです。
$ cat sid # Netscape HTTP Cookie File # http://curl.haxx.se/rfc/cookie_spec.html # This file was generated by libcurl! Edit at your own risk. #HttpOnly_localhost FALSE /hazelcast-session-clustering/ FALSE 0 JSESSIONID 34B2EA39818F75F6676894988E5108F9 #HttpOnly_localhost FALSE /hazelcast-session-clustering FALSE 0 hazelcast.sessionId HZB521CF3EB6FA450DA7FD347F9319D1CB
とりあえず、そのままアクセスを繰り替えして、5くらいまで進めてみます。
$ curl -o - -c sid -b sid http://localhost:8080/hazelcast-session-clustering/ Counter = 5
ここで、Tomcat1をシャットダウンします。
$ tomcat1/bin/shutdown.sh
Tomcat2にアクセス。
$ curl -o - -c sid -b sid http://localhost:9080/hazelcast-session-clustering/ Counter = 6
ちゃんと値が引き継がれた上で、インクリメントされています。セッションの情報が共有できていますね!
なんですけど、sticky-session=trueの場合は本当にセッションスティッキーにしてあげないと、簡単に不整合になります。
とりあえず、確認のためにTomcatを再起動します。
$ tomcat2/bin/shutdown.sh $ tomcat1/bin/startup.sh $ tomcat2/bin/startup.sh
Tomcat1で、5まで進めます。
$ curl -o - -c sid -b sid http://localhost:8080/hazelcast-session-clustering/ Counter = 5
Tomcat2にアクセスします。
$ curl -o - -c sid -b sid http://localhost:9080/hazelcast-session-clustering/ Counter = 6
値が引き継がれていますね。
このままTomcat2で10まで進めます。
$ curl -o - -c sid -b sid http://localhost:9080/hazelcast-session-clustering/ Counter = 10
Tomcat1に戻ります。
$ curl -o - -c sid -b sid http://localhost:8080/hazelcast-session-clustering/ Counter = 6
なんと、Tomcat2の結果がまったく共有されていません。
これは、sticky-session=trueの場合は、初回アクセスはフェッチ時にDistributed Mapからロードし、以後は1リクエストの処理中はローカル(アプリケーションサーバのHttpSession)のセッションでデータを管理して、最後にDistributed Mapに反映するという動きになっているからです。
なので、最後にアクセスしてsetAttributeしたセッションが、Distributed Mapに反映されます。setAttributeやremoveAttributeしなかった場合は、何も起こりませんが。また、更新時にはDistributed Mapへの反映のため、他のNodeとの通信も発生します。
もちろんパフォーマンス上はこちらの方が有利なのですが、この挙動が嫌な場合はsticky-sessionをfalseにするとどのNodeに振り分けても同じデータが見えることになります。
sticky-session=false
まあ、せっかくなので確認してみましょう。Tomcatを2つともシャットダウンして、アンデプロイしておきます。
そして、sticy-sessionをfalseにして
<init-param> <param-name>sticky-session</param-name> <param-value>false</param-value> </init-param>
パッケージングしてデプロイ。
今度は、交互にアクセスしても、値が引き継がれるようになります。
# Tomcat1 $ curl -o - -c sid -b sid http://localhost:8080/hazelcast-session-clustering/ Counter = 3 # Tomcat2 $ curl -o - -c sid -b sid http://localhost:9080/hazelcast-session-clustering/ Counter = 4 $ curl -o - -c sid -b sid http://localhost:9080/hazelcast-session-clustering/ Counter = 5 $ curl -o - -c sid -b sid http://localhost:8080/hazelcast-session-clustering/ Counter = 6
と、ここまで書いておいてなんですが、開発中のHazelcast 3.2ではまたパラメータが増えて内部の挙動が変わるみたいです。
このsticky-sessionの説明とソースを見比べて、「やってること、全然違うのでは?」と思っていたら、3.2でだいぶ変わっていたのでした…。まあ、考え方自体はそんなに変わらないので、「こういうのもあるんだ〜」と思っていただければ。
今回のサンプルは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-session-clustering