这里只是使用了Hazelcast在分布式集群服务下的分布式缓存解决方案,Hazelcast还有其他的功能可以自行去官网查看文档:Tutorialshttps://docs.hazelcast.com/tutorials/
基础单机版本环境搭建可以去官方的github上下载:
集群搭建
由于Hazelcast是无中心化的分布式集群,所以不需要一个单独服务器来管理集群,所以单机版的代码更换端口后启动多个即可自动成为集群,启动后可以看到日志中的打印的Members信息。
Hazelcast通过分区计算后(默认是哈希运算)将数据分布到分区上面去,默认分区数量为271个,我的例子中这271个分区会被分配到 这3 个节点上,每个节点存储多个分区的数据。因此,每个节点上会有一部分数据,而不是所有数据的复制。同时每个节点的数据还会有副本放在其他节点上(默认一份),保证了单节点故障后数据不会丢失,提升了可用性。
故障转移
修改一下Controller的方法以供测试,put方法存入1000个元素,getSize方法打印map的大小
@RestController
public class CommandController {
@Autowired
private HazelcastInstance hazelcastInstance;
private ConcurrentMap<String,String> retrieveMap() {
return hazelcastInstance.getMap("map");
}
@GetMapping("/put")
public void put() {
for (int i = 0; i < 1000; i++) {
retrieveMap().put(i+"",i+"");
}
}
@GetMapping("/get")
public CommandResponse get(@RequestParam(value = "key") String key) {
String value = retrieveMap().get(key);
return new CommandResponse(value);
}
@GetMapping("/getSize")
public CommandResponse getSize() {
String value = retrieveMap().size() + "";
return new CommandResponse(value);
}
}
首先调用put方法,往map里存放1000个元素,然后随便关掉一个服务模拟故障,可以看到log。
2024-08-23 16:54:53.353 INFO 26300 --- [.IO.thread-in-1] c.h.i.server.tcp.TcpServerConnection : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Connection[id=2, /172.30.8.178:5701->/172.30.8.178:54890, qualifier=null, endpoint=[172.30.8.178]:5703, remoteUuid=f68f2660-c4ec-483f-a930-45b4734f4262, alive=false, connectionType=MEMBER, planeIndex=0] closed. Reason: Connection closed by the other side
2024-08-23 16:54:53.369 INFO 26300 --- [cached.thread-7] c.h.i.server.tcp.TcpServerConnector : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Connecting to /172.30.8.178:5703, timeout: 10000, bind-any: true
2024-08-23 16:54:55.409 INFO 26300 --- [cached.thread-7] c.h.i.server.tcp.TcpServerConnector : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Could not connect to: /172.30.8.178:5703. Reason: IOException[Connection refused: no further information to address /172.30.8.178:5703]
2024-08-23 16:54:57.659 INFO 26300 --- [cached.thread-8] c.h.i.server.tcp.TcpServerConnector : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Connecting to /172.30.8.178:5703, timeout: 10000, bind-any: true
2024-08-23 16:54:59.702 INFO 26300 --- [cached.thread-8] c.h.i.server.tcp.TcpServerConnector : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Could not connect to: /172.30.8.178:5703. Reason: IOException[Connection refused: no further information to address /172.30.8.178:5703]
2024-08-23 16:54:59.811 INFO 26300 --- [cached.thread-8] c.h.i.server.tcp.TcpServerConnector : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Connecting to /172.30.8.178:5703, timeout: 10000, bind-any: true
2024-08-23 16:55:01.848 INFO 26300 --- [cached.thread-8] c.h.i.server.tcp.TcpServerConnector : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Could not connect to: /172.30.8.178:5703. Reason: IOException[Connection refused: no further information to address /172.30.8.178:5703]
2024-08-23 16:55:01.849 WARN 26300 --- [cached.thread-8] .h.i.s.t.TcpServerConnectionErrorHandler : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Removing connection to endpoint [172.30.8.178]:5703 Cause => java.io.IOException {Connection refused: no further information to address /172.30.8.178:5703}, Error-Count: 5
2024-08-23 16:55:01.850 INFO 26300 --- [cached.thread-8] c.h.i.cluster.impl.MembershipManager : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Removing Member [172.30.8.178]:5703 - f68f2660-c4ec-483f-a930-45b4734f4262
2024-08-23 16:55:01.852 INFO 26300 --- [cached.thread-8] c.h.i.p.impl.PartitionStateManager : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Storing snapshot of partition assignments while removing UUID f68f2660-c4ec-483f-a930-45b4734f4262
2024-08-23 16:55:01.856 INFO 26300 --- [cached.thread-8] c.h.internal.cluster.ClusterService : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2]
Members {size:2, ver:4} [
Member [172.30.8.178]:5701 - 39e7121d-0991-4f09-a27b-94eba3a1589e this
Member [172.30.8.178]:5702 - f90fd196-9cbb-4a7e-a2a5-ebec9b92853d
]
2024-08-23 16:55:01.863 INFO 26300 --- [cached.thread-2] c.h.t.TransactionManagerService : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Committing/rolling-back live transactions of [172.30.8.178]:5703, UUID: f68f2660-c4ec-483f-a930-45b4734f4262
2024-08-23 16:55:01.943 INFO 26300 --- [opper.migration] c.h.i.partition.impl.MigrationManager : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] Repartitioning cluster data. Migration tasks count: 183
2024-08-23 16:55:02.296 INFO 26300 --- [opper.migration] c.h.i.partition.impl.MigrationManager : [172.30.8.178]:5701 [hazelcast-cluster] [5.3.2] All migration tasks have been completed. (repartitionTime=Fri Aug 23 16:55:01 SGT 2024, plannedMigrations=183, completedMigrations=183, remainingMigrations=0, totalCompletedMigrations=183)
最后三行显示故障节点的数据已经被恢复且节点已经被移除。
具体流程:
1. 节点检测和通知
- 故障检测: Hazelcast 集群通过心跳机制监控每个节点的状态。当集群中的其他节点无法接收到某个节点的心跳时,会认为该节点已退出或发生故障。
- 成员变更通知: 一旦集群检测到节点退出,集群中的所有其他节点都会收到成员变更的通知。
2. 分区重新分配
- 分区重组: Hazelcast 会立即重新分配之前由退出节点负责的数据分区。该节点所拥有的分区将被重新分配给集群中的其他节点,并确保所有数据仍然有至少一个副本。
- 数据迁移: 被重新分配的分区数据会从现有的副本节点迁移到新的主节点和备份节点。这是一个异步过程,但 Hazelcast 会尽量加快迁移过程,以减少数据丢失的风险。
例:比如A、B、C组成的集群,C的备份在A, C 故障,会把C的分区数据从 A 的备份数据直接转移,A 成为这些分区的主节点,B 继而持有这些数据的备份副本
3. 数据副本的修复
- 副本恢复: 如果某个节点退出后导致某些数据分区的副本不足,Hazelcast 会尝试在其他节点上创建新的副本,以恢复配置的备份副本数量(通常是一个主副本和多个备份副本)。
- 一致性检查: Hazelcast 会在数据恢复后进行一致性检查,确保所有副本的数据是一致的。
4. 任务和操作的重分配
- 任务重分配: 如果退出的节点上运行了某些分布式任务(如 Executor 服务中的任务),这些任务将会被重新分配到其他可用节点上,以确保任务执行的连续性。
- 事务恢复: 如果退出节点参与了分布式事务,Hazelcast 会根据事务的状态进行恢复和重试,以确保事务的一致性和完整性。
5. 集群状态更新
- 集群元数据更新: Hazelcast 会更新集群的元数据,以反映当前集群的状态,包括节点成员列表、分区分配信息等。
- 客户端通知: 集群中的所有客户端会收到节点退出的通知,以便它们可以更新自己的路由和连接信息。
6. 系统日志和监控
- 日志记录: Hazelcast 会记录节点退出的详细信息,包括时间、节点地址、分区迁移情况等,以便系统管理员进行故障排查和分析。
- 监控警报: 如果你配置了监控系统(如 Hazelcast Management Center),会生成相应的警报,提示集群管理员节点退出事件。
7. 重连
- 故障节点重连: 会进行一系列数据转移,比如可能会将一些分区从 A 和 B 转移回 C,各个节点的副本也可能会重新进行分配,以确保各节点的数据和负载均衡。在数据重新分配和副本调整过程中,Hazelcast 会确保所有节点上的数据都是一致的。Hazelcast 会进行数据同步操作,以确保任何数据迁移或复制都是基于最新的、正确的数据版本。
重启C服务后,集群数据重新分配,由此实现了故障转移。
实战撮合系统整合Hazelcast
Hazelcast默认是AP弱一致性,后续会专门实现集成一个内存撮合系统并且保证数据强一致性(使用的 Raft 协议)。在 CP 模式下,所有对写入操作都需要得到大多数节点的确认,从而确保数据在多个节点之间的一致性。