CLOVER🍀

That was when it all began.

HazelcastのClusterSafe/ClusterState、バージョン間の互換性を確認する

先日書かれた、こちらの記事を見まして。

クラスタ化された Payara Server のアップグレード - notepad

ちょっと気になったのは、こちらの部分。

Case 1 : Shoal を使用していない場合
Payara は GlassFish から引き継いだ Shoal を使用しなくても、Hazelcast のみでクラスタリングが可能です。その場合はおそらく、すべてのノードが DAS となっているはずで、通常のアップグレードをすべてのノードに対して実施すればよいことになります。ノードの接続・切断は Hazelcast が自動的に行うため考慮する必要はありません。

http://www.coppermine.jp/docs/notepad/2017/12/how-to-update-clustered-payara-server.html

確かにそのとおりだと思うのですが、個人的にはもうちょっと気になるところがあります。

特に、このあたり。

  • Hazelcast Nodeを接続・切断してよいタイミング
  • Hazelcast自体のバージョン間の互換性

というわけで、この視点でHazelcast側を見ていこうかなぁと思います。

ClusterSafeとClusterStatus

Hazelcastは分散KVS的な側面を持っていますが、Nodeの追加、削除が起こるとデータのリバランシングが発生します。

特にNodeが失われると一時的にデータが一部なくなりますが、他のNodeから復元することが可能です。ただ、デフォルトではデータのバックアップ数は
1なので、2つのNodeが同時にダウンするとデータが欠損する可能性があります。

例えばPayaraの場合は

  • Distributed Map
  • Cache
  • Topic

あたりのデータ構造を使用していますが、MapとCacheについてはこの理屈が当てはまります。
※Topicについては分散配置ができないので、2つのNodeが同時にダウンするとすべてのデータを失う可能性があります

そんなわけなので、リバランスが終わって安全な状態になったかどうか、確認したりしたいですよね?

このあたりを見るのに、ClusterSafeがあります。

ClusterSafeについては、こちら。

Safety Checking Cluster Members

PartitionServiceが持つisClusterSafeメソッドでは、Hazelcastクラスタが安全な状態にあるかどうかをチェックします。データのリバランスがなく
(Partitionの移行が発生していない)、すべてのバックアップが各Partitionで同期されている場合にtrueを返します。

isMemberSafeやisLocalMemberは、指定したMember、もしくは自分自身が持つPartitionがプライマリーとの同期が済んでいればtrueを返します。
trueを返す場合は、該当のMemberをシャットダウンすることができます。

使用するAPIとしては、こちら。
PartitionService (Hazelcast Root 3.9.1 API)
PartitionServiceMBean (Hazelcast Root 3.9.1 API)

続いて、クラスタの状態を確認するものとしてClusterStateがあります。
Managing Cluster and Member States

ClusterStateでは、クラスタ操作の許可/制限の状態を確認することができます。例えば、ClusterStatusがACTIVEであれば特に制限はありませんが、FROZEMや
PASSIVEの場合はPartitionが固定され、新規Memberを受け付けなくなったりします。

ClusterStateに関するAPIは、こちら。
Cluster (Hazelcast Root 3.9.1 API)
ClusterState (Hazelcast Root 3.9.1 API)

また、API以外にも、JMXやHealth Check(REST API)でも確認することができます。

Monitoring with JMX
Health Check and Monitoring

JMXは「hazelcast.jmx」を「true」に、Health Checkは「hazelcast.http.healthcheck.enabled」を「true」にすることで、それぞれ有効化することができます。
システムプロパティか、Hazelcastの設定ファイル/Configのproperties/propertyで設定します。

Hazelcastのバージョン間の互換性は?

HazelcastのRolling Upgrade(クラスタ内のMemberを順次アップグレードしていくこと)について、ドキュメントに以下の記載があります。

Rolling Member Upgrades

ところでこれ、Enterprise版のHazelcastの話です。

ここで、まずHazelcastのバージョンの定義を見てみます。

・Minor version: A version change after the decimal point, e.g., 3.8 and 3.9.
・Patch version: A version change after the second decimal point, e.g., 3.8.1 and 3.8.2.
・Member codebase version: The major.minor.patch version of the Hazelcast binary on which the member executes. For example, when running on hazelcast-3.8.jar, your member's codebase version is 3.8.0.
・Cluster version: The major.minor version at which the cluster operates. This ensures that cluster members are able to communicate using the same cluster protocol and determines the feature set exposed by the cluster.

http://docs.hazelcast.org/docs/3.9.1/manual/html-single/index.html#terminology

Member codebase versionが「major.minor.patch」バージョンを、Cluster versionが「major.minor」バージョンを指すみたいですね。

で、ここを読むとHazelcastはCluster version間で互換性がないことがわかります。

Hazelcast Members Compatibility Guarantees

Enterprise版のHazelcast 3.8以降であれば、ひとつ前のMinor Versionからの互換性があるようです(Hazelcast IMDG Enterprise 3.8から3.9へはRolling Upgradeが可能)。
ポイントかと思いますので、押さえておきましょう。

試してみる

と、ドキュメントベースに説明だけだとなんなので、試してみましょう。

簡単にGroovyスクリプトで、次のことを試してみたいと思います。

用意したスクリプトは、こんな感じ。
hazelcast391-runner.groovy

@Grab('com.hazelcast:hazelcast:3.9.1')
import com.hazelcast.core.Hazelcast

import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.IntStream
import java.time.LocalDateTime

System.setProperty('hazelcast.jmx', 'true')
System.setProperty('hazelcast.http.healthcheck.enabled', 'true')

def log = { message ->
  println("[${LocalDateTime.now()}] ${message}")
}

log('starting Hazelcast Instance...')
def hazelcast = Hazelcast.newHazelcastInstance()

def running = new AtomicBoolean(true)

new Thread({ -> 
  while (running.get()) {
    def clusterState = hazelcast.cluster.clusterState
    def partitionService = hazelcast.partitionService

    log("Cluster#clusterState = ${clusterState}")
    log("PartitionService#clusterSafe = ${partitionService.clusterSafe}")

    // Reflection!!
    log("InternalPartitionServicem#migrationQueueSize = ${hazelcast.original.node.partitionService.migrationQueueSize}")

    TimeUnit.SECONDS.sleep(1L)
  }
}).start()

log('Hazelcast Instance started.')

System.console().readLine('> Enter input data')

def map = hazelcast.getMap('default')
IntStream.rangeClosed(1, 100000).forEach { i -> map.put("${hazelcast.cluster.localMember.uuid}-key${i}", "value${i}") }

log("putted data, size = ${map.size()}")

System.console().readLine('> Enter stop Hazelcast Instance')
running.set(false)

hazelcast.shutdown()

log('Hazelcast Instance shutdown.')

この構成を基本にして、あとでGrapeで指定したバージョンを変えたスクリプトを作成していきます。

@Grab('com.hazelcast:hazelcast:3.9.1')
import com.hazelcast.core.Hazelcast

JMXとHealth Checkは、今回はシステムプロパティで有効化しました。

System.setProperty('hazelcast.jmx', 'true')
System.setProperty('hazelcast.http.healthcheck.enabled', 'true')

ところどころEnterを押すと進んでいくのですが、途中でClusterStateやClusterSafeなどを観測したり、データを登録したりする処理を付けたり
しています。

JMX & Health Check

とりあえず、この状態で起動してみましょう。

$ groovy hazelcast391-runner.groovy

JMXの方は、起動時に有効化されていることがログ出力されます。

12 15, 2017 11:41:58 午後 com.hazelcast.internal.jmx.ManagementService
情報: [172.20.0.1]:5701 [dev] [3.9.1] Hazelcast JMX agent enabled.

MBeanの情報を、JConsoleで見てみます。

PartitionServiceMBeanのAttributeを見ると、ClusterSafeや自分自身の状態を確認できます。

続いて、Health Checkを確認してみましょう。Health Checkは、「http://[IPアドレス]:[ポート]/hazelcast/health」で確認することができます。

$ curl http://localhost:5701/hazelcast/health
Hazelcast::NodeState=ACTIVE
Hazelcast::ClusterState=ACTIVE
Hazelcast::ClusterSafe=TRUE
Hazelcast::MigrationQueueSize=0
Hazelcast::ClusterSize=1

表示できましたね。

その他、cluster.shで確認する方法やREST APIで確認する方法もあるようですが、今回は割愛。
Using the Script cluster.sh

Using REST API for Cluster Management

Enterprise版だと、Management Centerがもっと豪華に…。
Clustered REST via Management Center

とりあえず、JMXとHealth Checkはこんなところで。

クラスタ内のMemberを増減させてみる

用意したスクリプトには、クラスタの状態を出力し続ける処理と

new Thread({ -> 
  while (running.get()) {
    def clusterState = hazelcast.cluster.clusterState
    def partitionService = hazelcast.partitionService

    log("Cluster#clusterState = ${clusterState}")
    log("PartitionService#clusterSafe = ${partitionService.clusterSafe}")

    // Reflection!!
    log("InternalPartitionServicem#migrationQueueSize = ${hazelcast.original.node.partitionService.migrationQueueSize}")

    TimeUnit.SECONDS.sleep(1L)
  }
}).start()

10万件ほどデータを放り込む処理を付けてあります。

def map = hazelcast.getMap('default')
IntStream.rangeClosed(1, 100000).forEach { i -> map.put("${hazelcast.cluster.localMember.uuid}-key${i}", "value${i}") }

log("putted data, size = ${map.size()}")

クラスタの状態確認は、それぞれClusterおよびPartitionServiceからClusterStateとClusterSafeを確認しています。

    def clusterState = hazelcast.cluster.clusterState
    def partitionService = hazelcast.partitionService

    log("Cluster#clusterState = ${clusterState}")
    log("PartitionService#clusterSafe = ${partitionService.clusterSafe}")

あと、ちょっと強引にアクセスしているのがMigrationQueueで、これでMemberの増減に伴うデータのマイグレーション(リバランシング)の状態を確認できます。

    // Reflection!!
    log("InternalPartitionServicem#migrationQueueSize = ${hazelcast.original.node.partitionService.migrationQueueSize}")

これはMember単位のサイズになります。

実は、先ほどのHealth Check時にも出力されています。

$ curl http://localhost:5701/hazelcast/health
Hazelcast::NodeState=ACTIVE
Hazelcast::ClusterState=ACTIVE
Hazelcast::ClusterSafe=TRUE
Hazelcast::MigrationQueueSize=0
Hazelcast::ClusterSize=1

これをちょっとズルして取得している感じです。

では、まずはひとつ起動。

$ groovy hazelcast391-runner.groovy

データを放り込みます。

[2017-12-16T00:03:51.329] putted data, size = 100000

ちなみに、この時はこんな感じでクラスタの状態が出力されます。

[2017-12-16T00:04:33.622] Cluster#clusterState = ACTIVE
[2017-12-16T00:04:33.623] PartitionService#clusterSafe = true
[2017-12-16T00:04:33.624] InternalPartitionServicem#migrationQueueSize = 0
[2017-12-16T00:04:34.624] Cluster#clusterState = ACTIVE
[2017-12-16T00:04:34.626] PartitionService#clusterSafe = true
[2017-12-16T00:04:34.626] InternalPartitionServicem#migrationQueueSize = 0
[2017-12-16T00:04:35.627] Cluster#clusterState = ACTIVE
[2017-12-16T00:04:35.629] PartitionService#clusterSafe = true
[2017-12-16T00:04:35.630] InternalPartitionServicem#migrationQueueSize = 0

ここで1 Nodeなので、次のNodeを起動してみましょう。

$ groovy hazelcast391-runner.groovy

クラスタにMemberが参加するとともに、ClusterSafeがfalseとなり、MigrationQueueのサイズも増えます。

12 16, 2017 12:05:20 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.9.1] 

Members {size:2, ver:2} [
	Member [172.20.0.1]:5701 - 87b87aa5-4da8-40e9-924e-adb661d81b38 this
	Member [172.20.0.1]:5702 - c957669a-27c4-4483-917c-8d37afc54a6e
]

12 16, 2017 12:05:20 午前 com.hazelcast.internal.partition.impl.MigrationManager
情報: [172.20.0.1]:5701 [dev] [3.9.1] Re-partitioning cluster data... Migration queue size: 271
[2017-12-16T00:05:20.749] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:20.753] PartitionService#clusterSafe = false
[2017-12-16T00:05:20.753] InternalPartitionServicem#migrationQueueSize = 253
[2017-12-16T00:05:21.754] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:21.756] PartitionService#clusterSafe = false
[2017-12-16T00:05:21.757] InternalPartitionServicem#migrationQueueSize = 134
[2017-12-16T00:05:22.758] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:23.061] PartitionService#clusterSafe = false
[2017-12-16T00:05:23.063] InternalPartitionServicem#migrationQueueSize = 34
[2017-12-16T00:05:24.064] Cluster#clusterState = ACTIVE
[2017-12-16T00:05:24.079] PartitionService#clusterSafe = true
[2017-12-16T00:05:24.080] InternalPartitionServicem#migrationQueueSize = 0
12 16, 2017 12:05:24 午前 com.hazelcast.internal.partition.impl.MigrationThread
情報: [172.20.0.1]:5701 [dev] [3.9.1] All migration tasks have been completed, queues are empty.

最後に、MigrationQueueのサイズが0になるとClusterSafeがtrueとなるようです。
※このあたりは、あとでもう少し

3つ目のNodeを足しても同じ。

12 16, 2017 12:07:32 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.9.1] 

Members {size:3, ver:3} [
	Member [172.20.0.1]:5701 - 87b87aa5-4da8-40e9-924e-adb661d81b38 this
	Member [172.20.0.1]:5702 - c957669a-27c4-4483-917c-8d37afc54a6e
	Member [172.20.0.1]:5703 - 967fff3e-94e7-4b40-8c97-604bdb4a00ec
]

[2017-12-16T00:07:32.719] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:32.722] PartitionService#clusterSafe = false
[2017-12-16T00:07:32.723] InternalPartitionServicem#migrationQueueSize = 1
12 16, 2017 12:07:32 午前 com.hazelcast.internal.partition.impl.MigrationManager
情報: [172.20.0.1]:5701 [dev] [3.9.1] Re-partitioning cluster data... Migration queue size: 271
[2017-12-16T00:07:33.724] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:33.725] PartitionService#clusterSafe = false
[2017-12-16T00:07:33.726] InternalPartitionServicem#migrationQueueSize = 228
[2017-12-16T00:07:34.727] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:34.992] PartitionService#clusterSafe = false
[2017-12-16T00:07:34.993] InternalPartitionServicem#migrationQueueSize = 101
[2017-12-16T00:07:35.995] Cluster#clusterState = ACTIVE
[2017-12-16T00:07:36.065] PartitionService#clusterSafe = true
[2017-12-16T00:07:36.066] InternalPartitionServicem#migrationQueueSize = 0
12 16, 2017 12:07:36 午前 com.hazelcast.internal.partition.impl.MigrationThread
情報: [172.20.0.1]:5701 [dev] [3.9.1] All migration tasks have been completed, queues are empty.

なお、念のためですが、データを追加したりしても別にデータのリバランシングが発生するとかいうわけではありませんので。

異なるバージョンのHazelcastでクラスタを構成してみる

最後に、異なるバージョンのHazelcastを使用してクラスタを構成してみましょう。

最初に、Hazelcast 3.9.1(現時点での最新)のNodeを起動してみます。

$ groovy hazelcast391-runner.groovy

ここで、Grapeでのバージョン指定だけ変えたスクリプトを用意してみます。
hazelcast39-runner.groovy

@Grab('com.hazelcast:hazelcast:3.9')
import com.hazelcast.core.Hazelcast

起動。

$ groovy hazelcast39-runner.groovy

無事、クラスタが構成されました。

12 16, 2017 12:12:37 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.9.1] 

Members {size:2, ver:2} [
	Member [172.20.0.1]:5701 - 690a44e3-11b5-4728-93fb-2a1cbf76562a this
	Member [172.20.0.1]:5702 - 6233fe6c-f3e9-4b32-9efd-46a57a80f564
]

それでは、今度は3.9.1と3.8系の最新である3.8.8で試してみましょう。
hazelcast388-runner.groovy

@Grab('com.hazelcast:hazelcast:3.8.8')
import com.hazelcast.core.Hazelcast

起動。先に3.9.1の方を起動してみました。

## 3.9.1
$ groovy hazelcast391-runner.groovy

## 3.8.8
$ groovy hazelcast388-runner.groovy

すると、3.9.1側にはこういうログが出力され、クラスタに入ることを拒否されます。

12 16, 2017 12:15:09 午前 com.hazelcast.internal.cluster.impl.ClusterJoinManager
警告: [172.20.0.1]:5701 [dev] [3.9.1] Joining node's version 3.8.8 is not compatible with cluster version 3.9 (Rolling Member Upgrades are only supported for the next minor version) (Rolling Member Upgrades are only supported in Hazelcast Enterprise)
12 16, 2017 12:15:09 午前 com.hazelcast.nio.tcp.TcpIpConnection
情報: [172.20.0.1]:5701 [dev] [3.9.1] Connection[id=1, /172.20.0.1:5701->/192.168.254.128:34082, endpoint=[172.20.0.1]:5702, alive=false, type=MEMBER] closed. Reason: Connection closed by the other side

互換性ないよ、Rolling UpgradeしたかったらHazelcast Enterprise使ってね、って言われてますね。

警告: [172.20.0.1]:5701 [dev] [3.9.1] Joining node's version 3.8.8 is not compatible with cluster version 3.9 (Rolling Member Upgrades are only supported for the next minor version) (Rolling Member Upgrades are only supported in Hazelcast Enterprise)

なお、3.8.8の方は起動に失敗してシャットダウンしてしまいます。

12 16, 2017 12:15:09 午前 com.hazelcast.security
重大: [172.20.0.1]:5702 [dev] [3.8.8] Node could not join cluster. Before join check failed node is going to shutdown now!
12 16, 2017 12:15:09 午前 com.hazelcast.security
重大: [172.20.0.1]:5702 [dev] [3.8.8] Reason of failure for node join : Joining node's version 3.8.8 is not compatible with cluster version 3.9 (Rolling Member Upgrades are only supported for the next minor version) (Rolling Member Upgrades are only supported in Hazelcast Enterprise)
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
警告: [172.20.0.1]:5702 [dev] [3.8.8] Terminating forcefully...
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Shutting down multicast service...
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Shutting down connection manager...
12 16, 2017 12:15:09 午前 com.hazelcast.nio.tcp.TcpIpConnection
情報: [172.20.0.1]:5702 [dev] [3.8.8] Connection[id=1, /172.20.0.1:34082->/172.20.0.1:5701, endpoint=[172.20.0.1]:5701, alive=false, type=MEMBER] closed. Reason: TcpIpConnectionManager is stopping
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Shutting down node engine...
12 16, 2017 12:15:09 午前 com.hazelcast.instance.NodeExtension
情報: [172.20.0.1]:5702 [dev] [3.8.8] Destroying node NodeExtension.
12 16, 2017 12:15:09 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Hazelcast Shutdown is completed in 59 ms.
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
重大: [172.20.0.1]:5702 [dev] [3.8.8] Could not join cluster in 300000 ms. Shutting down now!
12 16, 2017 12:15:10 午前 com.hazelcast.core.LifecycleService
情報: [172.20.0.1]:5702 [dev] [3.8.8] [172.20.0.1]:5702 is SHUTTING_DOWN
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Node is already shutting down... Waiting for shutdown process to complete...
12 16, 2017 12:15:10 午前 com.hazelcast.core.LifecycleService
情報: [172.20.0.1]:5702 [dev] [3.8.8] [172.20.0.1]:5702 is SHUTDOWN
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
警告: [172.20.0.1]:5702 [dev] [3.8.8] Config seed port is 5701 and cluster size is 1. Some of the ports seem occupied!
12 16, 2017 12:15:10 午前 com.hazelcast.util.PhoneHome
警告: [172.20.0.1]:5702 [dev] [3.8.8] Could not schedule phone home! Most probably Hazelcast is failed to start.
12 16, 2017 12:15:10 午前 com.hazelcast.instance.Node
情報: [172.20.0.1]:5702 [dev] [3.8.8] Node is already shutting down... Waiting for shutdown process to complete...
Caught: java.lang.IllegalStateException: Node failed to start!
java.lang.IllegalStateException: Node failed to start!
	at com.hazelcast.instance.HazelcastInstanceImpl.<init>(HazelcastInstanceImpl.java:135)
	at com.hazelcast.instance.HazelcastInstanceFactory.constructHazelcastInstance(HazelcastInstanceFactory.java:218)
	at com.hazelcast.instance.HazelcastInstanceFactory.newHazelcastInstance(HazelcastInstanceFactory.java:176)
	at com.hazelcast.instance.HazelcastInstanceFactory.newHazelcastInstance(HazelcastInstanceFactory.java:126)
	at com.hazelcast.core.Hazelcast.newHazelcastInstance(Hazelcast.java:87)
	at com.hazelcast.core.Hazelcast$newHazelcastInstance.call(Unknown Source)
	at hazelcast388-runner.run(hazelcast388-runner.groovy:17)

起動順序を逆にすると、3.9.1側が停止することになります。

では、Hazelcast 3.8で。
hazelcast38-runner.groovy

@Grab('com.hazelcast:hazelcast:3.8')
import com.hazelcast.core.Hazelcast
## 3.8.8
$ groovy hazelcast388-runner.groovy

## 3.8
$ groovy hazelcast38-runner.groovy

こちらは、問題なくクラスタが構成できます。

12 16, 2017 12:19:53 午前 com.hazelcast.internal.cluster.ClusterService
情報: [172.20.0.1]:5701 [dev] [3.8.8] 

Members [2] {
	Member [172.20.0.1]:5701 - df07e168-d992-4bba-b7c0-c6fae1f35d30 this
	Member [172.20.0.1]:5702 - 86c92361-94e4-4260-ab71-9df691b458a7
}

くどくなりますが、今度は3.8.8と3.7.8。

## 3.8.8
$ groovy hazelcast388-runner.groovy

## 3.7.8
$ groovy hazelcast378-runner.groovy

3.7系が相手の場合は、Rolling Upgadeのサポートが3.8以降だからか、両方のMemberが独立して生き残ります。

3.8.8の方。互換性がないと怒られていますね。

12 16, 2017 12:21:11 午前 com.hazelcast.internal.cluster.impl.MulticastService
警告: [172.20.0.1]:5701 [dev] [3.8.8] Received a JoinRequest with an incompatible binary-format. An old version of Hazelcast may be using the same multicast discovery port. Are you running multiple Hazelcast clusters on this host? (This message will be suppressed for 60 seconds). 

3.7.8の方は、単純に認識できないだけになるようです。

12 16, 2017 12:21:14 午前 com.hazelcast.internal.cluster.impl.MulticastJoiner
情報: [172.20.0.1]:5702 [dev] [3.7.8] 


Members [1] {
	Member [172.20.0.1]:5702 - b90571a9-3f82-4613-8207-aa27445b4dcf this
}

12 16, 2017 12:21:14 午前 com.hazelcast.instance.Node
警告: [172.20.0.1]:5702 [dev] [3.7.8] Config seed port is 5701 and cluster size is 1. Some of the ports seem occupied!

今回は同じホストでクラスタを構成しようとしたので、TCPポートがかぶってずれましたが…。

というわけで、やっぱりMinor Versionで互換性がありません、と。Minor Versionを越えてアップデートしたければ、Enterprise版が必要だということですね。

少しだけ中身を

ClusterSafeとClusterStateがどういう実装になっているか、ちょっと確認してみましょう。

ClusterSafeは、全MemberにSafeStateCheckOperationを投げることで確認します。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/partition/PartitionServiceProxy.java#L117-L146

実体はInternalPartitionServiceImpl#isMemberStateSafeで、これがひとつでもfalseを返すとClusterSafeとしてはfalseとなります。全Memberがtrueに
なって、はじめてClusterSafeがtrueになりますと。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/partition/impl/InternalPartitionServiceImpl.java#L912-L914

InternalPartitionServiceImpl#isMemberStateSafeがtrueになるというのは、PartitionReplicaStateChecker#getPartitionServiceStateが
PartitionServiceState.SAFEを返すということで、この確認はバックアップの有無やデータのリバランス中でないかどうか、Masterが
マイグレーション中でないかどうかなどを元に確認します。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/partition/impl/PartitionReplicaStateChecker.java#L76-L94

ClusterStateの方は、ClusterStateManager#getSafeの結果がそのまま使われます。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/cluster/impl/ClusterServiceImpl.java#L955-L957

ClusterStateManagerがクラスタの状態を管理しています、と。
https://github.com/hazelcast/hazelcast/blob/v3.9.1/hazelcast/src/main/java/com/hazelcast/internal/cluster/impl/ClusterStateManager.java

まとめ

Hazelcastのクラスタの状態を確認する方法を調べたり、バージョン間の互換性についてまとめてみました。

このあたりはなんとなく把握はしていたのですが、実際に確認してみたのは初めてなので、いろいろ参考になりました。

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