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