CLOVER🍀

That was when it all began.

JCache(Hazelcast) on Payara Microを試す

もうひとつのGlassFishとして名前を目にするPayaraに、Hazelcastが搭載されたと聞いて。

Payara
http://www.payara.co.uk/home

Using the JCache api with CDI on Payara
http://www.payara.co.uk/using_the_jcache_api_with_cdi_on_payara

JavaEE Applications Supercharged – Using JCache with Payara
http://hazelcast.com/resources/javaee-applications-supercharged-using-jcache-payara/

Hazelcast (Payara 4.1.151)
https://github.com/payara/Payara/wiki/Hazelcast-%28Payara-4.1.151%29

搭載されたのは、Payara 4.1.151からみたいですね。

What's New in Payara 4.1.151?
http://www.payara.co.uk/whats_new_in_payara_41151

インメモリ・データグリッドであるHazelcastが、PayaraのJCache実装およびクラスタ機能として採用された模様。

今回、WARファイルを指定してコマンドラインでWebアプリケーションを起動することができる、Payara Microを使用してJCacheを使ってみようと思います。

Introducing Payara Micro
http://www.payara.co.uk/introducing_payara_micro

そもそも、Hazelcastって?

インメモリ・データグリッドと呼ばれる、Map、List、SetなどのコレクションをJavaVM間で分散保持することができる製品の一種です。分散ExecutorService、Map Reduce FrameworkやQueryといった分散処理の機能を持ち、単なるキャッシュを超えるものになります。

Hazelcast
http://hazelcast.org/

このHazelcastがJCache(JSR-107)の実装を提供しており、Payaraはこちらを採用したようですね。

Hazelcastはインメモリ・データグリッドであるので、JCacheの実装として採用してもキャッシュに保存したエントリを分散保持できますが、JCacheそのものは分散保持については特に規程していません。分散保持などについては、完全に実装依存の範囲になります。

今回は、Payaraにすでに組み込まれているHazelcastをそのまま利用することにします。

準備

それでは、使っていってみましょう。まずは、Mavenのpom.xmlを用意。
pom.xml

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.littlewings</groupId>
    <artifactId>payara-micro-jcache-example</artifactId>
    <packaging>war</packaging>
    <version>0.0.1-SNAPSHOT</version>

    <build>
        <finalName>payara-micro-jcache-example</finalName>
    </build>

    <dependencies>
        <dependency>
            <groupId>fish.payara.extras</groupId>
            <artifactId>payara-micro</artifactId>
            <version>4.1.152.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>
</project>

依存関係は、「payara-micro」があればOKです。こちらを使用してWARファイルを作成し、Payara Microに与えて使います。

できあがるWARファイルの名前は、「payara-micro-jcache-example.war」とします。

また、Payara MicroのJARファイルそのものは、以下から取得しておいてください。

Downloads
http://www.payara.co.uk/downloads

Payara Micro (Embedded)と書かれているものから、JARファイルをダウンロードします。今回は、「payara-micro-4.1.152.1.jar」となりました。

JCacheを利用したサンプルを作る

それでは、JCacheを利用した簡単なサンプルを書いています。JAX-RS+CDI管理Beanを使ったものにしましょう。

JAX-RSの有効化。
src/main/java/org/littlewings/hazelcast/rest/JaxrsApplication.java

package org.littlewings.hazelcast.rest;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("rest")
public class JaxrsApplication extends Application {
}

JAX-RSリソースクラス。@Injectで、CDI管理Beanを注入します。
src/main/java/org/littlewings/hazelcast/rest/CalcResource.java

package org.littlewings.hazelcast.rest;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.littlewings.hazelcast.service.CalcService;

@Path("calc")
@RequestScoped
public class CalcResource {
    @Inject
    private CalcService calcService;

    @GET
    @Path("add")
    @Produces(MediaType.TEXT_PLAIN)
    public int add(@QueryParam("a") @DefaultValue("0") int a, @QueryParam("b") @DefaultValue("0") int b) {
        return calcService.add(a, b);
    }
}

CDI管理Bean。このクラスに付与しているアノテーションが、JCacheのものです。
src/main/java/org/littlewings/hazelcast/service/CalcService.java

package org.littlewings.hazelcast.service;

import java.util.concurrent.TimeUnit;
import javax.cache.annotation.CacheDefaults;
import javax.cache.annotation.CacheResult;
import javax.enterprise.context.ApplicationScoped;

@CacheDefaults(cacheName = "calcCache")
@ApplicationScoped
public class CalcService {
    @CacheResult
    public int add(int a, int b) {
        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
        }

        return a + b;
    }
}

@CacheDefaultsアノテーションで、このクラスで使用するCacheを宣言します。

@CacheDefaults(cacheName = "calcCache")

これは付与しなくても動作するのですが、そうすると利用するCacheの名前がクラス名から自動的に決められたりするので、Cacheの設定をすることなどを考えると決めておいた方がよいのではないかと思います。

続いて、@CacheResultアノテーション。

    @CacheResult
    public int add(int a, int b) {

こちらは、メソッドの引数をキーにして、戻り値をCacheに保存します。

あと、Cacheを利用していることがわかりやすいように3秒スリープを入れています。

動作確認

それでは、こちらをパッケージングして

$ mvn package

デプロイ&起動。

$ java -jar payara-micro-4.1.152.1.jar --deploy target/payara-micro-jcache-example.war

では、アクセスしてみます。1回目。

$ time curl 'http://localhost:8080/payara-micro-jcache-example/rest/calc/add?a=3&b=5'
8
real	0m3.360s
user	0m0.007s
sys	0m0.000s

3秒と少し(たぶん、初期化分…)かかっています。

2回目。

$ time curl 'http://localhost:8080/payara-micro-jcache-example/rest/calc/add?a=3&b=5'
8
real	0m0.030s
user	0m0.000s
sys	0m0.008s

Cacheの内容が使われたことにより、高速に動作するようになりました。

QueryStringに渡すパラメータを変えてみます。

$ time curl 'http://localhost:8080/payara-micro-jcache-example/rest/calc/add?a=4&b=3'
7
real	0m3.022s
user	0m0.000s
sys	0m0.007s

こうすると、キーが変わるのでまた3秒かかるようになりました。

少し補足

Payaraで使っているJCacheの実装の多くはHazelcastのものですが、CDIとの連携(@CachePutなどのアノテーションに絡むInterceptorなど)はPayaraが実装を提供しているようです。

以下に、Interceptorなどのクラスがゴロゴロと。

https://github.com/payara/Payara/tree/payara-server-4.1.152/appserver/payara-appserver-modules/payara-jsr107/src/main/java/fish/payara/cdi/jsr107

また、Hazelcastのインスタンスは、Payaraが起動時にJCacheを含めて作ってしまいます。

https://github.com/payara/Payara/blob/payara-server-4.1.152/nucleus/payara-modules/hazelcast-bootstrap/src/main/java/fish/payara/nucleus/hazelcast/HazelcastCore.java#L203

Cacheの設定がしたい

ところで、先ほど使ったCacheは実行時にInterceptorが勝手に作ったCacheを使用しました。今回は、Time To Liveを設定してみましょう。

となると、Cacheを自分で作成するポイントが欲しいのですが、Cache自体はCDI管理Beanとして引き抜かれるわけではないので、@Producesだとうまくいきません。今回は、ここで@SingletonでEJBを使いました。@WebListenerでもよいかもしれません。
src/main/java/org/littlewings/hazelcast/producer/CacheProducer.java

package org.littlewings.hazelcast.producer;

import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.cache.CacheManager;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.expiry.CreatedExpiryPolicy;
import javax.cache.expiry.Duration;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Inject;

@Singleton
@Startup
public class CacheProducer {
    @Inject
    private CacheManager manager;

    @PostConstruct
    public void createCalcCache() {
        Configuration<?, ?> configuration =
                new MutableConfiguration<>()
                        .setExpiryPolicyFactory(
                                CreatedExpiryPolicy
                                        .factoryOf(new Duration(TimeUnit.SECONDS, 10)));

        if (manager.getCache("calcCache") == null) {
            manager.createCache("calcCache", configuration);
        }
    }
}

@Startupと併用し、@PostConstructを付与したメソッド内でCacheが未定義であれば作成するようにしました。

Cacheエントリの有効期限は、書き込み後10秒としました。何回読み込みアクセスをしても、10秒後には有効期限切れします。再度Cacheへのエントリとして登録すると、そこからまた10秒間有効となります。

この状態でアプリケーションをパッケージングして

$ mvn package

再びデプロイ&起動。

$ java -jar payara-micro-4.1.152.1.jar --deploy target/payara-micro-jcache-example.war

最初のアクセスには3秒かかり、しばらくは高速になりますが、10秒経つとまた3秒かかるようになります。

クラスタ化する

最後に、Hazelcastがインメモリ・データグリッドであることを活かして、クラスタ化してみます。特にソースコードは触らずに、先ほどの状態のままアプリケーションを2つ起動します。

## Node 1
$ java -jar payara-micro-4.1.152.1.jar --deploy target/payara-micro-jcache-example.war

## Node 2
$ java -jar payara-micro-4.1.152.1.jar --deploy target/payara-micro-jcache-example.war --port 8180

2つ目のNodeは、リッスンするHTTPポートが被るので、8180とするようにしました。

起動途中に他のNodeを認識してクラスタに追加されます。

Members [2] {
	Member [192.168.254.129]:5900
	Member [192.168.254.129]:5901 this
}

(Payara内の)Hazelcastが使う開始ポートは5900のようですが、すでに利用されていると勝手に+1してくれます。
※デフォルトのHazelcastだと、5701が開始ポートです

起動したら、Node 1に最初にリクエストを投げてみます。

$ time curl 'http://localhost:8080/payara-micro-jcache-example/rest/calc/add?a=3&b=5'
8
real	0m3.291s
user	0m0.005s
sys	0m0.005s

3秒かかりました。続いて、ポートを変えてNode 2にアクセス。

$ time curl 'http://localhost:8180/payara-micro-jcache-example/rest/calc/add?a=3&b=5'
8
real	0m0.231s
user	0m0.007s
sys	0m0.004s

Cacheの定義が共有されているため、Node 2にアクセスしても高速に動作します。

これで、クラスタ化まで確認できました。

OKそうですね。

オマケ

javaコマンドで起動するのもよいのですが、IDEから起動したいなとも思い、最初はこんなクラスを書いて試していました(実行前に「mvn package」を行って起動するように設定)。
src/main/java/org/littlewings/hazelcast/Bootstrap.java

package org.littlewings.hazelcast;

import java.io.File;

import fish.payara.micro.BootstrapException;
import fish.payara.micro.PayaraMicro;

public class Bootstrap {
    public static void main(String... args) throws BootstrapException {
        PayaraMicro
                .getInstance()
                .addDeployment("target/payara-micro-jcache-example.war")
                .bootStrap();
    }
}

が、これだと起動時の引数をPayaraMicroに渡せないので断念。PayaraMicro#mainに渡せば、一応できますが…それはナシな気も。

それに最終的にはクラスタにしたので、コマンドラインになってしまいましたしね。

気になること

Hazelcastが組み込み済みで、スタートアップとしては非常に簡単に使えるのですが、その分設定の自由度は少し落ちています。

ソースコードを見ている限りは、Payaraをアプリケーションサーバーとして使う場合は、以下のような感じになるみたいです。

  • 設定ファイルは、上書き可能?

https://github.com/payara/Payara/blob/payara-server-4.1.152/nucleus/payara-modules/hazelcast-bootstrap/src/main/java/fish/payara/nucleus/hazelcast/HazelcastCore.java#L122

  • クラスタの構成は、Server、Client/Serverのうち、Serverのみ

https://github.com/payara/Payara/blob/payara-server-4.1.152/nucleus/payara-modules/hazelcast-bootstrap/src/main/java/fish/payara/nucleus/hazelcast/HazelcastCore.java#L205

これがPayara Microになると、以下になります。

  • 設定ファイルの指定はできない

https://github.com/payara/Payara/blob/payara-server-4.1.152/nucleus/payara-modules/hazelcast-bootstrap/src/main/java/fish/payara/nucleus/hazelcast/HazelcastCore.java#L132

Payara Microの場合は、個々のCacheの設定やHazelcastのデータ構造についての設定は、APIでやる感じでしょうね。クラスタ起動後ですが…できたっけ…?クラスタの構成方法を変えれない(マルチキャストのみ)とかいうのは、大丈夫かなーとか思ったり…。

また、どちらの構成にも言えますが、Serverモードで起動するのでNodeがデータを持ってしまいます。構成によっては、Client/Server構成にもしたくなると思うのですが、そのあたりどうなんでしょう。

なんでHazelcastなんでしょう?

これがちょっとした疑問で、Hazelcastはローカルモードみたいなものは持たないので、必ず分散構成前提で起動します。なので、単一Nodeだと他のCacheライブラリに比べてオーバーヘッドがあるので不利だったりします。

かといって、EhcacheのJCache実装はかなり怪しい状態だったりしますし、Reference Implementationはプロダクションで使わない方がいい宣言されているので、ある種落ち着くのはHazelcastなのかなと思わなくもないです。

そんな事情もあるので、個人的には各アプリケーションサーバーにJCacheの実装として何が搭載されるのかはけっこう興味のある話題だったりします(特に、周辺製品にグリッドを持たないものは)。

まあ、解のひとつが見れたということで。

終わりに

最後になりましたが、今回作成したソースコードはこちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/payara-micro-jcache-example