CLOVER🍀

That was when it all began.

Infinispan Server 13.0でCluster Health APIを確認する

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

Infinispan Serverの、ヘルスチェックAPIを確認してみたいなと思いまして。

もっと言うと、クラスター内のノードが停止してデータのリバランスが発生した場合、現在リバランス中なのか?
もう安定したのか?を知る方法は?というのが動機です。

Cluster Health API

Infinispan ServerのCluster Health APIについては、こちらに記載があります。

Infinispan Server Guide / Retrieving Health Statistics

Serverのドキュメントと言いつつ、EmbeddedCacheManagerの情報も書かれていますが…。

Infinispanのクラスターのヘルスチェックを行う方法としては、以下の3種類があります。

今回は、REST APIによる確認を中心に行います。

Using the Infinispan REST Server / Getting Cluster Health

Infinispan ServerのCluster Health APIを呼び出すには、以下のパスにアクセスします。

http://[Infinispan Serverが動作しているホスト]:11222/rest/v2/cache-managers/{cacheManagerName}/health

{cacheManagerName}というのは、cache-containerの名前です。

デフォルトのinfinispan.xmlであれば、以下のように定義されていると思いますので

   <cache-container name="default" statistics="true">

この場合、defaultのcache-containerのCluster Health APIにアクセスするためには、以下のURLにアクセスすることに
なります。

http://[Infinispan Serverが動作しているホスト]:11222/rest/v2/cache-managers/default/health

ここで取得できる情報としては、クラスターレベルのHealthとキャッシュレベルのHealthの2種類があります。
それぞれ、以下の情報が確認できます。

Healthステータスには、以下の4種類があります。

  • HEALTHY … 正常
  • HEALTHY_REBALANCING … キャッシュは正常ではあるが、リバランス中
  • DEGRADED … DEGRADED modeになっている
  • FAILED … なんらかのエラーのため、キャッシュが開始できなかった

クラスターレベルのHealthステータスは、cache-container内のキャッシュにひとつでもHEALTHY以外のものがあった場合、
その中の最初に見つかったステータスを返します。

ステータスがHEALTHY_REBALANCINGの場合、まだデータのリバランスが終わっていないので安定していない状態、
ということになります。このような場合は、HEALTHYになるまで待つことになるでしょう。

ちなみにDEGRADED modeというのは、Split Brainを起こした後に読み書きが制限された状態のことを指します。

Configuring Infinispan Caches / Split brain

また、Healthに関する情報は、メトリクスからは取得できません。クラスターを構成するノード数のみが取得可能です。

Infinispan Server Guide / Enabling and configuring Infinispan statistics and JMX monitoring

では、説明はこれくらいにして確認していってみましょう。今回は、安定したクラスターの状態と、ノードを失って
リバランス中の状態を確認しようと思います。

なお、JMXを使った確認は行いません。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-91-generic", arch: "amd64", family: "unix"

Infinispan Serverは、13.0.5.Finalを使用します。クラスターは3ノード構成として、各ノードのIPアドレスは172.19.0.2〜4の
範囲とします。

準備

まずはInfinispan Serverの準備をします。

infinispan.xmlは、エンドポイントの認証設定のみ行います。

      <endpoints>
         <endpoint socket-binding="default" security-realm="default">
            <hotrod-connector>
              <authentication>
                 <sasl mechanisms="SCRAM-SHA-512 SCRAM-SHA-384 SCRAM-SHA-256
                                   SCRAM-SHA-1 DIGEST-SHA-512 DIGEST-SHA-384
                                   DIGEST-SHA-256 DIGEST-SHA DIGEST-MD5 PLAIN"
                       server-name="infinispan"
                       qop="auth"/>
              </authentication>
            </hotrod-connector>
            <rest-connector>
              <authentication mechanisms="DIGEST BASIC"/>
            </rest-connector>
         </endpoint>
      </endpoints>

少し前まで、endpointがendpointsの子要素になることはドキュメントに書かれていなかったのですが、改善された
みたいです。

Infinispan Server Guide / Configuring Endpoint Authentication Mechanisms

各サーバーには、管理用のユーザーとアプリケーション用のユーザーを作成しておきます。

$ bin/cli.sh user create -g admin -p password admin-user
$ bin/cli.sh user create -g application -p password app-user

起動。

$ bin/server.sh \
    -b 0.0.0.0 \
    -Djgroups.tcp.address=`hostname -i`

これで、Infinispan Serverの準備はおしまいです。

キャッシュは、プログラムで作成することにします。

Distributed Cacheを作成して、データを登録する

では、キャッシュを作成してデータを登録することにします。

Maven依存関係等は、こちら。

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>13.0.5.Final</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>13.0.5.Final</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.21.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

Hot Rod Clientでキャッシュ作成とデータ登録を行います。

テストコードはこちら。名前の割には、このテストコード内でCluster Health APIを呼び出しているわけではないのですが…。

src/test/java/org/littlewings/infinispan/remote/health/ClusterHealthApiTest.java

package org.littlewings.infinispan.remote.health;

import java.time.LocalDateTime;
import java.util.function.Consumer;
import java.util.stream.IntStream;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

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

public class ClusterHealthApiTest {
    static String createUri(String userName, String password) {
        return String.format("hotrod://%s:%s@172.19.0.2:11222,172.19.0.3:11222,172.19.0.4:11222", userName, password);
    }

    @BeforeAll
    public static void setupAll() {
        String uri = createUri("admin-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCacheManagerAdmin admin = manager.administration();

            org.infinispan.configuration.cache.Configuration distCacheConfiguration =
                    new org.infinispan.configuration.cache.ConfigurationBuilder()
                            .clustering()
                            .cacheMode(org.infinispan.configuration.cache.CacheMode.DIST_SYNC)
                            .encoding().key().mediaType("application/x-protostream")
                            .encoding().value().mediaType("application/x-protostream")
                            .build();

            admin.getOrCreateCache("distCache", distCacheConfiguration);
        }
    }

    <K, V> void withCache(String cacheName, Consumer<RemoteCache<K, V>> func) {
        String uri = createUri("app-user", "password");

        try (RemoteCacheManager manager = new RemoteCacheManager(uri)) {
            RemoteCache<K, V> cache = manager.getCache(cacheName);

            func.accept(cache);
        }
    }

    @Test
    public void putsData() {
        this.<String, String>withCache("distCache", cache -> {
            int targetEntries = 1000000;

            IntStream
                    .rangeClosed(1, targetEntries)
                    .parallel()
                    .peek(i -> {
                        if (i % 100000 == 0) {
                            System.out.printf("[%s] putted, %d entries%n", LocalDateTime.now(), i);
                        }
                    })
                    .forEach(i -> cache.put("key" + i, "value" + i));

            assertThat(cache.size()).isEqualTo(targetEntries);
        });
    }
}

とりあえず、Distributed Cacheを作成してデータを10万件登録してみました。

Cluster Health APIを使ってみる

では、この状態でCluster Health APIを呼び出してみます。

こんな結果になりました。

$ curl -s -u app-user:password 172.19.0.2:11222/rest/v2/cache-managers/default/health | jq
{
  "cluster_health": {
    "cluster_name": "cluster",
    "health_status": "HEALTHY",
    "number_of_nodes": 3,
    "node_names": [
      "infinispan-server-60394",
      "infinispan-server-61394",
      "infinispan-server-7187"
    ]
  },
  "cache_health": [
    {
      "status": "HEALTHY",
      "cache_name": "___protobuf_metadata"
    },
    {
      "status": "HEALTHY",
      "cache_name": "___script_cache"
    },
    {
      "status": "HEALTHY",
      "cache_name": "distCache"
    }
  ]
}

クラスターは3ノードで、ステータスはすべてHEALTHYなので、安定した状態です。

ここで、172.19.0.4のノードを停止してみます(killで止めてもいいですが)。

Infinispan Server Guide / Shutting down Infinispan Server

$ bin/cli.sh -c http://admin-user:password@172.19.0.4:11222
[infinispan-server-7187@cluster//containers/default]> shutdown server

停止して本当にすぐはまだ状態は変わりませんが

$ curl -s -u app-user:password 172.19.0.2:11222/rest/v2/cache-managers/default/health | jq
{
  "cluster_health": {
    "cluster_name": "cluster",
    "health_status": "HEALTHY",
    "number_of_nodes": 3,
    "node_names": [
      "infinispan-server-60394",
      "infinispan-server-61394",
      "infinispan-server-7187"
    ]
  },
  "cache_health": [
    {
      "status": "HEALTHY",
      "cache_name": "___protobuf_metadata"
    },
    {
      "status": "HEALTHY",
      "cache_name": "___script_cache"
    },
    {
      "status": "HEALTHY",
      "cache_name": "distCache"
    }
  ]
}

クラスター内のノードが失われたことが検出されると

2021-12-22 14:42:54,773 INFO  (jgroups-36,infinispan-server-28362) [org.infinispan.CLUSTER] [Context=___hotRodTopologyCache_hotrod-d
efault]ISPN100008: Updating cache members list [infinispan-server-28362, infinispan-server-31674], topology id 10

リバランスが始まります。

2021-12-22 14:59:30,214 INFO  (jgroups-7,infinispan-server-60394) [org.infinispan.CLUSTER] [Context=distCache]ISPN100008: Updating cache members list [infinispan-server-60394, infinispan-server-61394], topology id 10
2021-12-22 14:59:30,216 INFO  (jgroups-7,infinispan-server-60394) [org.infinispan.CLUSTER] [Context=distCache]ISPN100002: Starting rebalance with members [infinispan-server-60394, infinispan-server-61394], phase READ_OLD_WRITE_ALL, topology id 11

今回は作成したDistributed Cacheに絞ってログ表示していますが、これ以外にもリバランスの情報は表示されます。

Cluster Health APIでの確認を繰り返していると、ステータスがHEALTHY_REBALANCINGに変化します。

$ curl -s -u app-user:password 172.19.0.2:11222/rest/v2/cache-managers/default/health | jq
{
  "cluster_health": {
    "cluster_name": "cluster",
    "health_status": "HEALTHY_REBALANCING",
    "number_of_nodes": 3,
    "node_names": [
      "infinispan-server-60394",
      "infinispan-server-61394",
      "infinispan-server-7187"
    ]
  },
  "cache_health": [
    {
      "status": "HEALTHY",
      "cache_name": "___protobuf_metadata"
    },
    {
      "status": "HEALTHY_REBALANCING",
      "cache_name": "___script_cache"
    },
    {
      "status": "HEALTHY_REBALANCING",
      "cache_name": "distCache"
    }
  ]
}

作成したDistributed CacheがHEALTHY_REBALANCINGですね。また、クラスター全体もHEALTHY_REBALANCINGです。

さらにノードもなくなり、2ノードでクラスターが構成されていることになります。

$ curl -s -u app-user:password 172.19.0.2:11222/rest/v2/cache-managers/default/health | jq
{
  "cluster_health": {
    "cluster_name": "cluster",
    "health_status": "HEALTHY_REBALANCING",
    "number_of_nodes": 2,
    "node_names": [
      "infinispan-server-60394",
      "infinispan-server-61394"
    ]
  },
  "cache_health": [
    {
      "status": "HEALTHY",
      "cache_name": "___protobuf_metadata"
    },
    {
      "status": "HEALTHY",
      "cache_name": "___script_cache"
    },
    {
      "status": "HEALTHY_REBALANCING",
      "cache_name": "distCache"
    }
  ]
}

さらに待っていると、安定してHEALTHYになります。

$ curl -s -u app-user:password 172.19.0.2:11222/rest/v2/cache-managers/default/health | jq
{
  "cluster_health": {
    "cluster_name": "cluster",
    "health_status": "HEALTHY",
    "number_of_nodes": 2,
    "node_names": [
      "infinispan-server-60394",
      "infinispan-server-61394"
    ]
  },
  "cache_health": [
    {
      "status": "HEALTHY",
      "cache_name": "___protobuf_metadata"
    },
    {
      "status": "HEALTHY",
      "cache_name": "___script_cache"
    },
    {
      "status": "HEALTHY",
      "cache_name": "distCache"
    }
  ]
}

リバランスが終了したことも、ログ出力されています。

2021-12-22 14:59:33,613 INFO  (jgroups-41,infinispan-server-60394) [org.infinispan.CLUSTER] [Context=distCache]ISPN100010: Finished rebalance with members [infinispan-server-60394, infinispan-server-61394], topology id 14

これで、Infinispan ServerでのCluster Health APIの主要な確認は終わりですね。

Embedded Cacheの場合は?

軽くですが、Embedded Cacheの場合も見ておきましょう。こんな感じでの確認になります。

src/test/java/org/littlewings/infinispan/remote/health/EmbeddedCacheManagerHealthTest.java

package org.littlewings.infinispan.remote.health;

import java.io.IOException;
import java.util.Comparator;
import java.util.List;

import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.health.CacheHealth;
import org.infinispan.health.ClusterHealth;
import org.infinispan.health.Health;
import org.infinispan.health.HealthStatus;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.junit.jupiter.api.Test;

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

public class EmbeddedCacheManagerHealthTest {
    @Test
    public void health() throws IOException {
        GlobalConfiguration globalConfiguration =
                new GlobalConfigurationBuilder().clusteredDefault().transport().defaultTransport().build();
        try (EmbeddedCacheManager cacheManager = new DefaultCacheManager(globalConfiguration)) {
            cacheManager
                    .defineConfiguration(
                            "myCache",
                            new ConfigurationBuilder().clustering().cacheMode(CacheMode.DIST_SYNC).build()
                    );
            cacheManager
                    .defineConfiguration(
                            "myCache2",
                            new ConfigurationBuilder().clustering().cacheMode(CacheMode.DIST_SYNC).build()
                    );

            Health health = cacheManager.getHealth();
            ClusterHealth clusterHealth = health.getClusterHealth();
            assertThat(clusterHealth.getHealthStatus()).isEqualTo(HealthStatus.HEALTHY);
            assertThat(clusterHealth.getNumberOfNodes()).isEqualTo(1);

            List<CacheHealth> cacheHealths =
                    health.getCacheHealth().stream().sorted(Comparator.comparing(c -> c.getCacheName())).toList();
            assertThat(cacheHealths).hasSize(2);
            assertThat(cacheHealths.get(0).getCacheName()).isEqualTo("myCache");
            assertThat(cacheHealths.get(0).getStatus()).isEqualTo(HealthStatus.HEALTHY);
            assertThat(cacheHealths.get(1).getCacheName()).isEqualTo("myCache2");
            assertThat(cacheHealths.get(1).getStatus()).isEqualTo(HealthStatus.HEALTHY);
        }
    }
}

クラスターレベルのヘルスチェックはこのように行い、

            Health health = cacheManager.getHealth();
            ClusterHealth clusterHealth = health.getClusterHealth();
            assertThat(clusterHealth.getHealthStatus()).isEqualTo(HealthStatus.HEALTHY);
            assertThat(clusterHealth.getNumberOfNodes()).isEqualTo(1);

キャッシュレベルのヘルスチェックは、このようにして行えます。

            List<CacheHealth> cacheHealths =
                    health.getCacheHealth().stream().sorted(Comparator.comparing(c -> c.getCacheName())).toList();
            assertThat(cacheHealths).hasSize(2);
            assertThat(cacheHealths.get(0).getCacheName()).isEqualTo("myCache");
            assertThat(cacheHealths.get(0).getStatus()).isEqualTo(HealthStatus.HEALTHY);
            assertThat(cacheHealths.get(1).getCacheName()).isEqualTo("myCache2");
            assertThat(cacheHealths.get(1).getStatus()).isEqualTo(HealthStatus.HEALTHY);

先ほどまで行っていた、Infinispan ServerのCluster Health APIで取得していた情報と同じですね。

HEALTHY_REBALANCINGは、どのような状態?

HEALTHY_REBALANCINGというステータスが気になるので、少しソースコードを追ってみましょう。

まず、おさらい的にクラスター全体のステータスは、cache-containerが保持するキャッシュのうち、ひとつでも
HEALTHY以外のものがあればそれが反映されます。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/health/impl/ClusterHealthImpl.java#L29-L37

キャッシュはどうかというと、こちら。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/health/impl/CacheHealthImpl.java#L22-L32

HEALTHY_REBALANCINGステータスは、DistributionManager#isRehashInProgressがtrueを返す時になります。

この状態は、CacheTopology#getPendingCHがnullではない時、となっています。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/distribution/impl/DistributionManagerImpl.java#L100-L103

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/CacheTopology.java#L83-L85

CacheTopologyが持つCH(ConsistentHash)というのは、キーと所有者のマッピングを定義したものです。

Configuring Infinispan Caches / Key ownership

ConsistentHash (Infinispan JavaDoc 13.0.5.Final API)

CacheTopologyが持つCHには現在(current)とpendingがあります。Pending状態のConsistentHashというのは、
未来になるべきConsistentHashの状態のことを指します。

そして、CacheTopology#getPendingCHがnullではない時にHEALTHY_REBALANCINGステータスというでした。
なので、CacheTopology#getPendingCHがnullではない時というのは、クラスター内のノードに増減などがあって 再調整した結果、算出されたConsistentHashの状態になれていないことを表します。

リバランス開始の指示したりするのは、このあたりですね。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterCacheStatus.java#L922-L947

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterTopologyManagerImpl.java#L536-L539

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterTopologyManagerImpl.java#L634-L638

安定すると、このあたりが呼び出されるようです。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterCacheStatus.java#L405-L455

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterCacheStatus.java#L913-L921

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterCacheStatus.java#L832-L836

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/topology/ClusterTopologyManagerImpl.java#L640-L643

ちなみに、こう書いているとリバランスはDistributed Cacheのみの話に見えなくもないですが、リバランスがないのは
Local CacheとInvalidation Cacheだけみたいですね。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/core/src/main/java/org/infinispan/statetransfer/RebalanceType.java#L26-L42

Replicated Cacheに関しては、pendingという状態がちょっとイメージできていないのですが…今回はここは
パスしておきます…。

最後に、REST APIでアクセスしているCluster Health APIはこちらになります。

https://github.com/infinispan/infinispan/blob/13.0.5.Final/server/rest/src/main/java/org/infinispan/rest/resources/ContainerResource.java#L231-L255

中身は、EmbeddedCacheManagerで使っていたClusterHealth、CacheHealthの情報を扱っています。

まとめ

Infinispan Server、それからEmbedded CacheでCluster Health APIを試してみました。

データのリバランスが完了しているかどうかを確認する方法がわかったり、内部の様子も追えたので勉強になりました。

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

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-cluster-health-api