CLOVER🍀

That was when it all began.

HazelcastのClientで遊んでみる

うちのブログ、けっこうHazelcastについて書いていますが、Server側ばかりでほとんどClient側を使っていないので、たまには試してみようかなということで。

ClientにはJavaC++、.NETがあり、それ以外の形態としてREST、Memcachedがあります。

Clients
http://docs.hazelcast.org/docs/3.4/manual/html-single/hazelcast-documentation.html#clients

今回は、Java Clientを使用します。

Java Client
http://docs.hazelcast.org/docs/3.4/manual/html-single/hazelcast-documentation.html#java-client

Java Clientは、Hazelcastの機能がほぼ使えるClient Modeで、Serverとの主な違いはClient自身はデータを持たないことにあります。よって、Clientは接続先のServerがひとつもない状態では起動することができません。先に、接続先のServer(というかHazelcastクラスタ)が存在しておく必要があります。

なので、今回はServer側を用意し、それからそのHazelcastクラスタにアクセスするClientを書いていきます。

準備

ちょっとした意図があって、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>hazelcast-client-routing</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <modules>
        <module>hazelcast-client-routing-server</module>
        <module>hazelcast-client-routing-client</module>
    </modules>
    <packaging>pom</packaging>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.hazelcast</groupId>
                <artifactId>hazelcast</artifactId>
                <version>3.4.2</version>
            </dependency>
            <dependency>
                <groupId>com.hazelcast</groupId>
                <artifactId>hazelcast-client</artifactId>
                <version>3.4.2</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <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>
    </properties>
</project>

利用するHazelcastは、3.4.2です。このpomを親として、ServerとClientを書いていきます。

Server側

まずはServer側から。pomは、このように。
hazelcast-client-routing-server/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">
    <parent>
        <artifactId>hazelcast-client-routing</artifactId>
        <groupId>org.littlewings</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>hazelcast-client-routing-server</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
        </dependency>
    </dependencies>
</project>

非常に簡単ですが、設定ファイルも用意しました。
hazelcast-client-routing-server/src/main/resources/hazelcast.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.4.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <group>
        <name>my-cluster</name>
        <password>my-password</password>
    </group>

    <network>
        <port auto-increment="true" port-count="100">5701</port>
        <outbound-ports>
            <ports>0</ports>
        </outbound-ports>
        <join>
            <multicast enabled="true">
                <multicast-group>224.2.2.3</multicast-group>
                <multicast-port>54327</multicast-port>
            </multicast>
            <tcp-ip enabled="false"/>
        </join>
    </network>
</hazelcast>

クラスタ名とパスワードをちょっと変えた程度です。

Server側のプログラムも、非常に簡単です。
hazelcast-client-routing-server/src/main/java/org/littlewings/hazelcast/Server.java

package org.littlewings.hazelcast;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;

public class Server {
    public static void main(String... args) {
        HazelcastInstance hazelcast =
                Hazelcast.newHazelcastInstance(new ClasspathXmlConfig("hazelcast.xml"));

        try {
            System
                    .console()
                    .readLine("[%s] Hazelcast Server Startup.",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        } finally {
            hazelcast.getLifecycleService().shutdown();
            Hazelcast.shutdownAll();
        }
    }
}

ほぼ何もしていません。プロセスとして浮いていてもらうだけです。

Enterを入力したら、終了するプログラムになっています。

これでおしまい。

Client側

そして、Client側。
hazelcast-client-routing-client/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">
    <parent>
        <artifactId>hazelcast-client-routing</artifactId>
        <groupId>org.littlewings</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>hazelcast-client-routing-client</artifactId>
    <packaging>jar</packaging>


    <dependencies>
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast-client</artifactId>
        </dependency>
    </dependencies>
</project>

設定ファイルも用意。
hazelcast-client-routing-client/src/main/resources/hazelcast-client.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast-client xsi:schemaLocation="http://www.hazelcast.com/schema/client-config hazelcast-client-config-3.4.xsd"
                  xmlns="http://www.hazelcast.com/schema/client-config"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <group>
        <name>my-cluster</name>
        <password>my-password</password>
    </group>

    <network>
        <smart-routing>true</smart-routing>
        <redo-operation>true</redo-operation>
    </network>

    <load-balancer type="random"/>
</hazelcast-client>

Server側でクラスタ名とパスワードを変えているので、Client側も合わせて変えておきます。

プログラム。putとgetが、対話形式でできるようにしました。
hazelcast-client-routing-client/src/main/java/org/littlewings/hazelcast/Client.java

package org.littlewings.hazelcast;

import java.io.IOException;
import java.util.Objects;
import java.util.stream.Stream;

import com.hazelcast.client.HazelcastClient;
import com.hazelcast.client.config.XmlClientConfigBuilder;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;

public class Client {
    public static void main(String... args) throws IOException {
        HazelcastInstance client =
                HazelcastClient.newHazelcastClient(new XmlClientConfigBuilder("src/main/resources/hazelcast-client.xml").build());

        try {
            IMap<String, Language> map = client.getMap("language");

            Stream
                    .generate(() -> System.console().readLine("Command> "))
                    .filter(Objects::nonNull)
                    .forEach(line -> {
                        String[] tokens = line.split("\\s+");
                        String command = tokens[0];

                        switch (command) {
                            case "put":
                                String key = tokens[1];
                                Language language = new Language(tokens[2]);
                                map.put(key, language);
                                System.out.printf("putted %s/%s%n", key, language);
                                break;
                            case "get":
                                System.out.printf("get => %s%n", map.get(tokens[1]));
                                break;
                            default:
                                System.out.printf("Unknown command => %s%n", command);
                                break;
                        }
                    });
        } finally {
            client.getLifecycleService().shutdown();
            HazelcastClient.shutdownAll();
        }
    }
}

HazelcastのDistributed Mapに登録するクラス。こちらは、Client側にしかありません。
hazelcast-client-routing-client/src/main/java/org/littlewings/hazelcast/Language.java

package org.littlewings.hazelcast;

import java.io.Serializable;

public class Language implements Serializable {
    private String name;

    public Language(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}

動作確認

親プロジェクトでコンパイルして

$ mvn compile

まずはServerを起動して、クラスタを構成します。

$ cd hazelcast-client-routing-server

## Node 1
$ mvn exec:java -Dexec.mainClass=org.littlewings.hazelcast.Server

## Node 2
$ mvn exec:java -Dexec.mainClass=org.littlewings.hazelcast.Server

クラスタが構成されます。

Members [2] {
	Member [192.168.254.129]:5702 this
	Member [192.168.254.129]:5701
}

そして、Clientを起動。

$ cd ../hazelcast-client-routing-client
$ mvn exec:java -Dexec.mainClass=org.littlewinghazelcast.Client

現在のクラスタメンバーが表示されますが、Client自身はこの中のメンバーではありません。

Members [2] {
	Member [192.168.254.129]:5702
	Member [192.168.254.129]:5701
}

いくつかコマンドを実行してみます。

Command> put 1 java
putted 1/java
Command> put 2 groovy
putted 2/groovy
Command> put 3 perl
putted 3/perl
Command> get 2
get => groovy

ここで、いったんClientをCtrl-Cで終了して、再度起動してみます。

Clientが起動したら、getしてみます。

Command> get 1
get => java

Clientが終了しても、データは保存されたままです。ところで、ここまでが普通に動いていることからわかりますが、Server側はClientが保存するデータのClassを認識する必要がないということですね。Server側には、Languageクラスを置いていないので。

Serverを途中で1 Node落としてみる

ここで、Clientのネットワーク設定をちょっと変更します。

    <network>
        <cluster-members>
            <address>localhost:5701</address>
        </cluster-members>

        <smart-routing>true</smart-routing>
        <redo-operation>true</redo-operation>
    </network>

クラスタの初期メンバーlocalhostの5701としました。この設定にすると、最初にlocalhost:7001のメンバーにアクセスしにいきます。

ここで、Server側のNode 1を落とします。

情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Shutting down node engine...
6 06, 2015 7:56:36 午後 com.hazelcast.instance.NodeExtension
情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Destroying node NodeExtension.
6 06, 2015 7:56:36 午後 com.hazelcast.instance.Node
情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Hazelcast Shutdown is completed in 866 ms.
6 06, 2015 7:56:36 午後 com.hazelcast.core.LifecycleService
情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Address[192.168.254.129]:5701 is SHUTDOWN

クラスタに残ったメンバーは、

Members [1] {
	Member [192.168.254.129]:5702 this
}

だけです。

この状態でClientを起動しようとすると、

$ mvn exec:java -Dexec.mainClass=org.lhazelcast.Client

5701ポートでリッスンしているメンバーがいなくなってしまったので、エラーになります。

6 06, 2015 7:57:39 午後 com.hazelcast.client.spi.impl.ClusterListenerThread
重大: Error while connecting to cluster!
java.lang.IllegalStateException: Unable to connect to any address in the config! The following addresses were tried:[/127.0.0.1:5701]
	at com.hazelcast.client.spi.impl.ClusterListenerThread.connectToOne(ClusterListenerThread.java:286)
	at com.hazelcast.client.spi.impl.ClusterListenerThread.run(ClusterListenerThread.java:89)
Caused by: java.net.ConnectException: 接続を拒否されました
	at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
	at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)

ちなみに、Clientのネットワーク設定の初期値は

        <cluster-members>
            <address>localhost</address>
        </cluster-members>

で、この状態だと5701ポートでリッスンしているNodeがでなくても、5702などの他のポートでリッスンしているメンバーがいれば見つけてくれるようです。

それでは、1度Hazelcastクラスタを起動しなおします。

## Node 1
$ mvn exec:java -Dexec.mainClass=org.littlewings.hazelcast.Server

## Node 2
$ mvn exec:java -Dexec.mainClass=org.littlewings.hazelcast.Server

クラスタが構成されます。

Members [2] {
	Member [192.168.254.129]:5702 this
	Member [192.168.254.129]:5701
}

そして、クライアントを起動。

$ mvn exec:java -Dexec.mainClass=org.littlewings.hazelcast.Client

クラスタ内のメンバーを認識するので、

Members [2] {
	Member [192.168.254.129]:5702
	Member [192.168.254.129]:5701
}

適当にデータ登録。

Command> put 1 java
putted 1/java
Command> put 2 groovy
putted 2/groovy
Command> put 3 clojure
putted 3/clojure
Command> put 4 perl
putted 4/perl

ここで、Node 1を落とします。

情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Shutting down node engine...
6 06, 2015 8:07:48 午後 com.hazelcast.instance.NodeExtension
情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Destroying node NodeExtension.
6 06, 2015 8:07:48 午後 com.hazelcast.instance.Node
情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Hazelcast Shutdown is completed in 757 ms.
6 06, 2015 8:07:48 午後 com.hazelcast.core.LifecycleService
情報: [192.168.254.129]:5701 [my-cluster] [3.4.2] Address[192.168.254.129]:5701 is SHUTDOWN

残ったのはNode 2だけで、Clientもそれを認識します。

Members [1] {
	Member [192.168.254.129]:5702
}

このまま、操作を継続することができます。

Command> get 1
get => java
Command> get 3
get => clojure

初期メンバーに定義していたNode以外もクラスタメンバーとして認識するし、途中でクラスタメンバーに変化があってもそれを認識してClientも追従してくれるということですね。

まとめ

HazelcastのClientで遊んでみましたが、クラスタにさえ接続できれば、あとでクラスタのNodeに増減があってもそれに追従してくれることがわかりました。また、ClientはServerがいないと接続できないことの再確認や、Server側はClientが保存するデータの型(Class)を知らなくてもよいことが確認できました、と。

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