CLOVER🍀

That was when it all began.

Infinispan 9.4.7/10.0.0.Beta1で追加された、Zero Capacity Nodeを試す

これは、なにをしたくて書いたもの?

  • Infinispan 9.4.7.Final/10.0.0.Beta1でZero Capacity Nodeというものが追加されたらしいので、試してみようと

Zero Capacity Node?

Infinispan 9.4.8.Final/10.0.0.Beta2のリリース時のブログを見て、おや、と思ったものがありまして。

[ISPN-9699] - Cluster member owning no data

Infinispan: Infinispan 10.0.0.Beta2 and 9.4.8

[ISPN-9699] Cluster member owning no data - JBoss Issue Tracker

Infinispanには、以前からcapacity factorという各Nodeに対するデータ配置の割合をコントロールできるパラメーターがありました。

Capacity Factors

こちらでNodeごとにデータの持つ割合を調整できるのですが、指定できるのがDistributed Cache、かつCache単位でした。

今回は、CacheContainerごと指定するようです。つまり、データを持たないNodeが作れそうな雰囲気を感じます。

で、Zero Capacity Nodeの記述はブログの10.0.0.Beta2の内容に書かれていたのですが、以下のページをよくよく見ると
Infinispan 9.4.7.Finalにも入れられているようです。

[ISPN-9699] Cluster member owning no data - JBoss Issue Tracker

ということは、Infinispan 9.4でも使えるということになるので、どのようなものなのか確認しようと今回試してみることにしました。

環境

今回の環境は、こちらです。

$ java -version
openjdk version "1.8.0_191"
OpenJDK Runtime Environment (build 1.8.0_191-8u191-b12-2ubuntu0.18.04.1-b12)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)


$ mvn -version
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T03:41:47+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_191, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-45-generic", arch: "amd64", family: "unix"

Infinispanは、9.4.8.Finalを使用します。

お題

Local Cache、Distributed Cache、Replicated Cacheの3つのCacheを用意し、かつ単純に浮いているだけの
CacheServerを2つ、Zero Capacity Nodeを有効にしたひとつのNodeを使い、Zero Capacity Nodeの効果を見てみます。
※このお題には、オチがあります

Infinispanの設定

Infinispanの設定は、以下のようにしました。
src/main/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:infinispan:config:9.4 http://www.infinispan.org/schemas/infinispan-config-9.4.xsd"
        xmlns="urn:infinispan:config:9.4">

   <jgroups>
      <stack-file name="udp" path="default-configs/default-jgroups-udp.xml"/>
   </jgroups>

   <cache-container zero-capacity-node="${enabled.zero.capacity.node:false}">
      <local-cache name="localCache"/>

      <distributed-cache name="distributedCache"/>

      <replicated-cache name="replicatedCache"/>
   </cache-container>
</infinispan>

ポイントは、この部分。

   <cache-container zero-capacity-node="${enabled.zero.capacity.node:false}">

システムプロパティ「enabled.zero.capacity.node」を指定したらその値を使い、なければfalseとします。

なお、「zero-capacity-node」のデフォルト値はfalseです。Zero Capacity Nodeを有効にする場合だけ、このシステムプロパティを
使うことにします。

サンプルプログラム

では、プログラムを書いていきます。

最初に、ただ浮いてもらうだけのCacheServerを作成。
src/main/java/org/littlewings/infinispan/zerocapacity/EmbeddedCacheServer.java

package org.littlewings.infinispan.zerocapacity;

import java.io.IOException;

import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;

public class EmbeddedCacheServer {
    public static void main(String... args) throws IOException {
        EmbeddedCacheManager manager = new DefaultCacheManager("infinispan.xml");

        manager.getCache("localCache");
        manager.getCache("distributedCache");
        manager.getCache("replicatedCache");

        System.console().readLine("> Enter stop.");

        manager.stop();
    }
}

終了は、Enterで。

続いて、Zero Capacity Nodeとして動作するプログラム。
src/main/java/org/littlewings/infinispan/zerocapacity/Runner.java

package org.littlewings.infinispan.zerocapacity;

import java.io.IOException;
import java.util.stream.IntStream;

import org.infinispan.Cache;
import org.infinispan.distribution.DistributionInfo;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;

public class Runner {
    public static void main(String... args) throws IOException {
        System.setProperty("enabled.zero.capacity.node", "true");

        EmbeddedCacheManager manager = new DefaultCacheManager("infinispan.xml");

        String cacheName = args[0];
        String command = args[1];

        System.out.printf("self = %s, cache = %s, command = %s%n", manager.getAddress(), cacheName, command);

        Cache<String, String> cache = manager.getCache(cacheName);

        switch (command) {
            case "get":
                DistributionManager distributionManager = cache.getAdvancedCache().getDistributionManager();
                cache.forEach((key, value) -> {
                    DistributionInfo distributionInfo = distributionManager.getCacheTopology().getDistribution(key);

                    System.out.printf(
                            "key / value = [%s / %s], primary = %s, read-owners = %s, write-owners = %s%n",
                            key,
                            value,
                            distributionInfo.primary(),
                            distributionInfo.readOwners(),
                            distributionInfo.writeOwners()
                    );
                });

                break;
            case "put":
                IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));

                break;
            default:
                throw new IllegalArgumentException("Unknown command[" + command + "]");
        }

        cache.stop();
        manager.stop();

        System.out.println("done!!");
    }
}

まず、Zero Capacity Nodeを有効にします。

        System.setProperty("enabled.zero.capacity.node", "true");

プログラムは、Cacheの名前とコマンドを指定して起動する形にします。この時に引数の内容と、自Nodeの名前を出力しておきます。

        String cacheName = args[0];
        String command = args[1];

        System.out.printf("self = %s, cache = %s, command = %s%n", manager.getAddress(), cacheName, command);

        Cache<String, String> cache = manager.getCache(cacheName);

コマンドは、putでデータを100件登録、getでデータをコンソールに出力しつつ、データの配置状況も合わせて表示します。

        switch (command) {
            case "get":
                DistributionManager distributionManager = cache.getAdvancedCache().getDistributionManager();
                cache.forEach((key, value) -> {
                    DistributionInfo distributionInfo = distributionManager.getCacheTopology().getDistribution(key);

                    System.out.printf(
                            "key / value = [%s / %s], primary = %s, read-owners = %s, write-owners = %s%n",
                            key,
                            value,
                            distributionInfo.primary(),
                            distributionInfo.readOwners(),
                            distributionInfo.writeOwners()
                    );
                });

                break;
            case "put":
                IntStream.rangeClosed(1, 100).forEach(i -> cache.put("key" + i, "value" + i));

                break;
            default:
                throw new IllegalArgumentException("Unknown command[" + command + "]");
        }

これで、準備は完了です。

確認

では、確認してみましょう。

最初に、Distributed Cacheでput。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.infinispan.zerocapacity.Runner -Dexec.args='distributedCache put'

この状態だと、データを持つノードがいないためエラーになります。

2 24, 2019 12:29:31 午前 org.infinispan.topology.CacheTopologyControlCommand invokeAsync
WARN: ISPN000071: Caught exception when handling command CacheTopologyControlCommand{cache=distributedCache, type=JOIN, sender=xxxxx-36143, joinInfo=CacheJoinInfo{consistentHashFactory=org.infinispan.distribution.ch.impl.SyncConsistentHashFactory@ffffd8e9, hashFunction=MurmurHash3, numSegments=256, numOwners=2, timeout=240000, totalOrder=false, cacheMode=DIST_SYNC, persistentUUID=a5a8b8f5-7592-46e8-a124-df8160b087a4, persistentStateChecksum=Optional.empty}, topologyId=0, rebalanceId=0, currentCH=null, pendingCH=null, availabilityMode=null, phase=null, actualMembers=null, throwable=null, viewId=0}
java.lang.IllegalArgumentException: There must be at least one node with a non-zero capacity factor

WARNログは出力されていますが、起動に失敗しているわけではないので、データを持てるNodeを追加すると、そのまま
処理を続けられます。

というわけで、先に単独で浮いてもらうCache Serverを2つ用意しておきます。

## Cache Server 1
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.infinispan.zerocapacitymbeddedCacheServer


## Cache Server 2
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.infinispan.zerocapacitymbeddedCacheServer

今度は、実行に成功します。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.infinispan.zerocapacity.Runner -Dexec.args='distributedCache put'

データを取得してみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.infinispan.zerocapacity.Runner -Dexec.args='distributedCache get'

結果は、こんな感じに。

self = xxxxx-61699, cache = distributedCache, command = get
key / value = [key82 / value82], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key77 / value77], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key2 / value2], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key79 / value79], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key100 / value100], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key42 / value42], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key91 / value91], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key51 / value51], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key83 / value83], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]
key / value = [key15 / value15], primary = xxxxx-30095, read-owners = [xxxxx-30095, xxxxx-23560], write-owners = [xxxxx-30095, xxxxx-23560]

〜省略〜

最初に、自Node(Zero Capacity Nodeが有効なNode)の名前を出力していますが、データの配置を確認してみるとPrimaryにも
バックアップにもデータが配置されていないことがわかります。

データを持たないNodeとして、クラスタに参加できたようです。

なお、Replicated CacheとLocal Cacheですが実はZero Capacity Nodeが有効でも、特に動作に影響はありません。
※Zero Capacity Nodeを有効にしたNodeのみで実行しても、データを保存したりすることができます

なので、結果は省略するのですが、どうしてこうなるかという話。

ソースコードを見ると、割と簡単に判明します。

https://github.com/infinispan/infinispan/blob/9.4.8.Final/core/src/main/java/org/infinispan/statetransfer/StateTransferManagerImpl.java#L97

      float capacityFactor = globalConfiguration.isZeroCapacityNode() ? 0.0f : configuration.clustering().hash().capacityFactor();

要するに、Zero Capacity Nodeを有効にすると、Capacity Factorが常に0なCacheとして扱われるというわけです。

ですので、Distributed Cacheにしか効果がないわけですね。

現時点のInfinispanのドキュメントにはこのことは書かれていない(更新されたドキュメントがリリースされていない)ようですが、
リポジトリを見るとドキュメントの内容があったので、そちらを。

https://github.com/infinispan/infinispan/blob/9.4.8.Final/documentation/src/main/asciidoc/user_guide/clustering.adoc#zero-capacity-node

However, note that this will be true for distributed caches only. If you are using replicated caches, the node will still keep a copy of the value. Use only distributed caches to make the best use of this feature.

バッチリ書いてました!

また、Zero Capacity Nodeのみで動作させた時に、例外を出力させていたのはこのあたりです。やっぱり、Capacity Factorと
同じような場所ですね。

https://github.com/infinispan/infinispan/blob/9.4.8.Final/lock/src/main/java/org/infinispan/lock/impl/ClusteredLockModuleLifecycle.java#L91-L93

というわけで、Zero Capacity Nodeというのは、Distributed Cacheに対してCapacity Factorを一律0で指定する、簡易的な
方法だということがわかりました。

まとめ

Infinispan 9.4.7.Final/10.0.0.Beta1で追加された、Zero Capacity Nodeを試してみました。

ドキュメントが参照可能な形でなかったので、とりあえず動かして確認して、それからソースコードを確認したりしましたが、
挙動を見てからソースコードを見るとまあ納得かなぁと思います。

それで、9.4.7.Finalという途中のバージョンでも入ったのかなぁとも思いますし。

今回作成したソースコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-zero-capacity-node

ところで

Infinispan Serverで、Zero Capacity Nodeを指定するにはどうしたらいいんでしょう?

cache-containerでは項目が見当たらなかったような…まあ、Capacity Factorで指定する、でも困りはしないのでしょうけど。