前言
在 Raft 算法中,副本数据是以日志的形式存在的,领导者接收到来自客户端写请求后,处理写请求的过程就是一个复制和提交日志项的过程.
那么,Raft是如何复制日志,又是如何实现日志的一致性呢,其实这个就是Raft中非常核心的内容。
日志结构
日志的数据结构到底长啥样呢?
日志其实就是一种数据格式,它主要包括用户的指定数据,还有一些附加信息,比如:
- 索引值
- 任期编号
从图中可以看到,一届领导者任期,是有多条日志项的,而且日志项是连续的。
这里抛出几个问题:
- 日志如何复制
- 如何实现日志的一致性
日志如何复制
这里用一张流程图去说明
1、领导者收到客户端的请求,执行客户端指令,创建一个新的指令项,并且会同时创建一个新的日志项,并附加到领导者本地日志中。
2、领导者通过日志复制RPC, 将新的日志复制到其它的节点上。
3、当领导者将日志项,成功复制到大多数的服务器上的时候,领导者就会将该日志项提交到它本地的状态机上。
4、领导者将执行的结果返回给客户端。
5、当追随者收到心跳消息/日志复制RPC消息,如果发现领导者已经提交了某条日志项,它本身还没提交,那么追随者就会将这条日志项提交到本地的状态机上。
如何实现日志的一致性
在Raft算法中,是要以领导者的日志为准的,一旦发现追随者的日志不一致,领导者会要求追随者的日志和它保持一致。
那么这个过程是如何实现的呢?
先说结论:领导者通过日志复制RPC一致性检查,找到追随者节点上,与自己相同日志项的最大索引,在最大索引之前的日志,两者是一致的,在最大索引之后的日志,是不一致的
领导者会强制跟随者更新覆盖不一致的日志项,从而实现日志的一致性。
流程详解
PrevLogEntry:表示当前要复制的日志项,前面一条日志项的索引值。比如在图中,如 果领导者将索引值为 8 的日志项发送给跟随者,那么此时 PrevLogEntry 值为 7。
PrevLogTerm:表示当前要复制的日志项,前面一条日志项的任期编号,比如在图中, 如果领导者将索引值为 8 的日志项发送给跟随者,那么此时 PrevLogTerm 值为 4。
-
1、领导者通过日志复制 RPC 消息,发送当前最新日志项到跟随者(为了演示方便,假设当 前需要复制的日志项是最新的),这个消息的 PrevLogEntry 值为 7,PrevLogTerm 值 为 4。
-
2、如果跟随者在它的日志中,找不到与 PrevLogEntry 值为 7、PrevLogTerm 值为 4 的日 志项,也就是说它的日志和领导者的不一致了,那么跟随者就会拒绝接收新的日志项, 并返回失败信息给领导者。
-
3、这时,领导者会递减要复制的日志项的索引值,并发送新的日志项到跟随者,这个消息 的 PrevLogEntry 值为 6,PrevLogTerm 值为 3。
-
4、如果跟随者在它的日志中,找到了 PrevLogEntry 值为 6、PrevLogTerm 值为 3 的日志 项,那么日志复制 RPC 返回成功,这样一来,领导者就知道在 PrevLogEntry 值为 6、 PrevLogTerm 值为 3 的位置,跟随者的日志项与自己相同。
-
5、领导者通过日志复制 RPC,复制并更新覆盖该索引值之后的日志项(也就是不一致的日 志项),最终实现了集群各节点日志的一致。
需要你注意的是,跟随者中的不一致日志项会被领导者的日志覆盖,而且领导者 从来不会覆盖或者删除自己的日志。