さよならスティッキーセッション!PayaraでJavaEEでもセッションをKVSに。

この記事は Java EE Advent Calendar 2015 の 9 日目です。 昨日は btnrouge さんの「Payara Microのからくり」でした。

意図せずしてPayaraネタ連続です。

はじめに

Payara MicroはSpringBootやWildfly Swarmあるいは一日目に紹介されていたKumuluzEEと同様にJavaEE向けマイクロサービス環境です。 javaコマンドで単純に実行できるDockerフレンドリーな奴ですね。

先に上げたMWと一番の違いはPayara MicroはEJBを組み直してfat-jarを作るわけではなく、最小構成のコンテナでwarをデプロイして動かす、ということです。 具体的には下記のようなコマンドで実行します。

$  java -jar payara-micro-4.1.1.154.jar --deploy example-payara_micro.war

payara-micoro.jarがシンプルに起動だけする感じですね。単一jarでは動かないとい点はありますが、ギャップが小さい分、不具合が圧倒的に少ないですね。 Wildfly Swarmなんかは色々アグレッシブな分、まだ安定してない感じがしますが...

通常のwarと開発方法が同じなので、開発時はNetBeans + Payaraで作って実行時はPayara Microみたいな開発が楽な気がします。

さて、そんなPayara Microですがもう1点重要な点として、JavaEEの特徴の一つであるスティッキーセッションを要求されません! GlassFishを始め、JavaEEではインメモリセッションレプリケーションが基本になっており、原則同じサーバへのアクセスが要求されます。 しかし、PayaraではKVSであるHazelcastを組み込んであるため、どこのサーバへのアクセスも透過になるのでスケールアウト構成がシンプルに構築できます。

LL系だとKVSにセッションを格納するのは珍しく無いですが、JavaEEだとOracle Coherenceを担ぎ出す必要があったので大きな一歩ですね。JCacheはHTTP Sessionをカバーする仕様ではなさそうですし。

準備

下記からbootstrapになるpayara-microのjarファイルを落とします。現時点での最新版は下記からダウンロードしてください。

www.payara.fish

実践

それでは実際に試していきたいと思います。まずは、mavenプロジェクトで今回は作成した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>cn.orz.pascal</groupId>
    <artifactId>example-payara_micro</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>example-payara_micro</name>

    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.6</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>fish.payara.extras</groupId>
            <artifactId>payara-micro</artifactId>
            <version>4.1.152.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>example-payara_micro</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArguments>
                        <endorseddirs>${endorsed.dir}</endorseddirs>
                    </compilerArguments>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.6</version>
                <executions>
                    <execution>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${endorsed.dir}</outputDirectory>
                            <silent>true</silent>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>javax</groupId>
                                    <artifactId>javaee-endorsed-api</artifactId>
                                    <version>7.0</version>
                                    <type>jar</type>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

基本、JavaEEベースのものですね? 続いてコードになります。せっかくなので、CDIのSessionScopeを使います。

@Data
@Named
@SessionScoped
public class User implements Serializable{
   String name;
   int age;
}

リクエストを受け取り、CDIをバインドするJAX-RSサービスは、StatelessBeansで作ります。

/**
 * REST Web Service
 *
 * @author koduki
 */
@Path("session")
@Stateless
public class SessionResource {

    @Inject
    private User user;

    /**
     * Creates a new instance of SessionResource
     */
    public SessionResource() {
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String get() {
        String version = "2.0";
        String msg = "Application version " + version + System.lineSeparator();
        msg += String.format("I am %s. I am %d years old.", user.getName(), user.getAge());
        return msg;
    }

    @GET
    @Path("update/{name}")
    @Produces(MediaType.APPLICATION_JSON)
    public User update(@PathParam("name") String name, @QueryParam("age") int age, @Context HttpServletRequest request) {
        user.setName(name);
        user.setAge(age);

        return user;
    }
}

では、こちらを動かして見ましょう。

$ mvn package
$ java -jar  java -jar payara-micro-4.1.1.154.jar --deploy target/example-payara_micro.war
[2015-12-09T07:19:25.856+0900] [Payara 4.1] [INFO] [NCLS-CORE-00087] [javax.enterprise.system.core] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449613165856] [levelValue: 800] Grizzly Framework 2.3.23 started in: 33ms - bound to [/0.0.0.0:8080]
.
.
Members [1] {
    Member [192.168.99.1]:5901 this
}
]]
.
.
[2015-12-09T07:19:39.340+0900] [Payara 4.1] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449613179340] [levelValue: 800] Deployed 1 wars

Deployed 1 warsになれば完了です。

途中、Membersと出ているところがHazelcastのクラスタです。 これは、マルチキャストグループで範囲が指定されていて自動的にひっかけてきます。 登録作業がいらないのでDokcerとかでダイナミックにインスタンスが増減しても、問題にならないのはいいですね。

では、検証コマンドを。ブラウザだと確認が面倒なのでcurlで実施しました。

$ curl -c cookie.txt -b cookie.txt -i http://localhost:8080/example-payara_micro/webresources/session
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONID=3ccb47b039bb17f872d2216c9cde; Path=/example-payara_micro; HttpOnly
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:0; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 22:53:29 GMT
Content-Length: 52

Application version 2.0
I am null. I am 0 years old.

最初は、セッショに何も値が入ってないのでnullが返ります。続いて値の登録して、結果を確認して見ます。

$ curl -c cookie.txt -b cookie.txt -i "http://localhost:8080/example-payara_micro/webresources/session/update/Nanoha?age=9"
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:1; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 22:55:24 GMT
Content-Length: 25

{"age":9,"name":"Nanoha"}%

$ curl -c cookie.txt -b cookie.txt -i http://localhost:8080/example-payara_micro/webresources/session
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:2; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 22:55:57 GMT
Content-Length: 54

Application version 2.0
I am Nanoha. I am 9 years old.

まずは、セッションが想定どおりに動いているのが確認できました。続いて、もう一つ立ち上げます。

$ java -jar ~/Downloads/payara-micro-4.1.1.154.jar --deploy /tmp/example-payara_micro.war --port 8180
Dec 09, 2015 7:57:46 AM org.glassfish.security.services.impl.authorization.AuthorizationServiceImpl initialize
.
.
Members [2] {
    Member [192.168.99.1]:5901
    Member [192.168.99.1]:5903 this
}
]]
.
.
[2015-12-09T07:58:06.839+0900] [Payara 4.1] [INFO] [] [PayaraMicro] [tid: _ThreadID=1 _ThreadName=main] [timeMillis: 1449615486839] [levelValue: 800] Deployed 1 wars

今回は同一サーバに複数個立ち上げるのでポートを変更してあります。 また、Membersに新しく追加されたのが確認できるかと思います。

では、新規に立ち上げた方のサーバにアクセスしてみます。

$ curl -c cookie.txt -b cookie.txt -i http://localhost:8180/example-payara_micro/webresources/session
HTTP/1.1 200 OK
Server: Payara Micro #badassfish
Set-Cookie: JSESSIONIDVERSION=2f6578616d706c652d7061796172615f6d6963726f:3; Path=/example-payara_micro; HttpOnly
Content-Type: application/json
Date: Tue, 08 Dec 2015 23:01:07 GMT
Content-Length: 54

Application version 2.0
I am Nanoha. I am 9 years old.%

nullではなく、適切にレプリケーションされた値が入ってることが確認できました! これでラウンドロビンでアクセスができますね!

Docker対応

やはり、この手のものはDocker経由で使いたいので、Dockerでの動きを検証してみました。

github.com

上記のDockerファイルを元に作成して、問題なく動作しました。

少なくとも同一ホストであれば、特になんの設定もしなくても Docker間でのマルチキャストによるディスカバリも含めてセッションレプリケーションも問題なく動作しました。

複数グループ

複数のセッション/クラスタグループを作るには、マルチキャストグループを変更することで可能です。

payara-microの場合は、IPが224.2.2.4がデフォルトのようなので

$ java -jar payara-micro-4.1.1.154.jar --deploy example-payara_micro.war -mcAddress 224.2.2.4

こんな感じで指定すればいいかと思いきや、現状では下記のようなエラーが出ます。

Dec 09, 2015 8:32:16 AM org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.1.2.Final
Exception in thread "main" fish.payara.micro.BootstrapException: PlainTextActionReporterFAILURENo configuration found for server.hazelcast-runtime-configuration
    at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:714)
    at fish.payara.micro.PayaraMicro.main(PayaraMicro.java:105)
Caused by: org.glassfish.embeddable.GlassFishException: PlainTextActionReporterFAILURENo configuration found for server.hazelcast-runtime-configuration
    at com.sun.enterprise.glassfish.bootstrap.ConfiguratorImpl.configure(ConfiguratorImpl.java:75)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.configure(GlassFishImpl.java:71)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.<init>(GlassFishImpl.java:65)
    at com.sun.enterprise.glassfish.bootstrap.StaticGlassFishRuntime$1.<init>(StaticGlassFishRuntime.java:116)
    at com.sun.enterprise.glassfish.bootstrap.StaticGlassFishRuntime.newGlassFish(StaticGlassFishRuntime.java:116)
    at fish.payara.micro.PayaraMicro.bootStrap(PayaraMicro.java:694)
    ... 1 more

masterでは修正されたようなので、しばらく更新を待つしかないですね。 ちなみに、マイクロじゃない方のPayaraでは問題なく動作しましたので、すぐ使いたいならそちらで。

バージョニング

今回の構成ではラウンドロビンでバランシングができるので、シンプルなリリース要件ならブルーグリーンデプロイメントで問題ないと思います。 ただし、リリース中に一切ログインさせない、不整合も起こさせないといった完全なZero-downtime Deploymentを実施するならバージョン毎に別のセッションが必要です。

しかし、現行のGlassFish及びそれをベースとしたPayaraにはアプリバージョン毎のセッション等は作成できないので、 そちらに関しては別途の仕組みの検討が要りますし、いずれにしてもスティッキー性が必要になってきます。

まとめ

今回はPayara Microでのセッションレプリケーションを確認しました。実にDocker時代と相性の良さそうな機能ですね。 将来的には、mod_mrubyと組み合わせてZero-downtime Deploymentも作ってみたいと思います。

では、明日はsusumuisさんの「Javaプログラマー12年の僕がSpring童貞卒業的なこと書きます!」です!

Happy Hacking!

参考