Raft客户端实现要点

681 阅读3分钟

消息重复

客户端消息处理最困难的一点在于消息可能会重复。比如客户端向Leader发送了一条指令,Leader收到了这条指令并执行了,但是连接在响应返回之前断开了。客户端没有收到回复,所以接下来会重连然后重新发送这条指令。这时服务器就必须想办法去重。

消息去重

去重意味着客户端的消息必须有编号,服务器会记录这些编号,以便重复消息过来的时候,可以判断是否已经处理过了。 如果已经处理过了,会缓存响应内容。这时重复消息过来了,可以直接将响应内容返回给客户端而不需要进行重复处理。如果消息正在处理中,那么等消息处理结束,直接一块响应即可。

回话

服务器会为每个客户端连接维持一个回话session,记录客户端的交互状态。每个客户端回话会被赋予一个唯一ID。当连接不小心断开,通过重连还可以挂接到之前的session对象,因为客户端会将回话的ID记录在内存中。如果断开的时间较久,服务器的回话会过期,客户端带着回话ID进行再重连交互时,服务器会返回回话过期异常。这时客户端需要再注册一个新回话,并抛弃之前回话中的所有消息,重新进行交互。

回话期间的消息采用序列号进行唯一标识,序列号相同的消息是重复的消息,每生成一个新的消息,序列号递增。

会话过期

回话不可能永远持续下去,考虑到内存的上限,回话是需要过期的。回话的过期也必须通过日志协商,否则系统的一致性就很难满足。比如在一个特定的时间点,某个客户端的回话对象在一个节点上是活的,在另一个节点上是过期的。没有过期的回话对象内部还存储了最近客户端的指令ID。这个时候需要将已经提交的日志apply到状态机。这个apply的过程要检查指令的重复与否。这个重复检查的结果在两个节点上就可能是不一样的,紧接着会导致两节点的状态机数据出现不一致。

回话的过期一般有两种策略。第一种是限定session的数量,通过LRU算法来淘汰陈旧的session。另外一种是通过协商一致的时间源来过期。不同的节点需要在日志的时间上达成一致,这样才可以在apply相同的日志时有相同的时间,回话过期也就会有相同的结果。在时间上达成一致一般是以Leader在日志里放入自身的当前时间戳做到的,其它节点就是通过这个时间戳来作为时间源来决定回话的过期与否。

标准raft协议里没有提到回话的主动过期,比如客户端主动退出,此时应该可以允许客户端在连接断开之前发送一个RemoveClient的指令,注销当前的回话,及时给服务器腾出空间来。

关注公众号【码洞】,加入我们一起进阶Raft协议