これは、なにをしたくて書いたもの?
Infinispanは、WildFlyのセッションレプリケーションの仕組みとしても使われていますが、
実はTomcat用のモジュールが存在していたりします。
Infinispanのドキュメントには載っていないんですけどね。
Red Hat Data Grid版だと、JBoss Web Server向けのものがあると紹介されています。
Externalizing HTTP Sessions from JBoss Web Server (JWS) to Data Grid
こちらのコミュニティ版という感じでしょうね。
存在自体はやや前から知っていたのですが、今回1度試してみようかなということで。
WildFly Clustering Tomcat
正式名称がわかりませんが、リポジトリ名からこう呼ぶことにします。
A distributed session manager for Tomcat based on WildFly's distributed session management.
WildFlyのセッション管理の仕組みをベースに、Tomcatでも使えるようにしたものです、と。
平たく言うと、セッションをInfinispan Server(Hot Rod)にオフロードする仕組みですね。
インストール方法を見るとわかりますが、Tomcatのlib
ディレクトリに入れてManager
の実装を差し替えて使うものに
なります。
完全にTomcat向けです。これが理由からか、Tomcat 8、8.5、9、10向けのモジュールが用意されています。
Maven Centralで探す場合は、こんな感じで。
Maven Search / g:org.wildfly.clustering AND wildfly-clustering-tomcat AND hotrod
今回は、WildFly Clustering Tomcatの11.0.0.Finalで、Tomcat 9向けのものを使用します。
依存するInfinispanは11.0.4.Finalなので、Infinispan Serverも11.0.4.Finalを使用することにします。
では、試していきましょう。
環境
今回の環境は、こちら。
$ java --version openjdk 11.0.9.1 2020-11-04 OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04) OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.9.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-53-generic", arch: "amd64", family: "unix"
Tomcatも必要になりますが、これは9.0.39を使います。ローカルにインストールし、1〜3つまでのインスタンスを使うことに
します。
Infinispan Serverは、IPアドレスが172.17.0.2〜4の範囲で、最大3つまでのインスタンスを使うことにします。
WildFly Clustering Tomcatと関連ライブラリを取得する
まずは、WildFly Clustering Tomcatの関連ライブラリを取得します。
11.0.0.Finalのドキュメントを参考にしていきましょう。
pom.xml
を用意。Tomcat 9向けとするので、wildfly-clustering-tomcat-9.0-hotrod
を依存関係に入れておきます。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>wildfly-clustering-tomcat-example</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.wildfly.clustering</groupId> <artifactId>wildfly-clustering-tomcat-9.0-hotrod</artifactId> <version>11.0.0.Final</version> </dependency> </dependencies> </project>
ここから、lib
ディレクトリに依存ライブラリを出力。
$ mvn dependency:copy-dependencies -DoutputDirectory=lib
158個JARファイルが収集されます。
$ ls -1 lib | wc -l 158
ドキュメントに従うと、runtime
スコープのものに対しても行うそうです。
$ mvn dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=lib
が、数は変わりませんでしたが…。
$ ls -1 lib | wc -l 158
最初からruntime
スコープにしても、同じ結果になりました。が、ここは気にせずいきましょう。
サンプルアプリケーションを作成する
試すにも、セッションを使うアプリケーションが必要です。
というわけで、簡単なものを作成しましょう。カウンターをお題にします。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.infinispan</groupId> <artifactId>simple-web</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> </build> </project>
ROOT.war
を作成するWebアプリケーションです。
セッションに保存するクラス。カウンターにしましょう。
src/main/java/org/littlewings/infinispan/session/Counter.java
package org.littlewings.infinispan.session; import java.io.Serializable; import java.util.concurrent.atomic.AtomicInteger; public class Counter implements Serializable { private static final long serialVersionUID = 1L; AtomicInteger value = new AtomicInteger(); public int increment() { return value.incrementAndGet(); } public int getValue() { return value.get(); } }
カウンターを扱うクラス。
src/main/java/org/littlewings/infinispan/session/CounterServlet.java
package org.littlewings.infinispan.session; import java.io.IOException; import java.io.WriteAbortedException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/counter") public class CounterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); Counter counter = (Counter) session.getAttribute("counter"); if (counter == null) { counter = new Counter(); session.setAttribute("counter", counter); } int current = counter.increment(); resp.getWriter().write(String.format("current value = %d%n", current)); } }
HttpSession#setAttribute
するタイミングは、ちょっと意図的にしてあります(1回だけ)。
これで、ビルド。
$ mvn package
準備完了です。
Tomcat 1インスタンス、Infinispan Server 1インスタンスで試す
まずは、Tomcat 1インスタンス、Infinispan Server 1インスタンスで試してみましょう。
Tomcatはローカルで動かし、Infinispan Serverは172.17.0.2で動かします。
Tomcat 9.0.39をダウンロードして、展開。
$ curl -sL -O https://downloads.apache.org/tomcat/tomcat-9/v9.0.39/bin/apache-tomcat-9.0.39.tar.gz $ tar xf apache-tomcat-9.0.39.tar.gz $ cd apache-tomcat-9.0.39
webapps
配下は削除しておきます。
$ rm -rf webapps/*
WildFly Clustering Tomcatのインストール方法に習って、
Tomcatのlib
ディレクトリ内に、先ほど収集したWildFly Clustering Tomcatとその依存ライブラリをコピーします。
$ cp -i /[Path to WildFly Clustering Tomcat libraries]/lib/* lib
次に、TomcatのManager
の設定を行います。ドキュメントによると、Tomcatにデプロイするアプリケーションすべてで
使用する場合は$CATALINA_HOME/conf/context.xml
ファイルに設定を行い、Webアプリケーション単位で行う場合は
WARファイルに/WEB-INF/context.xml
ファイルを含めて行うようです。
Define the distributed Manager implementation either within the global $CATALINA_HOME/conf/context.xml, or within the /WEB-INF/context.xml of a web application:
今回は、$CATALINA_HOME/conf/context.xml
ファイルに設定しましょう。
デフォルトの状態を確認すると、Manager
の設定はコメントアウトされています。
conf/context.xml
<!-- Uncomment this to disable session persistence across Tomcat restarts --> <!-- <Manager pathname="" /> -->
これを、こんな感じに編集(その他の部分もコメントを削除して載せました)。Manager
には、HotRodManager
を指定します。
<?xml version="1.0" encoding="UTF-8"?> <Context> <WatchedResource>WEB-INF/web.xml</WatchedResource> <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource> <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource> <Manager className="org.wildfly.clustering.tomcat.hotrod.HotRodManager" server_list="172.17.0.2:11222"/> </Context>
HotRodManager
の設定は、WildFly Clustering Tomcat固有の設定と、Hot Rodのプロパティの指定、そしてTomcatのManager
として
共通の設定ができます。
固有の設定は以下の4つです。
template
… Infinispan ServerにCacheを作成する際の設定テンプレート。デフォルトはorg.infinispan.DIST_SYNC
granularity
… セッションをCacheにどのように格納するか。SESSION
は単一のCacheエントリにセッションの全属性を格納し、ATTRIBUTE
はセッションの各属性を個別のCacheエントリに格納する。デフォルトはSESSION
maxActiveSessions
… Near Cacheに保持するセッションの最大数で、デフォルトは無制限。0
を指定するとNear Cacheが無効になるmarshaller
… セッションの属性をシリアライズする際に使うMarshaller
の指定。JAVA
、JBOSS
、PROTOSTREAM
の3つがサポートされていて、デフォルトはJBOSS
どうやら、実行時にCacheを動的に作成するようです。Cacheの設定をtemplate
で指定するのですが、元ネタはこれですね。
Infinispan Server側に、自分でテンプレートを作成してもよいでしょう。
marshaller
は、セッションの情報をProtoStreamとするのは難しそうなので、JAVA
かJBOSS
を選ぶのでしょうけど、
いずれJBOSS
(JBossMarshaller)は削除されそう…。
セッションに格納されるオブジェクトはシリアライズされるので、JBossMarshallerやJava標準のシリアライザーを使う場合に
Serializable
でない場合は例外がスローされます。想像に難くはありません。
java.io.NotSerializableException: org.littlewings.infinispan.session.Counter
Hot Rodの設定は、こちらを見ます。
org.infinispan.client.hotrod.configuration (Infinispan JavaDoc 11.0.5.Final API)
プロパティ名は、以下のようにinfinispan.client.hotrod
の部分を削除します。
たとえば、infinispan.client.hotrod.server_list
ならserver_list
となります。
今のmaster
ブランチを見ると、そのうち接続先とプロパティをURIで指定できるようになりそうです。
Blog: Hot Rod URI - Infinispan
あとは、TomcatのManager
の共通的なプロパティが設定できます、と。
The ClusterManager object / Common Attributes
では、サンプルアプリケーションを配置して
$ cp /path/to/target/ROOT.war webapps/
Tomcatを起動してみます。
$ bin/startup.sh
すると、こんなログが…どうやら権限が足りないようです。
logs/catalina.out
Caused by: org.infinispan.client.hotrod.exceptions.HotRodClientException:Request for messageId=6 returned server error (status=0x85): java.lang.SecurityException: ISPN006017: Unauthorized 'EXEC' operation at org.infinispan.client.hotrod.impl.protocol.Codec20.checkForErrorsInResponseStatus(Codec20.java:329) at org.infinispan.client.hotrod.impl.protocol.Codec20.readHeader(Codec20.java:168) at org.infinispan.client.hotrod.impl.transport.netty.HeaderDecoder.decode(HeaderDecoder.java:140) at org.infinispan.client.hotrod.impl.transport.netty.HintedReplayingDecoder.callDecode(HintedReplayingDecoder.java:94)
というわけで、Infinispan Server側を設定します。
ユーザーを作成して
$ bin/cli.sh user create ispn-user -p ispn-password
Hot Rod Connectorに認証に関する設定を入れます。
Manually Configuring Hot Rod Authentication
<endpoints socket-binding="default" security-realm="default"> <hotrod-connector name="hotrod"> <authentication> <sasl mechanisms="SCRAM-SHA-512 SCRAM-SHA-384 SCRAM-SHA-256 SCRAM-SHA-1 DIGEST-SHA-512 DIGEST-SHA-384 DIGEST-SHA-256 DIGEST-SHA DIGEST-MD5 PLAIN" server-name="infinispan" qop="auth"/> </authentication> </hotrod-connector> <rest-connector name="rest"/> </endpoints>
cache-container
までは、認証設定を入れなくても良さそうです。
設定したら、Infinispan Serverを再起動。
Tomcat側のcontext.xml
には、作成したユーザーを使うように認証関連のプロパティを設定します。
<Manager className="org.wildfly.clustering.tomcat.hotrod.HotRodManager" server_list="172.17.0.2:11222" sasl_mechanism="PLAIN" auth_username="ispn-user" auth_password="ispn-password"/>
この状態でTomcatを再起動すると、今度は動作するようになります。
$ curl -c cookie -b cookie localhost:8080/counter current value = 1 $ curl -c cookie -b cookie localhost:8080/counter current value = 2 $ curl -c cookie -b cookie localhost:8080/counter current value = 3
値の更新後に、再度HttpSession#setAttribute
しなくても反映されていますね。
Tomcatを再起動しても、Infinispan Server側のCacheにデータが残っていればセッションを継続利用できます。
ところで、どんなCacheができているか確認してみましょう。Infinispan Serverにログインします。
$ bin/cli.sh [disconnected]> connect Username: ispn-user Password: *************
Cacheの一覧を見ると、localhost
という名前のCacheがあります。
[infinispan1-36096@cluster//containers/default]> ls caches/ localhost ___script_cache ___protobuf_metadata
中身は…まあわかありませんね。
[infinispan1-36096@cluster//containers/default]> ls caches/localhost ��Jftvn������� ��Jftvn������� ��Jftvn�������
describe
すると、Distributed Cacheであることがわかります。
[infinispan1-36096@cluster//containers/default]> describe caches/localhost { "distributed-cache" : { "mode" : "SYNC", "remote-timeout" : 17500, "state-transfer" : { "timeout" : 60000 }, "transaction" : { "mode" : "NONE" }, "locking" : { "concurrency-level" : 1000, "acquire-timeout" : 15000, "striping" : false }, "statistics" : true } }
ここで、同じWARファイルをファイル名を変えて追加してみます。simple-web.war
としましょう。
$ cp target/ROOT.war /path/to/apache-tomcat-9.0.39/webapps/simple-web.war
動作確認。
$ curl -c simple-web-cookie -b simple-web-cookie localhost:8080/simple-web/counter current value = 1 $ curl -c simple-web-cookie -b simple-web-cookie localhost:8080/simple-web/counter current value = 2 $ curl -c simple-web-cookie -b simple-web-cookie localhost:8080/simple-web/counter current value = 3
ここでCacheを見ると、localhost/simple-web
というCacheが増えています。
[infinispan1-36096@cluster//containers/default]> connect Username: ispn-user Password: ************* [infinispan1-36096@cluster//containers/default]> ls caches ___protobuf_metadata ___script_cache localhost/simple-web localhost
HotRodManager
のソースコードを見ると、Manager
から見たホスト名とコンテキスト名のようです。ホスト名は、server.xml
の
Host
から来てると思ったらいいでしょうか?
String deploymentName = host.getName() + context.getName();
この名前で、Cacheを作成します。
configuration.addRemoteCache(deploymentName, builder -> builder.forceReturnValues(false).nearCacheMode(NearCacheMode.INVALIDATED).transactionMode(TransactionMode.NONE).templateName(this.templateName));
だいたい、動作がわかった感じですね。
Tomcat 3インスタンス、Infinispan Server 3インスタンスで試す
では、最後にTomcatを3インスタンス、Infinispan Serverを3インスタンスにしてみましょう。
先ほどまで使っていたTomcatやInfinispan Serverは1度破棄して、ローカルにTomcatを3つ、Infinispan Serverは
172.17.0.2〜4で用意します。
Infinispan Serverには、3つともユーザーの作成と、Hot Rod Connectorの設定を行います。
$ bin/cli.sh user create ispn-user -p ispn-password
server/conf/infinispan.xml
<endpoints socket-binding="default" security-realm="default"> <hotrod-connector name="hotrod"> <authentication> <sasl mechanisms="SCRAM-SHA-512 SCRAM-SHA-384 SCRAM-SHA-256 SCRAM-SHA-1 DIGEST-SHA-512 DIGEST-SHA-384 DIGEST-SHA-256 DIGEST-SHA DIGEST-MD5 PLAIN" server-name="infinispan" qop="auth"/> </authentication> </hotrod-connector> <rest-connector name="rest"/> </endpoints>
Infinispan Serverがクラスタを構成したことを確認します。
13:47:35,681 INFO (main) [org.infinispan.CLUSTER] ISPN000094: Received new cluster view for channel cluster: [infinispan1-42931|6] (3) [infinispan1-42931, infinispan2-33585, infinispan3-6323]
次にTomcatを3つ用意して
$ tar xf apache-tomcat-9.0.39.tar.gz $ mv apache-tomcat-9.0.39 tomcat-1 $ tar xf apache-tomcat-9.0.39.tar.gz $ mv apache-tomcat-9.0.39 tomcat-2 $ tar xf apache-tomcat-9.0.39.tar.gz $ mv apache-tomcat-9.0.39 tomcat-3
WildFly Clustering Tomcatと依存ライブラリをコピー。
$ cp -i /[Path to WildFly Clustering Tomcat libraries]/lib/* tomcat-1/lib $ cp -i /[Path to WildFly Clustering Tomcat libraries]/lib/* tomcat-2/lib $ cp -i /[Path to WildFly Clustering Tomcat libraries]/lib/* tomcat-3/lib
それぞれのtomcat-*/conf/context.xml
に、以下の設定を追加します。
<Manager className="org.wildfly.clustering.tomcat.hotrod.HotRodManager" server_list="172.17.0.2:11222;172.17.0.3:11222" sasl_mechanism="PLAIN" auth_username="ispn-user" auth_password="ispn-password"/>
server_list
は2つのインスタンス分しか設定していませんが、これはInfinispanのHot RodクライアントがInfinispan Serverへの
接続時に追加取得してくれることに期待します。
リッスンポートとシャットダウンポートもずらしましょう。
$ grep port tomcat-*/conf/server.xml tomcat-1/conf/server.xml:<Server port="8005" shutdown="SHUTDOWN"> tomcat-1/conf/server.xml: Define a non-SSL/TLS HTTP/1.1 Connector on port 8080 tomcat-1/conf/server.xml: <Connector port="8080" protocol="HTTP/1.1" tomcat-1/conf/server.xml: port="8080" protocol="HTTP/1.1" tomcat-1/conf/server.xml: <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 tomcat-1/conf/server.xml: <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" tomcat-1/conf/server.xml: <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2 tomcat-1/conf/server.xml: <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol" tomcat-1/conf/server.xml: <!-- Define an AJP 1.3 Connector on port 8009 --> tomcat-1/conf/server.xml: port="8009" tomcat-1/conf/server.xml: <!-- You should set jvmRoute to support load-balancing via AJP ie : tomcat-2/conf/server.xml:<Server port="18005" shutdown="SHUTDOWN"> tomcat-2/conf/server.xml: Define a non-SSL/TLS HTTP/1.1 Connector on port 8080 tomcat-2/conf/server.xml: <Connector port="18080" protocol="HTTP/1.1" tomcat-2/conf/server.xml: port="8080" protocol="HTTP/1.1" tomcat-2/conf/server.xml: <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 tomcat-2/conf/server.xml: <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" tomcat-2/conf/server.xml: <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2 tomcat-2/conf/server.xml: <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol" tomcat-2/conf/server.xml: <!-- Define an AJP 1.3 Connector on port 8009 --> tomcat-2/conf/server.xml: port="8009" tomcat-2/conf/server.xml: <!-- You should set jvmRoute to support load-balancing via AJP ie : tomcat-3/conf/server.xml:<Server port="28005" shutdown="SHUTDOWN"> tomcat-3/conf/server.xml: Define a non-SSL/TLS HTTP/1.1 Connector on port 8080 tomcat-3/conf/server.xml: <Connector port="28080" protocol="HTTP/1.1" tomcat-3/conf/server.xml: port="8080" protocol="HTTP/1.1" tomcat-3/conf/server.xml: <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 tomcat-3/conf/server.xml: <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" tomcat-3/conf/server.xml: <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2 tomcat-3/conf/server.xml: <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol" tomcat-3/conf/server.xml: <!-- Define an AJP 1.3 Connector on port 8009 --> tomcat-3/conf/server.xml: port="8009" tomcat-3/conf/server.xml: <!-- You should set jvmRoute to support load-balancing via AJP ie :
webapps
の配下を削除して
$ rm -rf tomcat-[123]/webapps/*
WARファイルをコピー。
$ cp /path/to/target/ROOT.war ../tomcat-1/webapps $ cp /path/to/target/ROOT.war ../tomcat-2/webapps $ cp /path/to/target/ROOT.war ../tomcat-3/webapps
で、起動。
$ tomcat-1/bin/startup.sh $ tomcat-2/bin/startup.sh $ tomcat-3/bin/startup.sh
tomcat-*/logs/catalina.out
を見ると、server_list
には2つしかInfinispan Serverを登録していなかったのに、3つ分の
インスタンスを見つけています。
14-Nov-2020 23:04:11.885 INFO [HotRod-client-async-pool-1-2] org.infinispan.client.hotrod.impl.protocol.Codec20.readNewTopologyAndHash ISPN004006: Server sent new topology view (id=9, age=0) containing 3 addresses: [172.17.0.4:11222, 172.17.0.2:11222, 172.17.0.3:11222]
Tomcatのリッスンポートはこんな感じになっているので
$ sudo ss -tnlp | grep 8080 LISTEN 0 100 *:18080 *:* users:(("java",pid=57561,fd=203)) LISTEN 0 100 *:28080 *:* users:(("java",pid=57637,fd=203)) LISTEN 0 100 *:8080 *:* users:(("java",pid=57365,fd=203))
それぞれにアクセスして確認。
$ curl -c cookie-clustered -b cookie-clustered localhost:8080/counter current value = 1 $ curl -c cookie-clustered -b cookie-clustered localhost:18080/counter current value = 2 $ curl -c cookie-clustered -b cookie-clustered localhost:28080/counter current value = 3 $ curl -c cookie-clustered -b cookie-clustered localhost:8080/counter current value = 4 $ curl -c cookie-clustered -b cookie-clustered localhost:18080/counter current value = 5 $ curl -c cookie-clustered -b cookie-clustered localhost:28080/counter current value = 6
問題なく動いているようです。
web.xml
にdistributable
が要るかな?と思っていましたが、なくても良さそうです。HotRodManager
などのコードを見ても、
distributable
を見てはいなさそうでしたし。
まとめ
WildFly Clustering Tomcatを使って、TomcatのセッションをInfinispan Serverにオフロードしてみました。
認証設定がいると思っていなかったのでやや苦労しましたが…まあ、できたのでいいでしょう。