gitbook/分布式协议与算法实战/docs/205784.md
2022-09-03 22:05:03 +08:00

9.2 KiB
Raw Permalink Blame History

08 | Raft算法如何复制日志

你好,我是韩健。

通过上一讲的学习你应该知道Raft除了能实现一系列值的共识之外还能实现各节点日志的一致不过你也许会有这样的疑惑“什么是日志呢它和我的业务数据有什么关系呢

想象一下一个木筏Raft是由多根整齐一致的原木Log组成的而原木又是由木质材料组成所以你可以认为日志是由多条日志项Log entry组成的如果把日志比喻成原木那么日志项就是木质材料。

在Raft算法中副本数据是以日志的形式存在的领导者接收到来自客户端写请求后处理写请求的过程就是一个复制和应用Apply日志项到状态机的过程。

那Raft是如何复制日志的呢又如何实现日志的一致的呢这些内容是Raft中非常核心的内容也是我今天讲解的重点我希望你不懂就问多在留言区提出你的想法。首先咱们先来理解日志这是你掌握如何复制日志、实现日志一致的基础。

如何理解日志?

刚刚我提到,副本数据是以日志的形式存在的,日志是由日志项组成,日志项究竟是什么样子呢?

其实日志项是一种数据格式它主要包含用户指定的数据也就是指令Command还包含一些附加信息比如索引值Log index、任期编号Term。那你该怎么理解这些信息呢

  • 指令:一条由客户端请求指定的、状态机需要执行的指令。你可以将指令理解成客户端指定的数据。
  • 索引值:日志项对应的整数索引值。它其实就是用来标识日志项的,是一个连续的、单调递增的整数号码。
  • 任期编号:创建这条日志项的领导者的任期编号。

从图中你可以看到,一届领导者任期,往往有多条日志项。而且日志项的索引值是连续的,这一点你需要注意。

讲到这儿你可能会问不是说Raft实现了各节点间日志的一致吗那为什么图中4个跟随者的日志都不一样呢日志是怎么复制的呢又该如何实现日志的一致呢别着急接下来咱们就来解决这几个问题。先来说说如何复制日志。

如何复制日志?

你可以把Raft的日志复制理解成一个优化后的二阶段提交将二阶段优化成了一阶段减少了一半的往返消息也就是降低了一半的消息延迟。那日志复制的具体过程是什么呢

首先领导者进入第一阶段通过日志复制AppendEntriesRPC消息将日志项复制到集群其他节点上。

接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。

学到这里,有同学可能有这样的疑问了,领导者将日志项应用到它的状态机,怎么没通知跟随者应用日志项呢?

这是Raft中的一个优化领导者不直接发送消息通知其他节点应用指定日志项。因为领导者的日志复制RPC消息或心跳消息包含了当前最大的将会被提交Commit的日志项索引值。所以通过日志复制RPC消息或心跳消息跟随者就可以知道领导者的日志提交位置信息。

因此当其他节点接受领导者的心跳消息或者新的日志复制RPC消息后就会将这条日志项应用到它的状态机。而这个优化降低了处理客户端请求的延迟将二阶段提交优化为了一段提交降低了一半的消息延迟。

为了帮你理解,我画了一张过程图,然后再带你走一遍这个过程,这样你可以更加全面地掌握日志复制。

  1. 接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。

  2. 领导者通过日志复制RPC将新的日志项复制到其他的服务器。

  3. 当领导者将日志项,成功复制到大多数的服务器上的时候,领导者会将这条日志项应用到它的状态机中。

  4. 领导者将执行的结果返回给客户端。

  5. 当跟随者接收到心跳信息或者新的日志复制RPC消息后如果跟随者发现领导者已经提交了某条日志项而它还没应用那么跟随者就将这条日志项应用到本地的状态机中。

不过这是一个理想状态下的日志复制过程。在实际环境中复制日志的时候你可能会遇到进程崩溃、服务器宕机等问题这些问题会导致日志不一致。那么在这种情况下Raft算法是如何处理不一致日志实现日志的一致的呢

如何实现日志的一致?

在Raft算法中领导者通过强制跟随者直接复制自己的日志项处理不一致日志。也就是说Raft是通过以领导者的日志为准来实现各节点日志的一致的。具体有2个步骤。

  • 首先领导者通过日志复制RPC的一致性检查找到跟随者节点上与自己相同日志项的最大索引值。也就是说这个索引值之前的日志领导者和跟随者是一致的之后的日志是不一致的了。
  • 然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。

我带你详细地走一遍这个过程为了方便演示我们引入2个新变量

  • PrevLogEntry表示当前要复制的日志项前面一条日志项的索引值。比如在图中如果领导者将索引值为8的日志项发送给跟随者那么此时PrevLogEntry值为7。
  • PrevLogTerm表示当前要复制的日志项前面一条日志项的任期编号比如在图中如果领导者将索引值为8的日志项发送给跟随者那么此时PrevLogTerm值为4。

  1. 领导者通过日志复制RPC消息发送当前最新日志项到跟随者为了演示方便假设当前需要复制的日志项是最新的这个消息的PrevLogEntry值为7PrevLogTerm值为4。

  2. 如果跟随者在它的日志中找不到与PrevLogEntry值为7、PrevLogTerm值为4的日志项也就是说它的日志和领导者的不一致了那么跟随者就会拒绝接收新的日志项并返回失败信息给领导者。

  3. 这时领导者会递减要复制的日志项的索引值并发送新的日志项到跟随者这个消息的PrevLogEntry值为6PrevLogTerm值为3。

  4. 如果跟随者在它的日志中找到了PrevLogEntry值为6、PrevLogTerm值为3的日志项那么日志复制RPC返回成功这样一来领导者就知道在PrevLogEntry值为6、PrevLogTerm值为3的位置跟随者的日志项与自己相同。

  5. 领导者通过日志复制RPC复制并更新覆盖该索引值之后的日志项也就是不一致的日志项最终实现了集群各节点日志的一致。

从上面步骤中你可以看到领导者通过日志复制RPC一致性检查找到跟随者节点上与自己相同日志项的最大索引值然后复制并更新覆盖该索引值之后的日志项实现了各节点日志的一致。需要你注意的是跟随者中的不一致日志项会被领导者的日志覆盖而且领导者从来不会覆盖或者删除自己的日志。

内容小结

本节课我主要带你了解了在Raft中什么是日志、如何复制日志、以及如何处理不一致日志等内容。我希望你明确这样几个重点。

  • 在Raft中副本数据是以日志的形式存在的其中日志项中的指令表示用户指定的数据。

  • 兰伯特的Multi-Paxos不要求日志是连续的但在Raft中日志必须是连续的。而且在Raft中日志不仅是数据的载体日志的完整性还影响领导者选举的结果。也就是说日志完整性最高的节点才能当选领导者。

  • Raft是通过以领导者的日志为准来实现日志的一致的。

学完本节课你可以看到值的共识和日志的一致都是由领导者决定的领导者的唯一性很重要那么如果我们需要对集群进行扩容或缩容比如将3节点集群扩容为5节点集群这时候是可能同时出现两个领导者的。这是为什么呢在Raft中又是如何解决这个问题的呢我会在下一讲带你了解。

课堂思考

我提到领导者接收到大多数的“复制成功”响应后就会将日志应用到它自己的状态机然后返回“成功”响应客户端。如果此时有个节点不在“大多数”中也就是说它接收日志项失败那么在这种情况下Raft会如何处理实现日志的一致呢欢迎在留言区分享你的看法与我一同讨论。

最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。