CLOVER🍀

That was when it all began.

Hazelcast on Payara Microを試す

この前、Payara Microを使ってJCache越しにHazelcastで遊んでみました。

JCache(Hazelcast) on Payara Microを試す
http://d.hatena.ne.jp/Kazuhira/20150523/1432366272

今度は、HazelcastのAPIを直に使ってみたいと思います。利用するHazelcastのインスタンスは、Payara Micro自身が管理しているものを前提にします。

準備

Mavenの定義は、前と変わらず。
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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>payra-micro-rawapi</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <build>
        <finalName>payara-micro-rawapi</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>

「mvn package」で生成されるWARファイルの名前は、「payra-micro-rawapi.war」になります。

Payara Micro自体も、以下のページからダウンロードしておいてください。

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

今回も、「payara-micro-4.1.152.1.jar」を使用します。

あと、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 {
}

HazelcastInstanceを取得する

それでは、Hazelcastを使うにあたり、中心となるHazelcastInstanceの取得方法から。

ぶっちゃけ、ここを見ればOKです。

5. Using Hazelcast in your Applications
https://github.com/payara/Payara/wiki/Hazelcast-%28Payara-4.1.151%29#5-using-hazelcast-in-your-applications

JNDIルックアップしてね、と。
※JCacheの各種クラスもルックアップ可能ですが、今回は端折ります

確認用に、こんなクラスを用意。
src/main/java/org/littlewings/hazelcast/rest/HazelcastLookupResource.java

package org.littlewings.hazelcast.rest;

import javax.annotation.Resource;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.hazelcast.core.HazelcastInstance;
import fish.payara.nucleus.hazelcast.HazelcastCore;

@Path("lookup")
@RequestScoped
public class HazelcastLookupResource {
    // ここに、確認用のコードを書く
}

では、いくつかバリエーションを。

JNDIルックアップ。ドキュメント上は、こちらが正面案。

    @GET
    @Path("jndi")
    @Produces(MediaType.TEXT_PLAIN)
    public String jndi() throws NamingException {
        InitialContext context = new InitialContext();

        try {
            HazelcastInstance instance = (HazelcastInstance) context.lookup("payara/Hazelcast");
            return instance.getName();
        } finally {
            context.close();
        }
    }

JNDI名は、「payara/Hazelcast」だそうで。

@Resourceで注入。

    @Resource(name = "payara/Hazelcast")
    private HazelcastInstance hazelcastInstanceByResourcd;

    @GET
    @Path("resource")
    @Produces(MediaType.TEXT_PLAIN)
    public String resource() {
        return hazelcastInstanceByResourcd.getName();
    }

CDIで引き抜く。

    @Inject
    private HazelcastInstance hazelcastInstanceByCdi;

    @GET
    @Path("cdi")
    @Produces(MediaType.TEXT_PLAIN)
    public String cdi() {
        return hazelcastInstanceByCdi.getName();
    }

まあ、これや@Resourceで抜けるのは、このJAX-RSリソースクラスがCDI管理Beanだからかと…。

あと、fish.payara.nucleus.hazelcast.HazelcastCoreというクラスを使うと…もっと簡単に引っこ抜けるのですが…どう見ても内部APIなので割愛。興味があれば、後でGitHubのコードを。

ひとつくらいは、動作確認の結果を貼っておきましょう。

$ curl 'http://localhost:8180/payara-micro-rawapi/rest/lookup/jndi'
glassfish-web.server

いずれのAPIも、HazelcastInstanceの名前を返します。結果は、すべて同じ名前です(同じインスタンスを見ているから、そりゃあそうだという話ですが)。インスタンス名は、「glassfish-web.server」です。

Distributed Mapを使ってみる

続いて、Hazelcastが提供する分散データ構造を使ってみましょう。今回は、Distributed Mapを使います。その他、ListやSet、Queueなどいろいろあるので、興味のある方はHazelcastのドキュメントを参照してください。

では、Distributed Mapを使うということで…CDIのProducerを定義。
※このコードには問題があるので、後ろの方で修正します。
src/main/java/org/littlewings/hazelcast/producer/DistributedMapProducer.java

package org.littlewings.hazelcast.producer;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;

import com.hazelcast.config.Config;
import com.hazelcast.config.MapConfig;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;

@Dependent
public class DistributedMapProducer {
    @Inject
    private HazelcastInstance hazelcastInstance;

    @ApplicationScoped
    @Produces
    public IMap<String, String> createSimpleDistributedMap() {
        return hazelcastInstance.getMap("default");
    }

    @ApplicationScoped
    @Produces IMap<String, Integer> createWithExpiryDistributedMap() {
        Config config = hazelcastInstance.getConfig();

        MapConfig mapConfig = new MapConfig("withExpiryMap");
        mapConfig.setTimeToLiveSeconds(10);

        config.addMapConfig(mapConfig);

        return hazelcastInstance.getMap("withExpiryMap");
    }
}

ひとつはデフォルトのDistributed Mapを定義、そしてもうひとつはエントリの有効期限が10秒のDistributed Mapを定義しました。Qualifierは使いたくなかったので、型パラメーターで分けるということに…。

Payara Microでは、Hazelcastの設定はこんな形でAPI経由でしかできなさそうな感じで、設定ファイルは使える雰囲気がありません。また、先にHazelcastのインスタンスが起動してしまうので、クラスタ構成回りにはPayara Microの起動パラメーターの範囲でしか変更できなさそうな感じです。また、Hazelcastクラスタの構成については、Server(Embedded?)構成、Client/Server構成のうち、Server構成しかとることができません。

この点は、注意ですね。

それでは、これらを利用するCDI管理Beanを用意。

まずは、デフォルトのDistributed Mapを使う方。
src/main/java/org/littlewings/hazelcast/service/MessageService.java

package org.littlewings.hazelcast.service;

import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import com.hazelcast.core.IMap;

@ApplicationScoped
public class MessageService {
    @Inject
    private IMap<String, String> simpleMap;

    public String build(String key, String word) {
        if (simpleMap.containsKey(key)) {
            return simpleMap.get(key);
        }

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
        }

        String message = String.format("Hello %s!!", word);

        simpleMap.put(key, message);
        return simpleMap.get(key);
    }
}

キー、そして渡された単語に、「Hello 」をくっつけるだけの簡単なもの…。Distributed Mapを使っていることがわかりやすいように、3秒間のスリープを入れています。キーは、そのままDistributed Mapのキーになります。

有効期限付きの方。
src/main/java/org/littlewings/hazelcast/service/TripleService.java

package org.littlewings.hazelcast.service;

import java.util.concurrent.TimeUnit;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import com.hazelcast.core.IMap;

@ApplicationScoped
public class TripleService {
    @Inject
    private IMap<String, Integer> withExpiryMap;

    public int execute(String key, int seed) {
        if (withExpiryMap.containsKey(key)) {
            return withExpiryMap.get(key);
        }

        try {
            TimeUnit.SECONDS.sleep(3L);
        } catch (InterruptedException e) {
        }

        int tripled = seed * 3;

        withExpiryMap.put(key, tripled);
        return withExpiryMap.get(key);
    }
}

こちらは、Distributed Mapの型パラメーターがString、Integerなので、キーともらった値を3倍して返すクラスに…。

いや、よくわからないサンプルですが、あまり良いものが思いつかずでして…。

で、これらを使用するJAX-RSリソースクラス。
src/main/java/org/littlewings/hazelcast/rest/DistributedMapResource.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.MessageService;
import org.littlewings.hazelcast.service.TripleService;

@Path("dist")
@RequestScoped
public class DistributedMapResource {
    @Inject
    private MessageService messageService;

    @GET
    @Path("simple")
    @Produces(MediaType.TEXT_PLAIN)
    public String simple(@QueryParam("key") @DefaultValue("key") String key, @QueryParam("word") @DefaultValue("World") String word) {
        return messageService.build(key, word);
    }

    @Inject
    private TripleService tripleService;

    @GET
    @Path("expiry")
    @Produces(MediaType.TEXT_PLAIN)
    public int expiry(@QueryParam("key") @DefaultValue("key") String key, @QueryParam("value") @DefaultValue("0") int value) {
        return tripleService.execute(key, value);
    }
}

動作確認

パッケージングして

$ mvn package

動かしてみます。

今回は、最初から2 Nodeでいきましょう。

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

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

ひとつめのNodeのリッスンポートが8080、2つめが8180です。

起動中に、このような表示が出てクラスタが構成されたことが確認できます(こちらは、Node 2の表示です)。

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

では、アクセスしてみます。まずは、デフォルトのDistributed Mapから。Node 1へアクセス。

$ time curl 'http://localhost:8080/payara-micro-rawapi/rest/dist/simple?key=key1&word=Hazelcast'
Hello Hazelcast!!
real	0m3.400s
user	0m0.000s
sys	0m0.009s

3秒少々(起動直後なので、若干遅い…)。続いて、ポートを変えてNode 2へ。

$ time curl 'http://localhost:8180/payara-micro-rawapi/rest/dist/simple?key=key1&word=Hazelcast'
Hello Hazelcast!!
real	0m0.503s
user	0m0.000s
sys	0m0.006s

こちらは、3秒待ったりしません。

キーを変えると、また3秒かかります。

$ time curl 'http://localhost:8180/payara-micro-rawapi/rest/dist/simple?key=key10000&word=Hazelcast'
Hello Hazelcast!!
real	0m3.033s
user	0m0.006s
sys	0m0.004s

Node 1へ。

$ time curl 'http://localhost:8080/payara-micro-rawapi/rest/dist/simple?key=key10000&word=Hazelcast'
Hello Hazelcast!!
real	0m0.025s
user	0m0.008s
sys	0m0.000s

OKそうですね。

続いて、有効期限付きの方へ。Node 1から。

$ time curl 'http://localhost:8080/payara-micro-rawapi/rest/dist/expiry?key=key1&value=3'
9
real	0m3.058s
user	0m0.005s
sys	0m0.003s

Node 2。

$ time curl 'http://localhost:8180/payara-micro-rawapi/rest/dist/expiry?key=key1&value=3'
9
real	0m0.073s
user	0m0.007s
sys	0m0.000s

データが共有されていることがわかります。

そしてしばらくすると、

$ time curl 'http://localhost:8180/payara-micro-rawapi/rest/dist/expiry?key=key1&value=3'
9
real	0m3.024s
user	0m0.000s
sys	0m0.007s

と時間がかかるようになり、10秒後にはまた時間がかかようになるのですが…。

なんか、先にデフォルトのDistributed Mapにアクセスすると有効期限が無視されるようになりました…。デフォルト設定が最初に見られたのがダメなんでしょうなぁ、と。

というわけで、Producerをこう修正。

@Dependent
public class DistributedMapProducer {
    @Inject
    private HazelcastInstance hazelcastInstance;

    @ApplicationScoped
    @Produces
    public IMap<String, String> createSimpleDistributedMap() {
        return hazelcastInstance.getMap("default");
    }

    @ApplicationScoped
    @Produces IMap<String, Integer> createWithExpiryDistributedMap() {
        /*
        Config config = hazelcastInstance.getConfig();

        MapConfig mapConfig = new MapConfig("withExpiryMap");
        mapConfig.setTimeToLiveSeconds(10);

        config.addMapConfig(mapConfig);
        */

        return hazelcastInstance.getMap("withExpiryMap");
    }

    @PostConstruct
    public void configuration() {
        Config config = hazelcastInstance.getConfig();

        MapConfig mapConfig = new MapConfig("withExpiryMap");
        mapConfig.setTimeToLiveSeconds(10);

        config.addMapConfig(mapConfig);
    }
}

これなら、最初にデフォルトのDistributed Mapを参照しても大丈夫になりました。普段、XMLファイルで設定してるからですねぇ…ちょっと踏んだ気分ですが、できたので良しとしましょう。

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