Raft协议–日志复制–03
1、日志的组成
- 由 TermId,LogIndex,LogValue 组成
- 日志由有序编号(log index)的日志条目组成。
- 每个日志条目包含它被创建时的任期号(term)和用于状态机执行的命令。
- (TermId,LogIndex)能确定唯一一条日志
- 如果一个日志条目被复制到大多数服务器上,就被认为可以提交(commit)了。
- 大多数:(服务器节点总数/2)+1
上图显示,共有 8 条日志,提交了 7 条。提交的日志都将通过状态机持久化到磁盘中,防止宕机。
2、日志复制的过程
客户端的一条指令到达,Leader把将指令作为日志条目(Log entries)加入到它的日志中,然后并行的向其他Follower发起 AppendEntries(附加条目)RPC复制日志条目。当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机(比如如果是mysql的insert,那么就是执行insert操作),并向客户端返回执行结果。
-
客户端的每一个请求都包含被复制状态机执行的指令。
-
leader把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。
-
假如这条日志被安全的复制,领导人就应用这条日志到自己的状态机中,并返回给客户端。
-
如果 follower 宕机或者运行缓慢或者丢包,leader会不断的重试,直到所有的 follower 最终都复制了所有的日志条目。
2.1、复制状态机
2.1.1、概念
- 对于一个无限增长的序列 a[1, 2, 3…],如果对于任意整数i,a[i]的值满足分布式一致性,这个系统就满足一致性状态机的要求
- 基本上所有的真实系统都会有源源不断的操作,这时候单独对某个特定的值达成一致显然是不够的。为了让真实系统保证所有的副本的一致性,通常会把操作转化为 write-ahead-log(WAL)。然后让系统中所有副本对 WAL 保持一致,这样每个副本按照顺序执行 WAL 里的操作,就能保证最终的状态是一致的
2.1.2、步骤
- Client 向 leader 发送写请求
- Leader 把"操作"转化为 WAL 写本地 log 的同时也将 log 复制到所有 followers
- Leader 收到多数派应答,将 log 对应的"操作"应用到状态机
- 回复 client 处理结果
2.2、动画过程
客户端发起一个SET 5的请求
这条日志还没被其他任何节点接收,所以它的状态是uncommitted。
Leader会将这条日志通过心跳消息复制给其他的Follower节点
一旦有大多数节点成功写入这条日志,那么Leader节点的这条日志状态就会更新为committed状态,并且值更新为5:
Leader节点然后通知其他Follower节点,其他节点也会将值更新为5。如下图所示,这个时候集群的状态是完全一致的,这个过程就叫做日志复制(Log Replication):
2.3、日志复制 关键点
- 连续性:日志不允许出现空洞
- 有效性:
- 不同节点,拥有相同 term 和 logIndex 的日志 value 一定相同
- Leader 上的日志一定是有效的
- Follower 上的日志是否有效,通过 leader 日志对比判断
2.4、Followers 日志有效性检查
- AppendEntries RPC 中还会携带前一条日志的唯一标识(prevTermId,prevLogIndex)
- 递归推导
2.5、Followers 日志恢复
Leader 将 nextIndex 递减并重发 AppendEntries,直到与 leader 日志一致
3、日志的一致性
3.1、日志复制的两条保证
-
如果不同日志中的两个条目有着相同的索引和任期号,则它们所存储的命令是相同的
- 原因:leader 最多在一个任期里的一个日志索引位置创建一条日志条目,日志条目在日志的位置从来不会改变
-
如果不同日志中的两个条目有着相同的索引和任期号,则它们之前的所有条目都是完全一样的
- 原因:leader每次 RPC 发送附加日志时,会附加这条日志之前日志的索引和任期号一起发送给 follower
- 如果 follower 发现和自己的日志不匹配,那么就拒绝接受这条日志,这个称之为一致性检查
- 如果 follower 发现和自己的日志匹配,才会附加上去。
- 原因:leader每次 RPC 发送附加日志时,会附加这条日志之前日志的索引和任期号一起发送给 follower
3.2、日志的不正常情况
-
raft是通过跟随者强制复制领导者的日志来保证的。
-
一般情况下,Leader和Followers的日志保持一致,因此 AppendEntries(附加条目)一致性检查通常不会失败。然而,Leader崩溃可能会导致日志不一致
- 旧的Leader可能没有完全复制完日志中的所有条目。
-
下图阐述了一些Followers可能和新的Leader日志不同的情况。一个Follower可能会丢失掉Leader上的一些条目,也有可能包含一些Leader没有的条目,也有可能两者都会发生。丢失的或者多出来的条目可能会持续多个任期。
如上图,对于a到f,最终都会和leader同步。
d:索引11的日志有,但是leader没有
f:索引4的任期是2,leader是4
其实就是通过日志覆盖解决。但是对于日志覆盖,我们就会想到一个问题,会不会覆盖已经提交的日志(日志对应指令已经返回给客户端)。那当然不会,如果真有这样,就会有不一致,或者指令丢失现象。
3.3、如何保证日志的正常复制
-
Leader通过强制Followers复制它的日志来处理日志的不一致,Followers上的不一致的日志会被Leader的日志覆盖。Leader为了使Followers的日志同自己的一致,Leader需要找到Followers同它的日志一致的地方,然后覆盖Followers在该位置之后的条目。
-
具体的操作
- Leader会从后往前试,每次AppendEntries(附加条目)失败后尝试前一个日志条目,直到成功找到每个Follower的日志一致位置点(基于上述的两条保证),然后向后逐条覆盖Followers在该位置之后的条目。
-
总结
- 当 leader 和 follower 日志冲突的时候,leader 将校验 follower 最后一条日志是否和 leader 匹配,如果不匹配,将递减查询,直到匹配,匹配后,删除冲突的日志。这样就实现了主从日志的一致性。
4、Commit Index 推进
4.1、CommitIndex(TermId, LogIndex)
- 所谓 commitIndex,就是已达成多数派,可以应用到状态机的最新的日志位置
- 日志被复制到 followers 后,先持久化,并不能马上被应用到状态机
- 只有 leader 知道日志是否达成多数派,是否可以应用到状态机
- Followers 记录 leader 发来的当前 commitIndex,所有小于等于 commitIndex 的日志均可以应用到状态机。
4.2、CommitIndex推进
- Leader 在下一个AppendEntries RPC(也包括 Heartbeat)中携带当前的 commitIndex
- Followers 检查日志有效性通过则接受 AppendEntries 并同时更新本地 commitIndex,最后把所有小于等于 commitIndex 的日志应用到状态机
5、AppendEntries RPC
5.1、完整信息
- currentTerm
- logEntries[]
- prevTerm
- prevLogIndex
- commitTerm
- commitLogIndex
5.2、currentTerm,logEntries[]
日志信息,为了效率,日志通常为多条
5.3、prevTerm,prevLogIndex
日志有效性检查
5.4、commitTerm,commitLogIndex
最新的提交日志位点(commitIndex)