CLOVER🍀

That was when it all began.

Spring BootとHazelcast WMでセッションレプリケーションして遊ぶ

Spring Boot(Springも含めて)にちょっとずつ慣れていこうと思いまして、手近なもので遊んでみようかと。

Spring BootがEmbedded Tomcatをデフォルトで使うのですが、ここにHazelcast Web Manager(WM)を適用してセッションレプリケーションしてみます。

Web Session Replication
http://docs.hazelcast.org/docs/3.3/manual/html-single/hazelcast-documentation.html#web-session-replication

Tomcatを使う場合、Hazelcast EnterpriseならTomcat向けのものもあるらしいですが、とりあえず気にしない。

Tomcat Based Web Session Replication
http://docs.hazelcast.org/docs/3.3/manual/html-single/hazelcast-documentation.html#tomcat-based-web-session-replication

あと、Spring Sessionというもの(裏はRedis?)もあるようですが、とりあえずこちらも気にしない。

Spring Session
https://github.com/spring-projects/spring-session

Hazelcast WMを使用すると、ServletFilterを適用することになるので、その方法を調べることにもなりますしね。

pom.xml

とりあえず、pomを書きます。必要そうなところだけ抜粋。

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.1.9.RELEASE</version>
  </parent>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>        
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>${scala.version}</version>
    </dependency>
    <dependency>
      <groupId>com.hazelcast</groupId>
      <artifactId>hazelcast-wm</artifactId>
      <version>3.3.3</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>

      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <version>3.2.0</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion>${scala.version}</scalaVersion>
          <args>
            <arg>-Xlint</arg>
            <arg>-unchecked</arg>
            <arg>-deprecation</arg>
            <arg>-feature</arg>
          </args>
          <recompileMode>incremental</recompileMode>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <scala.version>2.11.4</scala.version>
  </properties>

相変わらずScala

エントリポイント

続いて、アプリケーションのエントリポイントを作成。
src/main/scala/org/littlewings/springboot/hazelcast/App.scala

package org.littlewings.springboot.hazelcast

import javax.servlet.DispatcherType
import javax.servlet.http.HttpSessionListener

import com.hazelcast.web.{ SessionListener, WebFilter }
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.context.embedded.FilterRegistrationBean
import org.springframework.context.annotation.{ Bean, ComponentScan }
import org.springframework.core.Ordered

object App {
  def main(args: Array[String]): Unit = SpringApplication.run(classOf[App], args: _*)
}

@EnableAutoConfiguration
@ComponentScan
class App {
  @Bean
  def hazelcastWmFilter: FilterRegistrationBean = {
    val registration = new FilterRegistrationBean
    registration.setFilter(new WebFilter)
    registration.addInitParameter("instance-name", "spring-boot-hazelcast-wm")
    registration.addInitParameter("session-ttl-seconds", "3600")
    registration.addInitParameter("sticky-session", "false")
    registration.addInitParameter("deferred-write", "false")
    registration.addUrlPatterns("/*")  // 何も指定しない場合は、これと一緒?
    registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE)
    registration.setOrder(Ordered.HIGHEST_PRECEDENCE)
    registration
  }

  @Bean
  def hazelcastSessionListener: HttpSessionListener = new SessionListener
}

ここでHazelcast WMを使用するためのServletFilterを登録するためのコードを書いているのですが、init-paramとかどうするんだろうと思っていましたがFilterRegistrationBeanのAPI見たら普通にできそうでした。

サンプルの動かし方の都合上、sticky-sessionはオフに。

    registration.addInitParameter("sticky-session", "false")

これでけっこう動きが変わります。パフォーマンスとはトレードオフのパラメータになります。

HttpSessionListenerも付けます。

  @Bean
  def hazelcastSessionListener: HttpSessionListener = new SessionListener

セッションの破棄とHazelcastを連携させるためのものです。

なお、今回は使いませんでしたがHazelcast WMには、Spring Securityと統合するためのSpringAwareWebFilterというWebFilterを拡張したものもあるみたいですよ。

SpringAwareWebFilter
http://docs.hazelcast.org/docs/3.3/javadoc/com/hazelcast/web/spring/SpringAwareWebFilter.html

Spring Security Support
http://docs.hazelcast.org/docs/3.3/manual/html-single/hazelcast-documentation.html#spring-security-support

RestController

あとは、コントローラーを作成。セッションにカウンタを仕込んでいるだけの、簡単なものです。
src/main/scala/org/littlewings/springboot/hazelcast/SimpleController.scala

package org.littlewings.springboot.hazelcast

import javax.servlet.http.HttpSession

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.{ RestController, RequestMapping }

@RestController
class SimpleController {
  @RequestMapping(Array("/hello"))
  def hello(session: HttpSession): String = {
    session.getAttribute("counter") match {
      case null =>
        session.setAttribute("counter", Integer.valueOf(1))
      case n: Integer =>
        session.setAttribute("counter", Integer.valueOf(n + 1))
    }

    session.getAttribute("counter").toString
  }
}

単にセッションが使えればいいだけだったので、AutowiredでHttpSessionを突っ込んでいた…のですが、@makingさんにツッコミをもらってControllerの引数に修正。

実行

spring-boot:runより、JARで実行した方がいい気がしたので、とりあえずパッケージング。

$ mvn package

これで、3つほどNodeを起動してみましょう。
*最初は4つで試そうと思ったのですが、重過ぎてやめました…

# Node 1
$ java -jar target/spring-boot-hazelcast-wm-integration-0.0.1-SNAPSHOT.jar --server.port=8080

# Node 2
$ java -jar target/spring-boot-hazelcast-wm-integration-0.0.1-SNAPSHOT.jar --server.port=8180

# Node 3
$ java -jar target/spring-boot-hazelcast-wm-integration-0.0.1-SNAPSHOT.jar --server.port=8280

起動途中に、WebFilterが入ったっぽいメッセージが。

2014-12-08 00:42:04.003  INFO 35300 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'webFilter' to urls: [/*]

あとはHazelcastがクラスタを構成してくれます。ちなみに、今回はデフォルト設定です。

Members [3] {
	Member [192.168.129.129]:5701
	Member [192.168.129.129]:5702
	Member [192.168.129.129]:5703 this
}

それでは、Cookieを保存しつつアクセスしてみます。

$ curl -o - -c session-id -b session-id http://localhost:8080/hello
1

隣のNodeへ。

$ curl -o - -c sessin-id -b session-id http://localhost:8180/hello
2

さらに隣のNodeへ。

$ curl -o - -c sessin-id -b session-id http://localhost:8280/hello
3

真ん中のNodeへ、もう1度。

$ curl -o - -c sessin-id -b session-id http://localhost:8180/hello
4

と、セッションに持っているカウントアップの結果が共有できていることがわかります。
*今回は試していませんが、sticky-sessionを設定を変えると最後の結果が変わるはず…

ちなみに、Cookieの中身はこんな感じ。

$ cat session-id
# 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	/	FALSE	0	JSESSIONID	F4A89AFC3EBA548D7ECEE81A4626B6FA
localhost	FALSE	/	FALSE	0	hazelcast.sessionId	HZ42E5F7DA03854CBD804BBB160AEA58DD

Hazelcastが別でCookieの管理をしています。このCookieの名前や設定は、WebFilterのinit-paramで制御することができます。

しかし、こういうの触ったり周辺情報を見ていると、Redisとか試してみた方がいいのかなーという気にもちょっとなります。ああ、よそ見が…。

今回作成したソースコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-wm-integration