CLOVER🍀

That was when it all began.

HazelcastのLite Membersを試す

Hazelcast 3.6から、Lite Membersという機能が追加されています。

Lite Members: With the re-introduction of Hazelcast Lite Members (it was removed starting with Hazelcast 3.0 release), you are able to specify certain members in your cluster so that they do not store data. You can use these lite members mostly for your task executions and listener registrations. Please refer to Enabling Lite Members.

http://docs.hazelcast.org/docs/release-notes/index.html#36

Enabling Lite Members

Lite Membersというのは、Server構成を取ってクラスタを構成しつつも、データを持たないMemberとしてクラスタに参加できる機能なようです。

Release Notesを見ていると、3.0のリリース時に削除されたものっぽいですねぇ…。

これができると何が嬉しいかということですが、ドキュメントによるとタスクを実行したり、Listenerを登録するMemberとして利用されることを想定しているみたいですね。

でも、どうなのでしょう?分散タスク/Aggregationsを実行する時にはData Localityの都合を考えると、それほど嬉しくないのでは?という気がしないでも。タスクをキックするだけ、という割り切りならよいかもしれません。

Listenerは好例でしょうね。

あとは、Client/Server構成時だと制限を受けるような時に、利用できると嬉しいかもですね(あんまりClient/Server構成と、Serverのみの構成時のメリット・デメリット覚えてませんけど)。

まあ、とりあえず使ってみましょう。

準備

Maven依存関係は、以下のように定義。

        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>3.6.2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.4.1</version>
            <scope>test</scope>
        </dependency>

とりあえず、hazelcastのみを利用します。あとは、テストコード用にJUnitとAssertJです。

テストコードの雛形

テストを実行するための、雛形コードを以下に。
src/test/java/org/littlewings/hazelcast/litemember/LiteMemberTest.java

package org.littlewings.hazelcast.litemember;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import com.hazelcast.config.ClasspathXmlConfig;
import com.hazelcast.config.Config;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.Partition;
import com.hazelcast.partition.NoDataMemberInClusterException;
import org.assertj.core.data.MapEntry;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class LiteMemberTest {
    // ここに、テストを書く!
}

Hazelcastを使った簡易なクラスタ構成のためのメソッドなども用意するのですが、これは順次記載していきます。

Lite Membersのみでプログラムを実行する

最初は、Lite Memberのみ(データを持たないMemberのみ)でクラスタを構成してみます。

Lite Memberを作成するためのメソッドとして、以下を用意。

    protected void withLiteMember(Consumer<HazelcastInstance> consumer) {
        Config config = new Config();
        config.setLiteMember(true);

        HazelcastInstance hazelcast = Hazelcast.newHazelcastInstance(config);
        try {
            consumer.accept(hazelcast);
        } finally {
            hazelcast.getLifecycleService().shutdown();
        }
    }

通常と違うところは、Config#setLiteMemberにtrueを設定していることだけです。これで、Lite Memberという扱いになります。

テストコードはこちら。

    @Test
    public void standaloneLiteMember() {
        withLiteMember(hazelcast -> {
            Map<String, String> map = hazelcast.getMap("default");
            assertThatThrownBy(() -> map.put("key", "value"))
                    .isInstanceOf(NoDataMemberInClusterException.class)
                    .hasMessage("Partitions can't be assigned since all nodes in the cluster are lite members");
        });
    }

検証コードが示していることから明らかかもですが、このコードはHazelcastのDistributed Mapにデータをputする時に失敗します。

データを持てるMemberが、ひとりもいないからですね。

というわけで、少なくともデータを持てるMemberを追加する必要がありそうです。

Lite Member+通常のMember

では、ひとつMemberを増やしてクラスタを構成してみましょう。

通常のHazelcastのMemberを任意数起動する、ヘルパーメソッドを用意します。

    protected void withHazelcast(Consumer<HazelcastInstance> consumer) {
        withHazelcast(1, consumer);
    }

    protected void withHazelcast(int numInstances, Consumer<HazelcastInstance> consumer) {
        List<HazelcastInstance> hazelcastInstances = IntStream
                .rangeClosed(1, numInstances)
                .mapToObj(i -> Hazelcast.newHazelcastInstance(new Config()))
                .collect(Collectors.toList());

        hazelcastInstances.forEach(h -> h.getLifecycleService().shutdown());
    }

こちらと、先ほどのLite Member用のメソッドを組み合わせて、テストコードを作成します。最初に通常のMemberを起動して、その後にLite Memberを起動するようにしました。

    @Test
    public void liteMemberWithNormalMember() {
        withHazelcast(hasDataHazelcast -> {
            withLiteMember(liteHazelcast -> {
                Map<String, String> map = liteHazelcast.getMap("default");
                map.put("key", "value");

                assertThat(map)
                        .containsExactly(MapEntry.entry("key", "value"));

                assertThat(liteHazelcast.getConfig().isLiteMember()).isTrue();

                assertThat(liteHazelcast.getCluster().getMembers()).hasSize(2);

                Set<Partition> partitions = liteHazelcast.getPartitionService().getPartitions();
                assertThat(partitions).hasSize(271);
                assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(1);

                Partition partition = partitions.stream().findAny().get();
                assertThat(partition.getOwner())
                        .isEqualTo(hasDataHazelcast.getCluster().getLocalMember())
                        .isNotEqualTo(liteHazelcast.getCluster().getLocalMember());
            });
        });
    }

この構成だと、データのputが可能になります。

Lite Memberと設定したMember自体は、そのように構成されていることが確認できます。

                assertThat(liteHazelcast.getConfig().isLiteMember()).isTrue();

また、Hazelcast内部のPartition数はデフォルトの271ですが、オーナーがひとつに偏っていることが確認できます。

                Set<Partition> partitions = liteHazelcast.getPartitionService().getPartitions();
                assertThat(partitions).hasSize(271);
                assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(1);

                Partition partition = partitions.stream().findAny().get();
                assertThat(partition.getOwner())
                        .isEqualTo(hasDataHazelcast.getCluster().getLocalMember())
                        .isNotEqualTo(liteHazelcast.getCluster().getLocalMember());

この時、普段見るHazelcastのクラスタ構成とは異なり、クラスタ構成時にMemberが複数現れません。

Members [1] {
	Member [172.17.0.1]:5701 this
}

Lite Member分が現れていない、という方が正確な気がしますが。ポートのリッスンも行われていないようです。

ただ、クラスタ内のMember数は「2」だと言いますが。

                assertThat(liteHazelcast.getCluster().getMembers()).hasSize(2);

なお、先ほどのLite Memberのみで起動した時はさすがにどうにもならないのか(?)、5701ポートでのリッスンとMembersの表示が行われますが。

通常のクラスタ構成時と比較してみる

一応、通常のクラスタ構成時にどうなるか、載せておきましょう。この構成では、Lite Memberはいません。

    @Test
    public void normalHazelcastCluster() {
        withHazelcast(2, hazelcast -> {
            Map<String, String> map = hazelcast.getMap("default");
            map.put("key", "value");

            assertThat(map)
                    .containsExactly(MapEntry.entry("key", "value"));

            assertThat(hazelcast.getConfig().isLiteMember()).isFalse();

            Set<Partition> partitions = hazelcast.getPartitionService().getPartitions();
            assertThat(partitions).hasSize(271);
            assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(2);
        });
    }

distinctをとっても、結果が2になりますね。この構成だと、両Memberがデータを持てるからです。

少し内部的な

ここで、少しHazelcast内でどうなっているか見てみましょう。

サラッと見た感じ(と挙動から)、データを扱ったりPartitionについて見るときには、データを持てるMemberでのみ見ているみたいですね。
https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast/src/main/java/com/hazelcast/partition/impl/InternalPartitionServiceImpl.java#L383-L386

絞り込みは、こちらのクラスを使って行われる模様。
https://github.com/hazelcast/hazelcast/blob/v3.6.2/hazelcast/src/main/java/com/hazelcast/cluster/memberselector/MemberSelectors.java

Lite Memberの判定を使っている箇所は、それほど多くありません。NodeやMemberなどに属性として保持されていますが、実際に条件分岐などに利用されているのは少なめです。


設定ファイルからLite Memberを構成する

最後に、Lite Memberを設定ファイルで構成してみたいと思います。

ヘルパーメソッドとして、設定ファイルからHazelcastInstanceを構成するメソッドを用意。

    protected void withHazelcast(String configFilePath, Consumer<HazelcastInstance> consumer) {
        withHazelcast(configFilePath, 1, consumer);
    }

    protected void withHazelcast(String configFilePath, int numInstances, Consumer<HazelcastInstance> consumer) {
        List<HazelcastInstance> hazelcastInstances = IntStream
                .rangeClosed(1, numInstances)
                .mapToObj(i -> {
                    ClasspathXmlConfig config = new ClasspathXmlConfig(configFilePath);
                    return Hazelcast.newHazelcastInstance(config);
                })
                .collect(Collectors.toList());

        hazelcastInstances.forEach(h -> h.getLifecycleService().shutdown());
    }

こちらを使って、設定ファイルを読み込ませてHazelcastクラスタを構成します。

設定ファイルは、Lite Memberとデータを持てるMember用で2つ用意。

Lite Member用。
src/test/resources/hazelcast-litemember.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.6.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <lite-member enabled="true"/>
</hazelcast>

lite-memberタグの、enabled属性をtrueにすれば、それでLite Memberとなります。特定のMapの配下などではありません。

データを持てるMemberの方は、ほぼ設定していませんがわかりやすくTTLくらい設けてみました。
src/test/resources/hazelcast-datamember.xml

<?xml version="1.0" encoding="UTF-8"?>
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.6.xsd"
           xmlns="http://www.hazelcast.com/schema/config"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <map name="default">
        <time-to-live-seconds>3</time-to-live-seconds>
    </map>
</hazelcast>

で、テストコード。クラスタ内のMember数は、Lite Member含めて3にしてみました。

    @Test
    public void preConfigurationedInstances() {
        withHazelcast("hazelcast-datamember.xml", 2, hasDataHazelcast -> {
            withHazelcast("hazelcast-litemember.xml", liteHazelcast -> {
                Map<String, String> map = liteHazelcast.getMap("default");
                map.put("key", "value");

                assertThat(map).containsExactly(MapEntry.entry("key", "value"));

                try {
                    TimeUnit.SECONDS.sleep(5L);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                assertThat(map).isEmpty();

                assertThat(liteHazelcast.getCluster().getMembers())
                        .hasSize(3);
                assertThat(liteHazelcast.getConfig().isLiteMember()).isTrue();
                Set<Partition> partitions = liteHazelcast.getPartitionService().getPartitions();
                assertThat(partitions).hasSize(271);
                assertThat(partitions.stream().map(p -> p.getOwner()).distinct().count()).isEqualTo(2);
            });
        });
    }

Lite Member越しに取得したDistributed Mapにデータを保存したり、TTLが効いていることが確認できます。

その一方で、クラスタの参加数は3ですが、データのオーナーは2であり、Lite Memberは引かれた数となっています。

クラスタ構成時の表示も、2つ分しか見えません。

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

まとめ

Hazelcast 3.6で追加された、Lite Memberを試してみました。

クラスタには参加したいけれど、データは持ちたくないMemberとして振る舞いたいというケースで利用するもののようです。

Listenerや特定の処理をキックするフロントになるようなMemberとして利用されたり、Client/Server構成ではできないような機能を利用する場合などに使われたりするのではないでしょうか。

なお、この機能に気付いたきっかけは、Payara 4.1.1.162だったりします。
What's New in Payara Server 162?

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