CLOVER🍀

That was when it all began.

HazelcastWMとTomcatで、簡単セッションクラスタリング

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はねぇ…。

通常のTomcatのセッションのクラスタリングと異なるのは

  • セッションのデータは、分散して持つ(レプリケーションではない)
  • 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