Generation Clock模式解决方案
作者: Unmesh Joshi译者: java达人一个单调递增的数字,表示服务器的generation 。问题在领导者和追随者设置中,有可能会出现领导者与追随者暂时断开联系的情况。leader进程中可能会出现垃圾收集暂停,或者暂时的网络中断,导致leader和follower之间的连接断开
作者: Unmesh Joshi
译者: java达人
一个单调递增的数字,表示服务器的generation 。
问题
在领导者和追随者设置中,有可能会出现领导者与追随者暂时断开联系的情况。leader进程中可能会出现垃圾收集暂停,或者暂时的网络中断,导致leader和follower之间的连接断开。在这种情况下,领导者进程仍在运行,在暂停或网络中断结束后,它将尝试向追随者发送复制请求。这很危险,因为集群的其他部分可能已经选择了新的leader并接受了来自客户机的请求。对于集群的其他部分来说,检测来自旧leader的任何请求非常重要。旧的leader本身也应该能够检测到它暂时与集群断开了连接,并采取必要的纠正措施放弃领导者身份。
解决方案
Generation Clock模式是Lamport时间戳的一个示例:这是一种简单的技术,用于确定跨一组进程的事件顺序,而不依赖于系统时钟。每个进程维护一个整数计数器,该计数器在该进程执行每个操作后递增。每个进程还将这个整数连同进程交换的消息一起发送给其他进程。接收消息的进程通过获取自己的计数器和消息的整数值之间的最大值来设置自己的整数计数器。这样,任何进程都可以通过比较相关的整数来确定哪个操作在另一个操作之前发生。如果消息在多个进程之间交换,也可以对多个进程之间的操作进行比较。可以这样比较的行为被称为“因果关系”。
维护一个单调递增的数字表示服务器的generation 。每次新领导者选举时,都应该以增加一个generation 为标志。generation 需要在服务器重新启动之后可用,因此它与 Write-Ahead Log中的每个条目一起存储。正如在High-Water Mark中所讨论的,追随者使用此信息来查找日志中的冲突条目。
在启动时,服务器从日志中读取最近已知generation 。
class ReplicationModule… this.replicationState = new ReplicationState(config, wal.getLastLogEntryGeneration());
有了领导者和追随者,服务器就会在每次有新的领导者选举时递增generation 。
class ReplicationModule… private void startLeaderElection() { replicationState.setGeneration(replicationState.getGeneration() + 1); registerSelfVote(); requestVoteFrom(followers); }
服务器将生成的generation 作为投票请求的一部分发送到其他服务器。这样,在一个成功的领导者选举后,所有的服务器都有相同的generation。一旦领导被选出来,追随者们就会被告知新generation 的情况
follower (class ReplicationModule...) private void becomeFollower(Long generation) { replicationState.setGeneration(generation); transitionTo(ServerRole.FOLLOWING); }
之后,领导者在发送给追随者的每个请求中都包含了generation。它将generation包含在每个心跳消息以及发送给追随者的复制请求中。
Leader在其Write-Ahead Log中保留每一个条目的generation
leader (class ReplicationModule...) Long appendToLocalLog(byte[] data) { var logEntryId = wal.getLastLogEntryId() + 1; var logEntry = new WALEntry(logEntryId, data, EntryType.DATA, replicationState.getGeneration()); return wal.writeEntry(logEntry); }
这样,作为Leader和follower复制机制的一部分,它也被持久化到follower日志中
如果一个追随者从一个被废弃的领导者那里得到一个信息,这个追随者可以辨别出来,因为它的generation 太小了。追随者然后返回一个失败的响应。
follower (class ReplicationModule...) Long currentGeneration = replicationState.getGeneration(); if (currentGeneration > replicationRequest.getGeneration()) { return new ReplicationResponse(FAILED, serverId(), currentGeneration, wal.getLastLogEntryId()); }
当一个领导者得到这样一个失败的响应时,他就变成了一个跟随者,并期待来自新领导者的交流。
Old leader (class ReplicationModule...) if (!response.isSucceeded()) { stepDownIfHigherGenerationResponse(response); return; }
private void stepDownIfHigherGenerationResponse(ReplicationResponse replicationResponse) { if (replicationResponse.getGeneration() > replicationState.getGeneration()) { becomeFollower(replicationResponse.getGeneration()); } }
考虑下面的例子。在三个服务器集群中,leader1是现有的leader。集群中的所有服务器的generation都为1。Leader1向追随者发送连续的心跳。Leader1有一个很长的垃圾收集暂停时间,比如5秒。追随者没有得到一个心跳,超时后选举一个新的领导者。新领导者将generation增加到2。垃圾收集暂停结束后,leader1继续向其他服务器发送请求。generation为2的追随者和新领导者拒绝请求,并发送失败响应,带上generation2。leader1处理失败响应,并回退做一个跟随者,generation 更新为2。
例子
Raft Raft使用Term的概念来标记leader generation。
Zab 在Zookeeper中,epoch号作为每个事务id的一部分进行维护。因此,在Zookeeper中持久化的每个事务都有一个以epoch标记的generation。
Cassandra 在Cassandra中,每个服务器存储一个generation编号,该编号在服务器每次重启时递增。generation信息保存在系统密钥空间中,并作为gossip消息的一部分传播到其他服务器。接收到gossip消息的服务器可以比较它所知道的generation值和gossip消息中的generation值。如果gossip消息中的generation值较高,则知道服务器已重启,然后丢弃为该服务器维护的所有状态,并请求新的状态。
Kafka中的Epoch 在Kafka中,每次为Kafka集群选择一个新控制器时,都会创建一个epoch数并存储在Zookeeper中。epoch包含在从控制器发送到集群中其他服务器的每个请求中。另一个称为LeaderEpoch的epoch被维护,以了解追随者分区High-Water Mark是否落后。