分布式一致性协议 Raft 论文原理一篇读透

本文详细解析了Raft一致性算法,包括选举过程、日志复制策略以及如何保证系统安全性。Raft通过限制选举规则确保了日志的完整性,并通过日志匹配属性保证了状态机的一致性。此外,文章还探讨了Raft如何处理节点宕机、动态变化以及与CAP定理的关系,展示了其易理解性和实现细节。
摘要由CSDN通过智能技术生成

参考文章

In Search Of Understandble Consensus Algorithm

前言

笔者在校期间, 学习过的分布式一致性协议是 Paxos, 当时导师声称 Paxos 是分布式系统一致性协议的统治者, 其他一致性协议都可以看作是 Paxos 的变体。 工作后则似乎听到了在 Paxos 之后出现的新晋流行算法 Raft, 笔者希望通过本文详细解析 Raft 的论文细节, 理清 Raft 协议的原理, 再另外单独写文章分析 Paxos 与 Raft 的联系与不同

分布式一致性协议的设计目的

分布式一致性协议的设计, 来源于我们对系统可用性,容错性的追求。 为了避免单机失效导致整个系统不可用的问题, 我们会很自然地希望复制数据多结点部署。 但数据一旦出现了多份, 就需要解决一致性的问题。

为了解决多份数据的一致性问题, 我们常常将其简化为多份状态机的一致性问题。 对于客户端发来的一系列指令, 只要我们能确保将指令以唯一确定的顺序发送给所有结点, 那么只要各个状态机的初始状态相同, 执行完所有指令后 (指令对状态机的影响必须是唯一确定的结果), 必然也会到达相同的状态, 实现一致性目标。 在多状态机一致性问题中, 共识算法( Consensus Algorithm) 的作用就是确保多份 log 中包含顺序和内容完全一致的指令。

  • Tip: 曾见到有同学纠结 共识(Consensus) 和与一致性(Consistency) 的概念区别,笔者个人的建议是: 两者差别更多地是在语境上, 并没有实质差别。 所谓达成共识, 就是对某个信息多方形成一致的意见。 接受业界习惯的特定用词语境即可。

Paxos 有什么问题

既然 Raft 是在 Paxos 之后设计出的一致性协议,那么必然需要理清 Paxos 存在的问题。

据 Raft 作者描述, Paxos 算法最开始是定义了一种算法, 能够使多个结点针对单个决议/单个数据项 达成共识。 Raft 将该算法称为 single-decree Paxos(单决议 Paxos)。 在实践中, 我们希望达成一致的数据, 显然不会是个单项孤立的数据, 在单决议 Paxos (single-decree Paxos)的算法基础上实现多决议/多数据一致性的算法被称之为multi-Paxos( 多决议 Paxos)

Raft 作者在指出 Paxos 的问题前先总结了其优点, 表示 Paxos 实现了安全性(safety)与存活性(liveness), 且在正常的情况下, 效率不差。

  • 安全性(safety)
    • 在集群内结点可能宕机, 通信消息可能丢失,延迟的情况下(消息不会被篡改),客户端发往集群的请求不会返回错误的结果
    • 所谓错误的结果是指违反 Linearizability 的结果, Linearability 的定义后续有详细说明
  • 存活性 (liveness)
    • 当集群半数以上结点正常运转且可以相互正常通信的情况下, 整个系统可以正常对外提供服务。
    • 注意上述存活性的定义中, 半数以上结点能够相互通信非常重要, 缺失了这一前提后, 再想实现同时满足 livess 和 safety 的系统就是一个不可能达成的目标, 这是被学界大佬证明过的, 类似于 CAP 定理

阐明完毕优点之后, Raft 作者认为 Paxos 存在如下缺点

  • 极度难以理解
    • Paxos 原始论文《The part-time parliament》是众所周知的晦涩难懂,只有很少的人在花费了极大的精力研读后才能理解。由此也产生了一些文献专门用于简化解释 Paxos 的原始论文, 这其中就包括 lamport 本人锁撰写的 《Paxos made simple》 以及其他学者撰写的《How to Build a Highly Available System Using Consensusr》以及 《The ABCD’s of Paxos》 等等。 这些解释的关注焦点都主要放在 single-decree Paxos, 但是理解这些简化后的论文依旧不是一件容易的事。
    • Raft 作者提到他们自己在阅读了一些有关 Paxos 的简化版解读之后并尝试自行设计一个替代性协议后, 才真正完全理解 Paxos 协议, 这个过程花了他们将近一年时间
  • 缺失了工程化的细节信息
    • Lamport 本人的论文描述主要集中于 single-decree Paxos, 他描述了几种实现 multi-Paxos 的可行方法, 但缺失了细节, 有一些论文试图更完整地描述 multi-Paxos 的实现方式, 但是这些论文描述的方案各不相同, 并且与Lamport 本人描述的 multi-Paxos 实现思路也有差异。 Google 实现的 Chubby 系统论文中声称基于 Paxos 的算法, 但是在 Raft 作者看来, Chubbty 也有很多细节信息没有发表在论文里。 作者引用了 Chubby 论文里的话来佐证 Paxos 的工程实现困难。

    • There are siginificant gaps between the desciption of the Paxos algorithm and the needs of a real-world system… the final system will be based on an un-proven protocol

因此, 作者设计 Raft 的核心设计目标就是如下4点(前两点性质 Paxos 已经具备, Raft 额外实现了后两个目标)

  • 安全性: 算法正确性能得到证明, 确保多状态机一致性的达成
  • 存活性: 半数以上结点正常工作且相互能够通信的情况下, 整个系统能持续对外提供服务
  • 易懂性: 聪明的你和我都能快速理解 Raft 协议的核心原理与正确性
  • 易实现: 充分描述算法的实现细节, 让心思简单的程序猿也能对着算法内容,按部就班地实现一个可用的系统。

Raft 整体思路

在这里插入图片描述
正如前面提到的, 多结点数据一致性的问题可以被抽象简化为多状态机副本一致性问题。如上图 Figure 1 所示,客户端可以向多份状态机组成的系统发送指令(客户端具体向哪个状态机发送指令, 向几个状态机发送指令, 都可以由共识协议算法来规定), 共识协议算法需要保证每份状态机最终执行的一样的客户端指令, 且执行顺序一致。为如果每条指令引发的状态机变化的结果是确定唯一的, 只要每台状态机都按相同顺序执行了相同的指令, 那必然会达到相同的状态, 由此解决了一致性问题。 为简化描述, 文章后续将需要保持内容与顺序一致的指令集简称为指令日志( command log ) 或直接简称为日志( log)

Raft 作为共识协议算法, 解决问题整体思路非常符合一般人的直观思路, 即设法让整个集群拥有一个主结点, 客户端只向集群中的主结点发送指令, 如果客户端将指令发送给了集群中的非主结点, 非主结点都会直接返回主结点的地址, 让客户端与主结点建立连接,由主结点进行所有指令的接收和分发, 这样非常简单地就可以确保集群中的每个状态机都收到一份相同的指令 log。

上述思路在主结点永不宕机的情况下可以完美运行,但是当主结点存在宕机可能时, 问题就变得复杂起来, 因为主结点向多个结点分发指令集的时候, 必然只能通过网络通信完成, 当主结点广播发送数据且未收到响应时发生了宕机, 我们没办法确认集群中的其他结点是否都收到了指令, 此时多份状态机持有并执行过的指令集就可能不一样, 在主结点已经宕机的情况下, 剩余的结点如何选出新的主结点,且对指令集形成共识, 恢复对客户端指令的响应能力, 是整个算法需要解决的难点问题。

Raft 采用了分而治之的思想, 将整个复杂的问题分解为三个相对独立的子问题逐个解决。

  • 主结点选举问题
    • 如何在一个主结点宕机后, 重新为集群选举出新的主结点。
  • 指令集分发问题
    • 主结点必须能够接收客户端发来的指令,并将指令 log 分发复制给集群中的其他结点, 并强迫其他状态机所持有且执行的指令 log 与自己的指令 log 达成一致。
  • 安全性问题
    • Raft 算法对于安全性的定义有多条始终成立的性质共同构成, 具体如下图
      在这里插入图片描述

学习 Raft 解决这三个问题的思路前, 需要先了解一下 Raft 系统中一些基础概念的定义

一些 Raft 基础概念的定义

Raft 集群大小

集群内的机器数量可以自由定义,但是通常为 5。 因为 5 台机器构成的 Raft 集群可以容忍2个结点的宕机, 这在一般的实践场景中已经足够, 同一时刻出现3个机器的宕机概率很小, 只要不出现三台机器同时失效的情况, 系统都可以正常对外提供服务。 如果是集群内某一台或者两台机器宕机, 我们都有充足的时间可以修复宕机机器, 如果集群内的机器数量过大, 可能会降低整个系统对客户端的指令处理速度( 因为要复制更多数据,协调更多数据的一致性)

Raft 集群中结点的角色

  • leader(主结点)
    • leader 负责接收、处理、响应所有来自客户端的请求
  • follower (从结点)
    • follower 自己不会发送任何请求, 只会响应来自 leader 和 candidate 的请求, 不会响应其他 follower 发来的请求, 如果 follower 收到了来自客户端的请求, follower 就将主结点的信息返回客户端, 让客户端去联系主结点。
  • candidate (主结点候选人)
    • candidate 是选举过程中的结点角色, 当 leader 失效后, 会有follower 转变为 candidate , 下一个 leader 从candidate 中产生。

在任意时刻, 集群内的任意结点都只会是上面三种角色的其中一种。状态转化的流程如下图描述
在这里插入图片描述

集群刚刚启动时, 所有结点都是 follower, 当 follower 收不到主结点发来的心跳时, 就会转变为 candidate, 发起 leader 选举流程。 当一个 candidate 收集了集群中半数以上结点的选票后, 就能变成 leader。 一个 leader 会持续运行直到自己宕机或者发现其他拥有更高任期 (term) 的 leader。 term 的概念在后文中定义。

Raft 集群中的时间

在这里插入图片描述
如上图所示, Raft 将系统内的时间划分成一段一段的任期(term)。

  • 每一个 term 的开始阶段都是选举过程, 在选举过程中, 会出现一个或多个候选人(candidate) 竞选 leader 的情形。在一次成功的选举后, 竞选成功的 leader 将会一直管理着整个集群直到下一个 term 出现。
  • 有时, 竞选过程可能存在分票的情况, 即有有多个竞选人获得了相同数量的选票, 在这种情况下, 没有选出 leader, 那么这个 term 就会直接结束,下一个 term 会进行新的选举
  • Raft 算法会确保每一个 term 之多只有一位 leader 。

值得注意的是, 集群中的每台服务器所观察到的 term 切换可能是不一样的。 例如

  • 服务器 A 上观察记录到的任期变化过程可能是: term1 --> term2–>term3
  • 服务器 B 上观察记录到的任期变化过程可能是 :term1–> term3

term 这个概念在 Raft 中的作用其实是一个逻辑时钟(logical clock)的概念

logic clock 是 lamport 大神在其经典论文 《Time, clocks, and the ordering of events in a distributed system》 中提出的用于解决分布式系统中, 多机器时间不同步的情况下, 如何确定不同机器上发生的事件先后顺序的概念。没有了解过的同学可以 google 一下相关的知识(百度应该也可以, 不过需要注意挑选, 有些文章质量很差)

在 Raft 中, term 可以帮助集群内的服务器检测出已经过时的信息, 例如过期的 leader。 term 的基本工作流程如下:

  • 每一个服务器都会存储一个 current_term 的变量, 该变量允许被增大,不允许被减小。
  • Raft 集群内每一个服务器对外发送请求的时候, 请求中都会携带 current_term 变量的值
  • 每个服务器 S 在收到任意请求时, 会先比较自己保存的 current_term 和收到的请求中携带的 current_term'的大小
    • 如果 current_term < current_term' , 那么服务器 S 就会把自己的 current_term 更新为 current_term'。如果服务器 S 此时的身份是 candidate 它会立刻将自己退化为 follower。
    • 如果current_term = current_term' , 正常处理请求即可
    • 如果current_term > current_term' , 说明发来请求的服务器还处于一个过期的任期中, 直接拒绝请求。

Raft 集群中的通信方式

Raft 集群中的服务器相互之间都通过远程调用(remote procedure call, RPC) 方式进行, 当 RPC 超时未获得响应时, 会进行重试。当一个服务器需要一次性发起多个 RPC 调用时, 会并发地进行, 以提供尽可能好的性能。 Raft 算法核心部分只需要如下两种 RPC

  • RequestVoteRPC
    • 由竞选过程中的 candidates 发起调用, 用于收集选票
  • AppendEntriesRPC
    • 由 leader 发起调用, 用于将指令 log 分发到各台服务器上或者作为心跳通知使用

如何选举 Leader

在铺垫好了 Raft 系统中的一些基本概念后, 我们可以开始学习 Raft 如何解决第一个子问题, 即一个主结点宕机后, 如何重新为集群选举出新的主结点。

Raft 通过心跳机制来触发 leader 的选举过程。 当 Raft 集群第一次启动时, 每台服务器的身份都是 follower。一台服务器只要能够收到来自 leader 或者 candidate 发来的 RPC 调用就会保持自己的 follower 身份。 Leader 会向所有的 follower 定时发送心跳( 也就是不携带数据的 AppendEntriesRPC)以维持他的权威。 如果一个 follower 在一段时间后( election_timeout) 一直没有收到任何 RPC 调用请求, 它就会认为当前集群没有有效的 leader , 发起一轮新的选举。

一个 follower 的发起选举流程如下:

  • 执行 current_term = current_term + 1
  • 将自己的身份转变为 candidate

一个 candidate 竞选方式如下:

  • 给自己投一票, 并向集群中所有其他服务器发起 RequestVoteRPC 并发调用。 发起投票后总共可能有如下三种结果
    • 该 candidate 成功获得了选举
    • 有其他服务器当选 leader , 自己竞选失败
    • 一段时间过去后, 没有服务器当选 leader, 自己也没有竞选成功

下面对以上三种情况分别进行分析

candidate 赢得选举

在一个 term 内, 某个 candidate 候选人如果成功收集到集群内半数以上的选举, 它就可以当选 leader 。 每一个 follower 在某个特定的 term 中, 只会投出一张选票, 投票的原则是”先到先得“:最先收到哪一个 candidate 的 RequestVoteRPC 调用, 就将选票投给它。 (后续还会引入一些其他的投票规则)

收集半数以上选票才能竞选成功这一规则确保了同一个 term 中至多只会产生 1 个 leader 。一旦一个 candiate 赢得选举后, 它就会向集群内其他服务器发送心跳, 以此维持它的权威并且避免其他服务器发起新一轮选举。

candidate 输掉选举

在募集到半数以上选票前, 一个 candidate 可能收到其他自称为 leader 的服务器发来的 AppendEntriesRPC 调用, 处理方式如下:

  • 如果AppendEntriesRPC 中携带的 current_term‘ 大于等于 candidate 自己保存的 current_term , 那么该 candidate 就承认发来 AppendEntriesRPC 的 leader , 将自己的状态退化为 follower。
  • 如果AppendEntriesRPC 中携带的 current_term‘ 小于 candidate 自己保存的 current_term , 那么该 candidate 就拒绝 AppendEntriesRPC 的请求, 保持自己的 candidate 状态, 等待完成选票的收集。

candidate 平票

如果同时有多个 follower 变成了 candidate 参与竞选, 那每个候选者的票数可能都无法达到半数以上。 这种情形下, 每个候选者都会因选票募集超时而开启一轮新的选举。 然而如果不添加一些其他措施的话, 分票现象可能会一直发生。

为了尽可能减少平票问题的出现, Raft 采用了随机化election_timeout的策略: 每一个服务器的超时时间都是在一个范围内随机选择的(例如100-300ms)。 这种做法可以保证在大部分情况下, 都只有一个服务器会发生超时,进而成为 candidate 发起竞选, 并且在其他服务器超时前赢得选举, 成为 leader, 通过广播心跳来避免他们因超时未收到任何 leader 的心跳发起选举。

如果在这种情况下, 依旧碰巧有多个服务器同时超时发生了平票, 那么继续采用随机化等待时间机制, 即每个服务器在选举开始前重新随机设置自己的 election_timeout, 发起下一轮选举前, 先等待 election_timeout 中所设置的时长, 以此减少下一轮再次出现多个 candidate 平票的可能。

  • 思考: 如果 leader 正常工作, 集群中有一个结点只是与 leader 结点间通信不畅, 发起了一轮选举, 集群中的服务器会如何响应它

Raft 作者表示, 他们在算法设计过程中,对于如何解决 “无限平票”问题修改了好几版解决方案, 最终采用了 “随机等待重试” 的策略。 采用这一策略的出发点正是他们极度重视的算法易懂性(Understandability)。

如何分发 Command Log

一旦 leader 被选出, 它就可以开始接收客户端发来的请求。 每一个客户端请求都包含着一个需要被多个状态机副本执行的指令。 Leader 把这个指令作为一条日志记录(log entry) 追加到自己 command log 末尾, 然后再对其他所有服务器并行发起 AppendEntriesRPC 调用。 当一条 log entry 被 ”安全“ 地复制到多分状态机上后(后文会定义"安全复制" 的判断标准),leader 就可以执行这条日志中的指令,然后将指令的执行结果返回给客户端。 如果 follower 因为宕机、运行缓慢、或者网络数据通信丢包等问题导致 leader 发出的 AppendEntriesRPC 调用没有得到响应, leader 会不限次数地一直进行重试直到所有的 follower 都存储了相同的 log 记录。

Command Log 的格式

Command Log 的形式如下图

在这里插入图片描述

  • 日志中的每一条记录都包含客户端发来的指令, 例如上图中的x<-3 表示对变量 x 执行赋值 3 的操作。
  • 每一条记录都包含了 leader 收到这条客户端指令, 创建日志记录时刻的 current_term , 也就是上图中每个赋值语句上方的数字。 在日志记录中保存日志记录创建时刻的 current_term 值的作用后文会详细讨论
  • 每一条日志记录也有一个整形索引, 用于标识该条记录在整个日志中所处的位置。 对应于上图的 log index
  • 如果一条日志记录被认为已经安全地实现了一致性分发, 该指令被称为 ”commited", 已经处于“committed" 的指令就可以被状态机执行。leader 会决定何时一条日志记录可以被判定为 “commited", 具体的判断标准是: leader 成功地将该条指令日志记录复制到了半数以上的集群机器后, 就可以认为该日志的状态是 ”commited"。 例如对于上图中的 leader 而言, 最后一条属于 “commited" 状态的日志记录是 index 为 7 的记录, 因为它已经被复制到了集群的 3 台机器上(包含 leader 自己)。

可能有同学会担心, 上述 commited 判断标准是否足够安全, 当 leader 刚刚分发日志到 3 台机器后, 如果突然宕机, 产生了新的 leader, 新的 leader 恰好是没有收到最后一条 commited 指令的机器怎么办。 我们后续讨论并解决这个问题, 并且还会进一步完善 “committed” 判定标准, 以避免一些其他的问题。

leader 会记录它已知的处于 commited 状态的 log entry 索引的最大值 max_committed_index, 并且会把max_committed_index 的值作为AppendEntriesRPC 的参数内容发送给其他机器。当 follower 得知自己的 log 中索引为 max_committed_index 的日志已经与 leader 的 log 达成一致( 如何判断后文会详述) , 且已经属于 commited 状态, 就可以将该指令以及在该记录之前 committed 的指令按索引顺序依次执行到状态机。

Command Log 的分发机制

前面定义了 Log 的一些基本格式和术语, 这里可以继续看 Raft 算法所设计的日志记录的维护机制。 Raft 算法所设计的日志记录分发维护机制的出发点是在不同的服务器上保持多份 command log “极高程度的的一致性”,以简化系统的行为。所谓“极高程度的一致” 这一概念, 作者为其进行了精确的定义, 将其称为 “Log Matching Property”

  • Log Matching Property
    • 如果两份 log 在相同的 index = n 位置的日志记录的 term 值也相同, 那么这两份 index = [0…n] 的所有日志记录内容都完全一致。

上面这个目标看起来挺难实现的, 我们可以将其分解为两个子目标分开解决

  • Log Matching Sub Property 1
    • 如果两份不同机器上的日志上任意一条记录拥有相同的 index 和相同的 term 值, 那么他们存储的指令内容也是一样的。
  • Log Matching Sub Property 2
    • 如果两份不同机器上的日志上任意一条记录拥有相同的 index 和相同的 term 值, 那么他们之前的日志内容也是完全一样的。

由于一个 leader 在一个特定的 term , 特定的 index 位置至多只会创建 1 条日志记录且 leader 不会修改已经创建的日志索引, 第一个子目标Log Matching Sub Property 1 可以确信已经实现。

第二个子目标的实现需要我们引入一种简单的一致性检查机制:

  • leader 在发送 AppendEntriesRPC 调用时, 参数中除了携带准备进行 append 操作的 e n t r y entry entry , 还需要额外将紧挨着 e n t r y entry entry 的前一条记录 e n t r y p r e v i o u s entry_{previous} entryprevious 的 index 和 term 值一并发送出去。
    • 例如当前需要 Append 的日志记录 index 为 5 , term 为 2, 那 leader 发送的 AppendEntriesRPC 调用中就会携带 previous_entry_index =4previous_entry_term = 2 这两个信息。
  • follower 收到 AppendEntriesRPC 调用后, 就可以根据RPC 携带的 e n t r y p r e v i o u s entry_{previous} entryprevious 的信息校验自己对应位置的数据是否和 e n t r y p r e v i o u s entry_{previous} entryprevious 一致。 如果不一致, 它就可以拒绝执行 AppendEntriesRPC

添加上述的校验机制后, 我们可以用它完成归纳法证明:

  • 假设两份日志初始状态满足 Log Matching Property, 那么日志追加时刻的校验机制就可以持续保证 Log Mactching Property 始终成立。

笔者: 此处的归纳法证明稍微有点简略,如果整个过程中只有一个 leader 那么这个归纳法证明显然成立,但是当 leader 发生过变化且一个 leader 宕机前发出的多个 AppendEntriesRPC 调用可能部分存在执行失败情况时, 我们就没那么确定上述归纳证明的正确性是否依旧成立。

笔者这里通过反证的策略来验证一个场景:例如我们担心出现有两份日志中 index = 5 处有两条 term 均为 y 的记录,他们 index =4 的记录内容一致, term 均为 x, 但是 index = 3 的记录一个 term 为 a, 另外一个 term 为 b。

此时就可以将问题追溯到这两份 log 中 index = 4 的记录是怎么产生的, 分如下 2 种情况推理:

  • [index = 4, term=x] 的这两条记录是一个 leader 发起的 AppendEntriesRPC 产生的

    • 由于一个 leader 在一个特定的 y 中, 至多只会产生一条 index = 4 的日志记录, 所以如果该 leader 发出的 AppendEntriesRPC 的调用必然是针对它本地 log 中的同一条记录, 其中携带的前一条记录的 index 和 term 值一定是相同的。 所以日志追加时刻的检查规则必然不会允许 index =3 处的记录 term 值不同。该可能被排除
  • [index = 4, term=x] 的这两条记录是不同 leader 发起的 AppendEntriesRPC 产生的

    • 由于一个 term 中至多只会产生一个 leader, 所以两个 leader 分别发送的 AppendEntriesRPC 产生的的记录, 其 term 值必然不相等, 该可能性也被排除。
  • 由于上述两种可能均被排除, 所以担心的场景被验证是不存在的。 其他类似的场景, 读者也可以按照相同方法进行推理验证


考虑一下, 系统何时会发生 AppendEntriesRPC 调用由于未能通过检查规则而调用失败 ? 当 leader 发生过多次更替时, 可能出现下图的情况。

在这里插入图片描述

注意,每条日志记录中的数字代表的是该条指令记录的 term 值, 指令的具体内容并没有在上图中展示。 基于我们之前提到过的 Log Matching Sub Property 1 , 我们可以知道, index 相同且 term 值也相同的两条日志记录内容必然也相同, 所以上图中不必展示指令内容, 我们也知道哪些日志记录内容是一样的, 对于上图而言, 同一索引处, 相同颜色, 相同 term 值得日志记录内容必定一样。

上图中第一行是当前 leader 机器所保存的 Command Log 状态, 该 leader 的当前任期 term 是 8 。 在它刚刚竞选成功后, (a)- (f) 的日志状态均有可能出现在它的 follower 中。

  • (a)(b) 两种情况很好理解, 当 leader 的 AppendEntriesRPC 还没有被 follower 成功执行时就可能出现, leader 自身的日志状态完全可以领先 follower, 因为 leader 只要收到客户端发来的指令就可以在自己的 log 中执行 AppendEntry 操作, 之后再对 follower 发起并行的 AppendEntriesRPC 调用。
  • (c)(d) 的情况也不难理解, 当 term 为 6 的 leader 存活时, 它可能向 (c)(d) 所处的机器成功执行了 AppendEntriesRPC 操作追加了多条记录, 但是这些 AppendEntriesRPC 的操作可能并没有在当前 term 为 8 的 leader 所在机器上成功执行, 然后 term 为 6 的 leader 宕机了。
  • (e) 的情况可能发生于 term 1 的 leader 宕机后, 新的 leader 在 term 4 成功向该 follower 追加了 4 条日志之后宕机, 但是其中后2条记录没有成功在其他机器上追加成功, term5-term7 的 leader 出现后, 都没有与 e 所在的机器成功通信, 在其他机器上追加了不同的记录后又宕机了
  • (f) 的情况可能发生于, f 所在的机器成为了 term 2 的 leader ,然后在自己的日志中追加了 3 条日志后宕机了, 然后快速重启又成功当选 term 3 的 leader, 继续追加了 5 条记录后又宕机了。

现在我们考虑一下, term 8 的 leader 如何应对上图可能出现的诸多不一致情况。 在 Raft 的设计中, Leader 解决上述不一致问题的策略如下:

  • leader 会强制用自己的 log 内容去覆盖 follower 的日志内容
    • 此处机智的读者应该担心这种覆盖如何确保不会覆盖掉已经被 commited 的指令内容。 后文会详细探讨这个问题
  • 为了让 follower 的日志记录和自己的日志保持一致, leader 必须先找到 follower 日志中最早发生差异的位置, 从该位置删除掉后续的所有记录, 然后把自己在那个位置之后的日志记录内容都发给 follower。
  • 上面提到的日志最早差异点的搜索过程可以通过 AppendEntriesRPC 的检查机制完成
    • Leader 会维护每一个 follower 当前需要追加日志的位置next_indexi[i], i= 1...n, 当 leader 刚刚竞选成功时, 会将所有的 next_index 值初始化成自己 log 里最后一条记录的下一个 index 值, 例如上图中的 leader 就会将所有的 next_index 初始化为 11。然后发起 AppendEntriesRPC 的目标索引位置都填写为 next_index, 如果被拒绝,就对 next_index 递减后进行重试, 这样最终就会找到 leader 和 follower 日志中最早的差异点, 然后 AppendEntriesRPC 执行成功后, follower 和 leader 不一样的记录就会被逐个替换掉。
    • 如果觉得上述从 log 末尾逐步向前重试迭代的日志追加方式太低效, 我们可以规定 follower 拒绝 AppendEntriesRPC 时, 返回未能通过校验的日志记录所在的 term 以及该 term 内第一条日志记录所在的索引, 这样 leader 下一次重试 AppendEntriesRPC 时, 就可以直接将next_index 参数的值指向出现校验失败 term 的第一条日志记录位置, 并且一次 RPC 调用就发送一整个 term 的日志记录, 而不是每次调用只 append 一条。 不过 Raft 作者怀疑实践中这样做的必要性, 毕竟真实场景下, 出现不一致数据的概率并不高。

通过以上描述的日志管理机制, 一个 leader 在竞选成功后, 并不需要专门采取任何特殊动作来修复可能存在的 follower 与 leader 间的日志不一致问题, 它只需要正常的发起 AppendEntriesRPC 并在失败时进行重试就可以确保 leader 与 follower 的日志最终收敛一致。

注意, 在整个系统运行的过程中, leader 是永远不会覆盖或者删除自己本地的日志记录的。

这种日志分发策略实现了我们系统的一些设计目标: 只要有半数以上的结点可以正常运行, 那么整个系统就可以正常的接收、分发、 执行客户端发来的指令。 由于 leader 只要成功将收到指令记录分发复制到半数以上的集群结点后,就可以判定它处于 “committed” 状态予以执行, 个别运行缓慢的 follower 并不会拖累整个系统的运行速度。

如何保障算法的安全性(Safety)

前面两个小节已经分别描述了一开始提出的3个问题中的两个

  1. 如何选举 leader
  2. leader 如何复制分发日志

下面就进入最有难度的第三个问题, 如何证明上述算法可以在一切异常情况下, 都能实现算法所定义的安全性
在这里插入图片描述
这里我们先看一下最为关键的 State Machine Security 的定义

集群中的每个状态机自己的指令 log , 如何确保其中一台状态机 machine A 一旦执行了其中第 N 条指令, 其他任意一台状态机执行的第N 条指令内容必定和 X 一样。

遗憾的是, 之前所设计的 leader 选举机制和 command log 分发机制还不足以确保每台状态机都能以相同的顺序执行相同内容的指令。例如, 一个 leader 向集群内半数以上的机器成功执行了 AppendEntriesRPC 操作, 认为在 index = n 位置追加的记录 e n t r y A entry_A entryA 已经处于 commited 状态, 执行了 e n t r y A entry_A entryA , 此时 leader 发生了宕机, 然后集群中那个唯一没有成功收到 e n t r y A entry_A entryA 的 follower 成功竞选为了 leader , 此时它所执行的 index = n 的记录就可能和 e n t r y A entry_A </

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值