Raft协议

2019年01月16日

论文地址

寻找一个可理解的一致性算法:https://raft.github.io/raft.pdf

0. 摘要

Raft是一个管理复制日志的一致性算法。它产生的结果和(multi-)Paxos一样,并且和Paxos一样高效,但是它的结构和Paxos不同。这使得Raft比Paxos更加易于理解,也为构建实际的系统提供了更好的基础。为了增强可理解性,Raft分离了一致性的核心要素,例如 领袖选举(leader election),日志复制(log replication),安全性(safety),并且它强制使用了一个更大程度的一致性 去减少必须考虑的状态数量。结果证明,Raft比Paxos更加容易学习。Raft还包含一个改变集群成员的新机制,使用票数重叠来保证安全性。

1. 介绍

一致性算法允许一系列机器作为一个整体组工作,并且在其中某些成员失败时幸存下来。因此,它们在构建可靠的大规模软件系统中扮演者核心角色。在过去十年间,Paxos住到了一致性算法的讨论:大多数一致性的实现都是基于Paxos或者被Paxos所影响,Paxos成为了向学生传授一致性的主要工具。

不幸的是,尽管多次尝试让Paxos更加平易近人,但是它仍然非常难以理解。此外,它的架构要求复杂的变化来支持实际的系统。结果就是学生和系统构建者都在于Paxos斗争。

在我们自己与Paxos挣扎之后,我们开始去寻找一种新的一致性算法,它可以为教育和系统构建者提供更好的支持。我们的方法是不同寻常的,我们的主要目标是它的易懂性:我们是否可以为实际系统定义一个一致性算法,并且用一种明显比Paxos简单的方式描述它?此外,我们希望此算法可以帮助系统构建者进行开发。这不仅对算法起作用很重要,对它起作用的原因也很明显。

这个一致性算法叫做:Raft。在Raft的设计中,我们使用了特殊的技巧来改善它的可理解性,包括分解(Raft分离了领袖选举,日志复制,和安全性) 和 状态空间缩减(相比于Paxos,Raft减少了不确定性的程度,并且服务器间的不一致性也会降低)。通过两所大学43个学生的调研,显示出Raft明显比Paxos更加易于理解:在学习两个算法之后,33名学生回答的关于Raft的问题都好于Paxos的问题。

Raft在很多方面斗鱼现有的一致性算法相似(尤其是和 Oki and Liskov 的视图复制)。但是还是有一系列不通的方面:

  • 强势领袖(Strong leader):Raft使用了比其他一致性算法更强的领袖模式。例如,日志项智能从leader流向其他服务器,这样简化了复制日志的管理,并且让Raft更易于理解。
  • leader选举(Leader election):Raft使用随机的时间来进行leader选举。这只是在任何一致性算法都需要的心跳机制上增加了一点内容,但是却简单高效的解决了冲突。
  • 成员变化(Membership changes):raft对变化的机制是在集群服务器转变过程中对两个不同的配置使用一个新的联合一致性(joint consensus)方法。这允许集群在配置变化期间也能持续进行正常操作。

我们相信Raft在教育和实现基础方面都是是一个比Paxos和其他一致性算法更加优秀的协议。它比其他算法更加坚定和易于理解。他的描述完整满足实际系统的需要,它有几个开源的实现,并在一些公司中使用。它的安全性已得到正式的规定和验证,而且其效率也是可与其他算法媲美的。

这个论文剩下的部分会介绍复制状态机问题(第二部分),讨论Paxos的优势和劣势(第三部分),描述我们理解能力的一般方法(第4部分), 提出Raft一致性算法(第5-8部分),评价Raft(第9部分),讨论相关的工作(第10部分)。

2. 复制状态机(Replicated state machines)

一致性算法通常会在复制状态机(Replicated state machines)环境中出现.在这个方法中,状态机在一组服务器集合上计算相同相同的一致副本,即使一些服务器下线科通继续操作。

复制状态机常用来在分布式系统中解决一系列容错问题。例如,拥有单独集群leader的大规模系统(GFS,HDFS,RAMCloud等),它们通常使用隔离的状态机去管理leader选举和存储leader崩溃后恢复所必须的状态信息。复制状态机的例子包括 Chubby 和 ZooKeeper。

复制状态机通常使用如图1所示的复制日志来实现。每个服务器存储一个包含一系列命令的日志,状态机按照顺序执行它们。每个日志包含相同顺序的相同命令,所以每个状态机处理的命令序列是一致的。由于每个状态机是确定的,所以相同状态的计算会产生相同的输出序列。

保证复制日志的持久化是一致性算法的任务。服务器上的一致性模块(consensus module)收到从客户端来的命令,并将它们加到本身的日志中。它会和在其他服务器上的一致性模块进行通讯,保证即使有服务器宕机,每个日志最终都包含相同顺序的相同请求。一旦命令被合适的进行了复制,每一个服务器的状态机都会按照日志的顺序处理,并将结果返回给客户端。这些服务器近似形成了一个单独的,高可靠的状态机。

实际系统的一致性算法通常还有以下的特性:

  • 保证在所有的非拜占庭条件下(包括网络延迟、网络分区和丢包)安全的复制和记录(绝不返回错误结果)。
  • 只要大多数服务器是可用 并且可以相互之间可以通讯的,它们就是完全可用的。因此,一个5台服务器的集群可以容忍任何两台服务器的失败。假设服务器是因为停机而失败的,它们可能稍后会从稳定存储的状态中恢复,并重新加入集群。
  • 它们不会依靠时间来保证日志的一致性:错误的时钟和极端的消息延迟,最坏情况下会导致可用性问题。
  • 在常见情况中,只要集群中大多数服务器都进行了响应,那命令就是可以完成的。少数较慢的服务器不会影响系统的整体性能。

3. Paxos的问题(What’s wrong with Paxos?)

最近十年,Leslie Lamport 的Paxos协议几乎变成了一致性的同义词:它是在课堂最常教授的协议,是一致性协议的最常见实现。Paxos首先定义了一个通过单个决策能够达成一致的协议,例如单独的复制日志项。我们把这个子集称为单决策(single-decree)Paxos。然后Paxos结合多个此协议的实例来加快一系列的决策,例如日志,这个叫multi-Paxos。Paxos确保了安全性和活跃性,并且它支持集群成员的变化。它的正确性已经被证明了,而且在通常情况下也是非常高效的。

不幸的是,Paxos有两个明显的缺点。第一个缺点就是它非常难以理解。Paxos的完整解释【15】是出了名的难懂,只有少数人通过极大的努力成功的弄懂了它。所以有了一些简单的说法来尝试解释它的核心【16,20,21】。这些都是聚焦在单决策子集(single-decree),但是仍然具有一定的挑战性。在DSDI 2012的一次调查中发现,即使是经验丰富的研究员,也只有少数人对Paxos满意。在阅读了几个简单的解释并设计了我们自己的alter-native协议之前,我们无法理解完整的协议,这一过程花费了将近一年的时间。

我们假设Paxos的不透明度是因为选择将单决策子集作为它的基础。单决策Paxos是密集而精妙的:它分为两个阶段,没有简单直观的解释,不能独立理解。很难对单决策协议的工作原理有直观的解释。multi-Paxos在这基础上增加了一些额外的复杂度和微妙之处。我们相信多决策下一致性的整体问题可以分解成更加直接和明显的方式。

Paxos的第二个问题是没有为构建实际系统实现提供一个好的基础。一个原因就是multi-Paxos没有一个广泛人认同的算法。Lamport的描述主要是关于单决策的Paxos,他只是大致描述了一下multi-Paxos,但是大量的细节都是缺失的。这里有一些尝试充实和优化Paxos的论文,例如【26,39,13】,但是它们之间都是不同的,和Lamport的描述也不相同。例如 Chubby【4】这样的系统拥有一个类Paxos的算法,但是里面有大量细节都没有公布(尽管它最终表明领导能力的弱形式是一种性能优化)。在只有一个决策需要处理的简单世界中,这是有道理的,但是很少有系统会使用这种方法。如果有一系列的决策需要处理,更加简单和快速的方式是选出一个leader,然后由leader来进行决策。

因此,实际系统和Paxos几乎没有相似之处。每个由Paxos开始的实现,在实现过程中总是会发现一些不同,然后发展成一个明显不同的架构。这是非常耗时且容易出错的,对Paxos的不同理解也加剧了这个问题。Paxos的公式通过定理在证明它的正确性是简单的,但是实现Paxos如此困难也证明了它没什么价值。下面这个对Chubby实现的评论就是典型:

在真实世界的系统与Paxos算法的描述之间有明显的差别。。。最终的系统会基于一个未被证实的协议【4】

因为这些问题,我们完成了Paxos对系统构建或者教育没有提供良好支撑的问题。考虑到一致性在大规模软件系统中的重要性,我们决定设计一个比Paxos更好的替代算法,Raft就是实验的结果。

4. 为可理解性设计(Designing for understandability)

我们设计Raft,有几个目标:它必须为系统构建提供完整和实际的基础,为开发者显著减少设计工作;它必须再所有条件下都是安全的,在典型的操作条件下是可用的,并且它对普通操作必须是高效的。但是我们最重要,也是最困难的挑战是 - 易懂性。它必须尽可能简单的让大量听众都明白它。此外这个算法要尽量直观,让系统构建者在实际实现中可以进行不可避免的扩展。

在Raft的设计中,有需要要点,我们必须从大量备选方案中进行选择。在这些备选方案中我们考虑的基础就是易懂性:解释每种备选方案的困难程度(例如,状态空间有多复杂?有没有隐含的微妙的含义?),让读者完全明白这个方法含义的难易程度?

我们意识到这种分析具有高度的主观性。尽管如此,我们使用了两种普遍的技术。第一个技术是众所周知的问题分解:我们会尽可能的将问题拆分成相互独立的,可以被解决,解释和理解的一系列问题。例如,在Raft中我们分成了leader选举,日志复制,安全性,和成员变化。

我们的第二个方法是通过减少需要考虑的状态数来简化状态空间,使系统更加连贯,尽可能消除不确定性。具体来说,日志不允许出现洞(holes),Raft限制了日志可以变成不一致的方式。虽然我们尽可能的消除不确定性,但是在一些情况里,不确定性实际上提高了可理解性。特别是,随机方法引入了不确定性,但是它们倾向于通过类似的方式处理所有可能的选择,来减少状态空间(随便选一个,选择哪一个并不重要)。我们使用随机来简化Raft的leader选举算法。

5. Raft一致性算法(The Raft consensus algorithm)

Raft是一个用来管理在第二部分中描述的复制日志的算法。图2对算法做了一个总结,仅供参考。图3列出了这个算法的关键特性。这些图的的元素将在本章的余下部分分别讨论。

图二

  1. 状态(State):

1.1. 在所有机器都持久化的状态(在rpc调用返回前更新稳定存储):

  • currentTerm: 服务器看到的最新日志项id(首次启动初始化为0,单调递增)
  • votedFor: 当前收到的选举候选人编号(如果没有则为null)
  • log[]: 日志项集合,每一项都包含状态机需要的命名,以及leader收到日志项时的日志id(论文中这里是term,感觉翻译成id好理解些)。首次为1.

1.2. 在所有服务器上一遍的状态:

  • commitIndex:提交的日志项已知的索引(初始化为0,单调递增)。
  • lastApplied:状态机已执行的日志项的最高索引(初始化为0,单调递增)

1.3. 在leader节点上易变的状态(在重新选举后会重新初始化):

  • nextIndex[]: 下一个需要发送给每个从节点的日志项索引(初始化为leader最后一个索引+1)
  • matchIndex[]: 每个从节点复制的最高日志项索引(初始化为0,单调递增)。
  1. 请求投票调用(RequestVote RPC)

2.1. 参数

  • term: 日志id
  • condidateId:请求投票的候选人id
  • lastLogIndex:候选人最新的日志项索引(5.4小节)
  • lastLogTerm:候选人最新日志项(5.4小节)

2.2. 返回结果

  • term: 当前项,用于候选人更新自身
  • voteGranted:true表示候选人收到一次投票

2.3. 接收者实现:

  • 如果 term < currentTerm 返回false (5.1 节)
  • 如果voteFor为null,并且候选人的最新日志项与接收者最新日志一样,则授予投票(5.2节,5.3节)
  1. 追加日志调用(AppendEntries RPC),leader用于日志复制和心跳

3.1 参数

  • term: leader的term
  • leaderId:让从节点重定向客户端
  • prevLogIndex:上一个新日志的日志所有
  • prevLogTerm:prevLogIndex对应的日志项
  • entries[]: 需要存储的日志项(心跳请求为空,为提高效率可能会发多个)
  • leaderCommit:leader的commitIndex

3.2. 结果:

  • term: 当前项,用于leader更新自身数据
  • success:如果从节点匹配上了preLogIndex和preLogTerm,则为true

3.3 接收者实现:

  • 如果 term < currentTerm 则返回false(5.1节)
  • 如果日志不包含 preLogIndex 或者 与 preLogTerm 不匹配,则返回false (5.3节)
  • 如果新的日志项和已经存在的日志项发生了冲突(相同索引,不同内容).则删除已经存在的日志项,并覆盖。(5.3节)
  • 追加所有新增的日志项
  • 如果 leaderCommit > commitIndex,则更新 commitIndex=min(leaderCommit,最新日志项的索引)
  1. 服务器规则(Rules for Servers)

4.1 所有服务器:

  • 如果 commitIndex > lastApplied: 增加lastApplied,在状态集中执行log[lastApplied] (5.3节)
  • 如果RPC请求或者返回的term T > currentTerm: 设置 currentTerm=T,并转化给从节点(5.1节)

4.2 从节点(5.2小节):

  • 应答候选人和leader的RPC请求
  • 如果选举超时,没有收到 从当前leader节点发来的AppendEntries调用,也没有投票给候选人:将自身转换为候选人

4.3 候选人:

  • 转化为候选人后,开始选举:
    • 增加 currentTerm
    • 投票给自己
    • 重置选举计时器
    • 发送RequestVote请求给所有服务器
  • 如果收到了大多数服务器的投票,则变为leader
  • 如果收到了新的leader的AppendEntries请求,则转换为从节点
  • 如果选举超时,则开始新一轮投票

4.4 Leaders:

  • 当选后:发送空的AppendEntries初始化请求(心跳)给其他服务器;在空闲期间重复此操作以预防选举超时
  • 如果收到来自client端的命令:追加到本地日志中,在状态机应用之后应答(5.3小节)
  • 如果对从节点来说最新日志的 index>=nextIndex:通过AppendEntries发送从nextIndex开始的日志给从节点。
  • 如果成功:更新从节点对应的nextIndex和matchIndex
  • 如果由于日志不一致导致AppendEntries失败了,则减少nextindex并重试
  • 如果存在一个N满足N>commitIndex,大多数matchIndex[i]>=N,并且log[N].term == currentTerm,则设置 commitIndex=N (5.3和5.4小节Â)

图二:根据Raft一致性算法做的简明摘要(不包括成员改变和日志压缩)。第一点总描述的是服务器独立且不断重复的规则的行为。在5.2小节中讨论了常规特性。在【31】中更加规范的描述了算法。

图3

  • 选举安全性(Election Safety):在所有term中至多只有一个leader(5.2小节)
  • leader只可追加(Leader Append-Only): leader节点绝不会覆盖或删除日志项,只会追加日志项(5.3小节)
  • 日志匹配(Log Matching): 如果两个日志具有相同的所有和内容,则在索引中完全相同的。(5.3小节)
  • leader完整性(Leader Completeness): 如果日志提交到了一个term找那个,则它会出现在所有编号比这个term打的日志中。(5.4小节)
  • 状态机安全性(State Machine Safety): 如果一个服务器在一个索引上提交了一个日志给状态机,则其他机器绝不会再相同索引上提交不同的日志项。(5.4.3小节)

图3:Raft在任何时间都保证上面这些属性。后面的章节是这些讨论这些属性的地方。

Raft实现一致性通过 先选出一个 leader,然后给与这个leader完整能力去管理复制日志。leader接收客户端的日志项,将它们复制到其他服务器,然后告诉服务器在安全的时候在它们的状态机中执行。拥有一个leader可以简化复制日志的管理。例如,leader可以决定在什么位置放置新的日志项,不用询问其他服务器,并且数据以简单的方式从leader流向其他服务器。leader可能会失败或者与其他机器不可联通,这会造成新leader的选举。

通过leader这种方式,Raft将一致性问题分解成三个相对独立的子问题,它们分别由以下的子章节进行讨论:

  • leader选举:当当前的leader失败时,必须选出一个新的leader(Section 5.2)
  • 日志复制:leader不许接收从客户端来的日志,并复制到集群中,强制其他日志与其一致。(Section 5.3)
  • 安全性:Raft的安全特性即使图3中的状态机安全性:如果任何一台机器将一个常规日志项应用到了它的状态机上,那么在其他机器在相同的日志索引位置不会出现不同的命令。5.4节描述了Raft是如何保证这个特性的,它在5.2节中描述的选举机制中加入了额外的限制。

在介绍了一致性算法之后,本节将讨论可用性问题以及定时在系统中的作用。

5.1 Raft基础(Raft basics)

一个Raft机器包含一组服务器,5个是一个经典的数字,它可以容忍两台机器的失败。在任何时刻,每一个台机器都是以下三种状态之一:leader,follower 或者 condidate(候选人).在正常的一个操作里有且只有一个leader,其他的服务器都是从节点。从节点是被动的:它们自己不会发出请求,只会简单的应答leader会在候选人的请求。leader主节点处理所有的客户端请求(如果客户端与从节点进行通信,则从节点会重定向到主节点)。第三个候选人这个状态,是在5.2节中描述的新leader选举时使用的。图4描述了这些状态的装换,状态状态将在后续进行讨论。

Raft将时间分为任意长度的term(从后面的说明看,term可以理解为一个leader任期,从选举开始,直到任期结束),如图5所示,Terms是一些连续的整数。每个term从election开始,其中一个或多个候选人尝试成为leader节点。如果一个候选人赢得了选举,则这个服务器将在余下的term中作为leader节点服务。在一些情景下多个候选人会平分票数,这种情况下,term就会在没有leader的情况下结束。然后在短时间内会发起一次新的term(一次新的选举)。Raft保证在一个指定的term中最多只有一个leader。

在不同的服务器上可能会观察到不同时间的term转换,在一些情形下,服务器可能观察不到一个选举或者一个完整的term。在Raft中terms起着逻辑时钟的作用【14】,它允许服务器发现过期的信息,例如过期的leader。每个server都存储这 current term 数字,随着时间单调递增。在发送服务器间通讯是会交换当前的term;如果一个服务器的term比例外一个服务器的小,则它会将它的当前的term更新为较大值。如果候选人或者leader发现它自身的term过期了,则会立刻恢复为从节点状态。如果一个服务器收到了过期的term发来的请求,则会拒绝这个请求。

Raft服务器之间通讯使用远程过程调用(RPC),最基础的一致性算法只要求两个RPC,RequestVote在选举期间用来初始化(5.2节),AppendEntries在leader复制日志项给从节点以及发送心跳时使用(5.3节)。第7节增加了第三个RPC用来在服务器之间传输快照。如果服务器没有及时收到回复,会进行重试,并且使用并发调用RPC获得最佳性能。

5.2 Leader选举(Leader election)

Raft使用心跳机制来触发leader选举。当服务器启动时,先作为从节点开始运行。只要它收到来自leader和候选人有效的RPC,它就会一直保持从节点的状态。Leader周期性的发送心跳(使用AppendEntries发送没有日志项的请求)给所有从节点来保持它的权利。如果从节点超过时间周期没收到选举超时(election timeout)的通讯,则它会假设现在没有可用的leader了,并且发起一次新的选举去选择一个新的leader。

开始一次选举时,从节点会增加它当前的term,并转变为候选人状态。然后投票给自己并且并发发送RequestVote请求给集群中的其他机器。候选人状态一直持续到发生以下三件事情之一:(a)它赢得了这次选举。(b)另一个服务器证实了自己是leader。或者(c)时间周期内没有服务器赢得选举。下面的段落将分别讨论这些结果。

候选人在同一个term中如果收到了整个集群大多数服务器的投票,则赢得此次选举。每个服务器在一个给定的term中只会给一个候选人投票,基于先到先得(注意在5.4节中在投票时增加了额外的限制)。采用大多数人的规则确保了在一个常规的term中只会有一个候选人赢得选举(在图3中描述的选举安全性属性)。一旦候选人赢得了选举,它就会变成leader。然后它会发送心跳消息给所有的服务器以确保它的权利,以及防止生成新的选举。

在等待投票期间,候选人可能会收到来自另一个服务器要求成为leader的AppendEntries请求。如果leader的term(包含在RPC请求参数中)大于等候选人的当前term,则候选人认为这个leader是合法的,并且转为从节点状态。如果RPC中的term比候选人的term小,则候选人会拒绝这个RPC调用,并保持为候选人状态。

第三种可能的状态是 候选人在选举中即没有赢也没有输:如果大量的从节点同时变为了候选人,则可能不会有候选人能获得大部分投票。当这种情况发生时,每个候选人都会超时,并且通过增加term,然后发起新的一轮RequestVote调用。然而,没有额外的措施,前面发生的票数分割的情况可能会无限重复。

Raft使用随机选举超时来确保平分票数的请求是罕见的,使用这种方法来快速解决这个问题。为了预防票数分歧,选举超时时间是在一个区间内(例如150-300ms)随机选择的。这将分散服务器使得在大对数情况下只有一个服务器会超时,它会在其他服务器超时前赢得这次选举并发送心跳。在处理票数分割的时候也使用了同样的机制。每个候选人在重新开始一次选举前会随机等待一段时间。这回减少在新的一次选举时再次出现票数分割的可能性。9.3节显示出这种方式可以快熟选出leader。

选举就是一个例子,说明可理解性如何指导我们在设计方案之间进行选择。最初我们想使用排名系统:每个候选人拥有一个唯一的排名,用来和其他候选人竞争。如果候选人发现另一个候选人拥有更高的排名,它会转变为从节点,让更高排名的候选人更容易赢得下一次选举。我们发现这种方法有一个可用性上的小问题(如果更高排名的服务器失败了,低排名的服务器可能需要等待一个超时时间后再次成为候选人,但是如果处理太快了,它可能会重置leader的选举进度)。我们对这个算法做了好几次调整,但是每次调整后都会有新的问题出现。最终我们放弃了它,并使用了更加明显和易懂的随机重试方法。

5.3 日志复制 (Log replication)

一旦leader选举出来了,它就得开始为客户端请求提供服务。每个客户端请求包含由复制状态机执行的一个命令。leader将命令作为一个新项追加到自身的日志中,然后并发发送AppendEntries请求给其他服务器复制这个新的日志项。当日志项被安全的复制了(在下面有详细描述),leader会将日志项在自身的状态机用应用并返回执行结果给客户端。如果从节点崩溃、运行缓慢或者发生了网络丢包,leader会无限重复执行AppendEntries调用(即使在它已经应答了客户端之后),知道所有的从节点最终都存储了所有的日志项。

图6:日志由日志项组成,按顺序由数字标记。每个日志项包含创建它时的term(框中的数字)和一个命令。如果日志项安全的在状态机中被应用了,则可将其考虑为 已提交的。

日志如图6所示的进行组织。在leader收到一个日志项的时候,每个日志项存储着状态机命令和term序号。term序号用来在日志间发现不一致性,也同时保证图3中的一些特性。每个日志项还有一个整数索引来识别它在日志中的位置。

leader会决定何时安全的将日志项在状态机中应用,已应用的叫做已提交(committed)的日志项.Raft会保证素有已提交的日志项是持久化的,并且最终会在所有可用的状态机中应用。一旦leader创建了一个日志项并将其防护知道了大多数服务器上,它就是已提交的(例如图6中的日志项7)。也会同时提交此日志项之前的日志,包括前一个leader创建的日志项。5.4节讨论了一些在leader改变时应用这个规则的一些细节,同时也证明了提交的定义是安全的。leader会持续追踪被提交的它已知的最高索引,将其之后的AppendEntries请求中(包括心跳),是的其他服务器最终客户知道它。一旦从节点知道日志项是已提交的了,它就会在自身的状态机中应用它(按照日志顺序)。

我们设计Raft的日志机制在不同的服务器之间保持高度的一致。这不仅简化了系统的行为,使其更加可预测,同时它也是保持安全性重要的组成部分。Raft包含以下的特性,这些特性一起形成了在图3的日志匹配特性(Log Matching Property):

  • 如果在两个不同的日志包含相同索引和term的日志项,则它们存储的命令是一样的。
  • 如果不同日志中的两个日志项拥有相同的所有和term,则这俩日志项前的所有日志项都是相同的。

第一个特性遵循这样一个事实:一个给定的日志索引和term,leader至多只会创建一个日志项。并且日志项的位置绝不会改变。第二个特性由AppendEntries执行的简单一致性来保证。在发送AppendEntries请求时,leader会包含待加日志项的前一个日志项的索引和term,如果从节点在其日志中没有找到相同索引和term的日志项,则会拒绝新的日志项。一致性检查作为一个归纳步骤:日志的初始空状态满足日志匹配特性,并且一致性检查在日志扩展时保留日志匹配特性。因此当AppendEntries成功返回时,leader就知道从节点的日志和新日志项之前的日志是一致的。

在正常操作期间,leader和从节点的日志是保持一致的,所以AppendEntries的一致性检查不会失败。但是,leader崩溃会造成日志的不一致性(老的leader可能还有将日志完全复制给所有的从节点)。这个不一致性会在leader和从节点的一系列崩溃中加剧。图7举例说明了从节点的和新的leader节点日志不同的情况。从节点可能会缺少当前leader的日志项,还可能会包含当前leader不存在的日志项,或者两种情况都有。缺少和额外的日志项可能会跨越多个term。

图7:当leader刚赢得选举时,从节点可能出现a-f的情况。每个框代表一个日志项,框里面的数字是它所属的term。从节点可能缺少日志项(a-b),可能包含额外未提交的日志项(c-d),或者两种情况都有(e-f)。例如,term 2所属的leader增加了一些日志项到自身的日志中,在未进行提交时崩溃了,然后它快速重启,变成了term为3的leader,并增加了更多的日志项到它自身的日志中,在term 2 或者term 3提交前,服务器再次崩溃,并错过了好几个term,则可能出现情况f。

在Raft中,leader通过强制从节点复制它的日志的方式来处理不一致性。这意味着在从节点有冲突的日志项会被leader节点的日志项覆盖。在5.4节中会说明,只需再增加一个限制,这样做就是安全的。

为了让从节点的日志和leader保持一致性,leader节点需要找到两份日志相同的最后一个日志项,删除从节点在这一项之后所有日志项,并且将这个点之后的所有日志项都发送给从节点。所有这些操作都是为了响应AppendEntries RPC执行的一致性检查而发生的。leader我每个从节点都保存了一个nextIndex,它是leader下一个应该发送给从节点的日志项索引。当leader首次当选时,所有的nextIndex都会初始化它最新日志项的下一个索引(在图7中是11)。如果从节点日志项和leader的不一致,则下一次AppendEntries请求的一致性检查就会失败。在请求被拒绝之后,leader会减小nextIndex并重试。最终nextIndex会到达leader节点和从节点匹配的那个日志项所在的索引。然后AppendEntries就可以成功了,它会移除所有冲突的日志项并将leader节点的日志复制给从节点(如果有的话)。一旦AppendEntries执行成功了,从节点的日志也就和leader节点的日志一致性了。并且在整个term剩余时间内会保持这种方式。

如果需要,可以通过减少AppendEntries拒绝的次数来优化这个协议。例如,当发生AppendEntries请求拒绝的时候,从节点可以返回发生冲突的日志项所在的term,以及这个term的第一个日志项索引。通过这个信息,leader节点可以避开在整个term中所有冲突的日志项来减小nextIndex,这样每个冲突的term只需要一个AppendEntries调用,而不是每个冲突的日志项都需要一个AppendEntries调用。在实践中,我们十分怀疑这个优化的必要性,因为失败很少发生,而且不太可能有许多不一致的尝试。

使用这种机制,当leader节点刚选举出来时,它不用任何额外的操作来修复日志的一致性。只需要在正常的操作中,通过AppendEntries的一致性检测自动修复这个问题。leader节点绝不会覆盖或者删除它自身的日志(在图3中所说Leader的日志追加特性)。

日志复制及制作表现出来的一致性特性,如第2章的描述:只要大多数服务器是正常的,Raft可以接受,复制,以及应用日志项。在正常的情形下,新的日志项会在一轮RPC调用中复制到集群的大多数机器上,运行较慢的机器不会影响性能。

5.4 安全性(Safety)

前面描述了Raft如果做leader选举和复制日志项。但是到目前为止的机制描述,还不能充分的保证每个状态机都按照相同的循序执行相同的命令。例如,在leader提交几个日志项的时候,有个从节点可能是不可用的。然后选举除了另外一个新的leader将这些日志项覆盖了,结果就造成了 不同的状态机肯能会执行不同的命令序列。

这一章将通过增加leader选举的一个限制来完整Raft算法。这个限制确保leader节点包含任意一个term在之前提交的所有日志项(图3中Leader的完整性特性)。通过这个限制,我们会使得提交规则更加清晰。最终,我们会提出Leader日志完整性特性的证明,并且展现出leader复制状态机的正确行为。

5.4.1 选举限制(Election restriction)

在任何基于leader的一致性算法中,leader必须最终存储所有已提交的日志项。在一些例如 ViewStamped Replication【22】的一致性算法中,leader即使没有包含全部的已提交日志项也可被选举为leader。这些算法包含一些额外的机制去确认缺少的日志项并在选举期间或者选举之后将其传输到新leader中。不幸的是,这会导致额外的机制和复杂性。Raft使用了一种更加简单的方式,即保证选举出来的新leader必须包含所有已提交的日志项,这样就无需将这些日志项传输给leader了。这也意味着日志项的流动是单向的,即从leader节点流向从节点。并且leader绝不会覆盖它自身已经存在的日志项。

Raft使用投票过程来预防出现未包含所有日志项的候选人赢得选举。候选人为了赢得选举必须和集群中大多数机器通讯,也就意味着这些机器中至少有一台包含了所有已提交的日志项。如果候选人的日志至少和大多数机器一样新,则它必然包含所有已提交的日志项。RequestVote里实现了这个限制:PRC中包含候选人日志的信息,如果它自身的日志比候选人的新,则它会拒绝投票。

Raft通过比较两个日志中最后一个日志项的index和term来决定哪个日志更新。如果最后一个日志项属于不同的term,则term更大的日志更新。如果是同一个term的,则日志更长的日志更新。

5.4.2 从之前的term提交日志项(Committing entries from previous terms)

如5.3章中描述,如果一个日志项在大多数机器上保存了,则leader会认为当前term的这个日志项是已提交的。如果leader在提交一个日志项过程中崩溃了,下一个leader会尝试完成这个日志项的复制。但是,leader不能立即推断出前一个term的日志项被大多数服务器存储了就是已提交的(committed)。图8展示的就是这样一种情况,老的日志项已经被存储在大多数机器上,当时它仍然可能被下一个leader覆盖。

图8:上面的时间序列展现出为什么leader不能由于一个日志项是来自于老的term就断定它是已提交的原因。在(a) 中 S1 是leader,并且部分复制了索引index为2的日志项,在(b)中S1奔溃了,S5使用term 3赢得了S3和S4的投票成为了leader。在(c)中S5奔溃了,S1重启,并且被选为了leader,并且继续进行复制。在这个点上,来自term 2的日志被复制到了大多数服务器上,但是它是未提交的。如果在(d)时S1崩溃,S5被选为了leader(通过赢得S2,S3和S4的选票),并且使用了它自身的term 3中的日志项覆盖了其他机器的日志。但是,如果S1在奔溃前将当前term 4中的日志项复制到了大多数机器上,则类似于(e)中,那这个日志项是已提交的(S5不可能赢得选举).在这个点上日志里所有之前的日志也都是已提交的。

为了排除类似于图8中问题,Raft不会通过复制数量来提交前一个term的日志项。只有当前leader的当前term提交的日志项是用复制数量来判断是否提交的;一旦当前term的日志项通过这种方式提交,由于日志匹配特性(Log Matching Property),那么先前的日志项都是已提交的。某些情况下leader可以安全的推断是老的日志项是已提交的(例如,日志项已经被存储在所有的服务器上),但是为了简单起见,Raft使用了更加保守的方法。

当leader从上一个term中复制日志项时,由于日志项包含了它们原始的term编号,所以Raft在提交规则里引入了额外的复杂性。在其他的一致性算法中,如果新的leader从上一个term中重新复制日志项,它会给它赋一个新的term编号。Raft的方法可以更加简单的解释日志项,因为它们在整个日志生命周期中包含了相同的term编号。此外,Raft的新leader比其他算法的会发生更少的日志项(其他的算法在日志项变为已提交之前会发送多余的日志项给它们重新编号).

5.4.3 安全性讨论(Safety argument)

考虑到完整的raft算法,我们现在可以更精确地说明leader完整性属性(这个讨论基于安全性证明,9.2章)。我们先假设leader完备性不成立,然后在证明它的矛盾。假设term T对应的leader (leader t)在它的iterm中提交了一个日志项,但是这个日志项没有被之后term对应的leader所存储。考虑最小的term U > T ,这个leader U没有存储这个日志项。

  1. Leader U 在其选举时肯定是没有这个日志项的(Leader绝不会删除或者覆盖自身的日志项)。
  2. leader T将日志项复制到了集群的大多数机器上,并且leader U收到了来自集群中大多数机器的投票。因此,至少有一个服务器(投票者)接收了来自leader T的日志项,并且投票给了leader U,在图9中,这个投票者是达成矛盾的关键。

  1. 投票者在投票给leader U之前,接收了来气leader T的已提交日志项,否则,它会拒绝来自leader T的AppendEntries请求(此时它当前的term会比T大)。
  2. 假设当投票者投票给leader U时,leader U是存储着这个日志项的,因为每一个介入的leader都包含这个日志项(假设),但是leader绝不会移除日志项,而从节点只会在它和leader发生冲突时移除日志项,这样它们又发生了矛盾。
  3. 投票者将它的票投给了leader U,所以leader U的日志至少要和投票者的日志一样新,这导致了矛盾。
  4. 首先,如果投票者和leader U共享最新的同一个term的日志项,那leader U的日志必须至少和投票者的日志一样长,所以leaderU的日志包含每个投票者的日志项。这也是冲突的,因为我们的假设是投票者包含这个已提交的日志项,而leaderU是不包含的。
  5. 除此之外的情况,leader U的最新日志项的term必须大于投票者的term,也即是它要大于T,因为投票者的最新日志项的term至少是T(它包含来自termT已提交的日志项)。假设这个已提交的日志项是由早于U的leader创建的。那么,由于有 日志匹配特性,leader U的日志项也必须包含这个已提交的日志项,这也是矛盾的。
  6. 这样矛盾也就结束了。因此所有大于T的term必须包含所有在T中已提交的日志项。
  7. 日志匹配特性保证了之后的leader也会包含间接提交的日志项,例如图8(d)中索引为2的日志项。

给出了Leader完整性特性,我们可以证明图3中状态机安全特性,如果一个服务器将一个日志项在它自身的状态机中应用,那么其他服务器不可能在相同索引的位置应用一个不一样的日志项。在服务器将日志项应用到其状态机时,它的日志肯定是与leader日志一致的,并且这个日志项是已提交的。

5.5 从节点和候选人节点崩溃(Follower and candidate crashes)

在这之前我们主要关注的是leader节点的失败,从节点和候选人节点的奔溃比leader节点的崩溃处理起来简单的多,它们的处理方式是一样的。如果从节点和候选人节点崩溃,则后续发送给它的RequestVote和AppendEntries请求都会失败。Raft采用无限次重试来处理这种失败。如果崩溃的节点重启了,这些RPC就会成功。如果服务器在收到请求并处理完成但是返回之前奔溃了,则当它重启时会再次收到一个一样的请求。Raft的RPC都是幂等的,所以即使收到重复的请求也是无损的。举例来说,如果从节点收到的AppendEntries请求里包含了它已经有的日志项,这些在新请求中的日志项就会被忽略。

5.6 计时和可用性 (Timing and availability)

我们对Raft的要求之一就是安全性不能依赖于时间:系统不能因为一些事件处理得更快或者更慢就产生错误的结果。但是,可用性(系统及时应答客户端的能力)无可避免的要依赖于计时,举例来说,如果消息交换的时间比服务器崩溃之间的时间还长,候选人就不会有足够时间去赢得选举。没有一个稳定的leader,Raft就无法前进。

leader选举是Raft的一部分,对它而言,计时是非常关键的。只要系统满足下面的计时要求,Raft将能够选举和维持一个稳定的leader:

上面的 brodcastTime 是 服务器并行发送Rpc给集群中的所有机器并收到其返回的平均时间,electionTimeout是在5.2章中描述的选举超时时间,MTBF是一台服务器崩溃之间的平均时间。选举超时时间大于消息广播时间可以让leader可靠的发送心跳消息让其他机器保持从节点状态。选举超时时间采用随机的方法,也让票数均分的情况基本不会发生。选举超时时间应该小于MTBF,这样系统才能稳步推进。当leader奔溃时,系统会有一段约等于选举超时时间的时间段不可用,我们希望这在整个时间内只占用很小的一部分。

广播时间和MTBF是底层系统的属性,而选举超时时间是我们必须选择的。Raft通常会要求接收者将消息持久化到稳定的存储中,所以广播时间或许在 0.5ms到20ms之间,这个依赖于存储技术。所以,选举超时时间可能会在10ms到500ms之间。通常服务器的MTFB时间是几个月或者更久,它很容易满足上面的时间要求。

6. 机器成员变化(Cluster membership changes)

到目前为止,我们都是假设机器的配置(参与一致性算法的服务器集合)是固定的。实际上,偶尔会需要改变这个配置,例如当服务器故障时替换它们或者修改复制程度。虽然这个可以通过将整个集群下线,再更新配置文件,最后再重启整个集群的方式来处理,这会导致在整个变更期间集群是不可用的。此外,如果有任何手动操作,那会有操作错误的风险。为了避免这个问题,我们决定自动化配置变更,并将它们合并到Raft一致性协议中。

为了让配置变更机制是安全的,不能出现在转换过程中两个leader同时当选的情况。不幸的是,任何直接将服务器老配置更换为新配置的方式都是不安全的。想一次性自动更换所有机器的配置是不可能的,集群可能会在转换期间分割为两个独立的集群(例子看图10)

图10:直接从一个配置更换为另一个配置是不安全的,因为不同的服务器会在不同的时间进行转换。在这个例子中,集群冲3台机器扩充为5台。不幸的是,有一个时间点上有两个leader可以被同时选举出来,一个包含老配置的大多数机器,另一个包含新配置的大多数机器。

为了确保安全性,配置的变更必须采用两阶段(two-phase)的方法。有非常多的方式可以实现两阶段变更。例如,一些系统(例如[22])使用第一阶段无效化老的配置,让机器不能再处理客户端请求,然后第二阶段启动新的配置。在Raft中,集群首先切换到过渡配置,我们将其称为 联合一致性(joint consensus);一旦联合一致被提交了,系统再转变为新的配置。联合一致将新老配置联合了起来:

  • 在两种配置中,日志项将复制到所有的机器中。
  • 两个配置中的任何服务器都可能作为leader提供服务。
  • 协议(选举和日志提交)要求新老配置分开。

联合一致性允许各个服务器在不同的时间在不同的配置之间进行转换,而不影响安全性。而且,联合一致性允许在配置变更期间继续为客户端请求提供服务。

集群的配置被存储起来,在日志复制中当做特殊的日志项在服务器间交流,图11展现了配置变更过程。当leader接收到了一个将配置从C(old)变更为C(new)的请求时,它会为联合一致性存储这个请求(图中的C(old,new)),并将其作为一个日志项,通过前面的机制复制这个日志项。一旦一个服务器将新配置的日志项加入了本身的日志中,它就会用新的配置来做之后的决策(服务器会一直使用它日志中最新的配置,不理会这个日志项是否是已提交的)。这也就意味着当C(old,new)被提交时,leader会使用C(old,new)的规则来做决策。如果leader奔溃了,新的leader可能是C(old),也可能是C(old,new),这个依赖于候选人节点是否收到了C(old,new)日志项。在任何情况下,这段时间内C(new)都不能做出单方面的决策。

图11:配置变更的时间线。虚线表示配置项被创建但未提交,实线表示配置项最新一次提交。leader首先创建C(old,new)配置项在其自身日志中,然后提交C(old,new) (C(old) 和C(new) 的大多数)。然后创建Cnew日志项将其提交到C(new)的大多数。这里没有任何一个时间点C(old)和C(new)两个都可以单独的决策。

一旦C(old,new)被提交了,C(old)和C(new)可以不征求另一个的同意直接进行决策,日志的完整性保证只有C(old,new)配置的机器可以被选为leader。现在就可以安全的创建一个C(new)的日志项并将其复制到集群中。同样,配置一旦被看见,就会被服务器应用。当新的配置在C(new)的规则下被提交的时候,老的配置就无关紧要了,并且不在新配置的机器可以被关闭。如图11所示,没有C(old)和C(new)可以同时单方面做决定的时刻,这样也就保证了安全性。

关于重新配置还有另外三个问题。第一个问题是,新的服务器初始化没有存储任何的日志项。如果他们在这种状态下加入集群,它们可能要好一会儿才能刚上日志的进度,在这期间它不可能去提交新的日志项。为了避免可用性间隙,Raft在配置变更之间引入了一个额外的阶段,新机器加入集群先作为不可投票的成员(leader将日志复制给它们,但是它们并不算在参与决策的大多数机器中)。一旦新机器嘴上了集群的进度,重新配置过程就可以像上面描述的那样进行了。

第二个问题是,leader节点可能并不是新配置中的一部分。在这种情况下,一旦它提交了C(new)日志项,leader会下线(返回到从节点状态)。这意味着有一段时间(在提交C(new)的过程中),leader节点管理整个集群,但是不包括它自己本身,它自己本身也不能算作日志复制中的计数。当C(new)变为已提交的时候,leader开始转换,因为这是新配置可以独立操作的第一个时间点(总是可能从C(new)中选举一个leader).在这个时间点之前,可能是有来自C(old)的机器可能被选为leader。

第三个问题是,移除服务器(不在C(new)中的机器)会中断机器。这些机器不会再收到心跳,所以它们会超时并开始一轮新的选举。它们会使用新的term编号发送RequestVote请求,这会造成当前的leader变为从节点状态。最终会选出一个新的leader,但是移除的机器会再次超时并重复上面的过程,这会导致很差的可用性。

为了预防这个问题,当服务器相信当前leader还存活着时,会拒绝RequestVote请求。具体来说,如果一个服务器在没有超过最小超时时间之前收到了RequestVote请求,则它不会更新它的term编号,也不会授予投票。这样不会影响正常的选举,在开始选举之前,它会等待一个最小的选举超时时间。但是,它可以帮助避免待移除服务器中断集群:如果leader可以持续的将心跳发送给集群,那么它就不会被更大的term编号所替代。

7. 日志压实(Log compaction)

在正常的操作期间Raft的日志会包含非常多的客户端请求,但是在真实的系统中,日志不可能无限制的增加的。当日志增加到非常长的时候,它会占有非常大的空间,并且日志回放也会花费更多的时间。如果没有一些机制去废弃过期的日志信息,最终会造成可用性问题。

快照是一种简单的压实方式。在这种方式中,将当前系统的状态写入一个快照中并持久化到稳定的存储中,然后可以废弃这点之前的所有日志。在Chubby和Zookeeper中就是使用这种方式,下面的部分将描述Raft中的快照。

还有一些例如日志清理【36】和日志结构合并树【30,5】的渐进式压实方法。这些对数据的操作一次只会压实一小部分数据,所以它对压缩的负载也更加均匀。第一步是选择一个积累了大量已删除和被覆盖日志项的数据区域,然后重写存活的对象并释放这个区域。与快照相比,这需要显著的额外机制和复杂性,后者总是在整个数据集上操作,从而简化了问题。虽然日志清理需要修改raft,但是状态机可以使用与快照相同的接口来实现LSM树。

图12表述的是Raft中快照的基本想法。每个服务其独立生成快照,覆盖在其日志中已提交的日志项。大多数工作由状态机将当期的状态写入快照组成。Raft也会包含少量的元数据在快照中:lastIncludedIndex是快照替换的最后一个日志项的索引(最后一个在状态机中应用的日志项), lastIncludedTerm是前面那个日志项所属的term编号。这些是用来保证AppendEntries的一致性校验的,因为他需要前一个日志索引和term编号。为了兼容集群成员变化(第6章),快照也会包含最后包含的索引日志项对应的最新的配置,一旦服务器完成了快照写入,它就可以删除所有最新包含的日志项之前的日志项了,也可以删除任何之前的快照。

虽然服务器通常是独立的生成快照,但是leader偶尔也需要将快照发送给从节点。当leader用快照丢弃了日志项的时候,就需要将快照发送给同步较慢的从节点。幸运的是,这个情况在正常操作下是不可能的:一个跟上leader的从节点已经有了这个日志项。但是,异常慢的从节点或者新加入集群的机器不会拥有这个日志项。让这样的从节点了解最新的信息的方式,就是leader将快照通过网络发送给它。

leader使用一个叫InstallSnapshot的RPC发送快照给落后较多的从节点,接口定义如图13。但从节点收到这个RPC带来的快照时,它必须要决定应该如何处理意见存在的日志项。通常这个快照中会报错接受者日志中不存在的信息。在这种情况下,从节点废弃整个日志,全部使用快照来取代,可能未提交或者冲突的日志项。如果从节点收到的是描述它自身日志前缀的快照(由于重发或者错误),则快照覆盖到的日志将被删除,但是快照后面的日志项仍然是有效的,必须被保留。

这种快照方法背离了Raft强leader原则,因为从节点可以在不了解leader的情况下生产快照。但是,我们认为这个背离是合理的。当有leader的时候可以帮助解决达到一致性过程中决策冲突,在快照里面一致性其实已经达到了,所以不会产生决策冲突。数据依然只会从leader流向从节点,只是从节点可以自行组织它的数据。

我们考虑过使用基于leader模式的方法来做快照,即只有leader生成快照,然后将快照发往所有的从节点。但是,这样做有两个缺点。首先,发送快照给从节点会浪费带宽并减慢快照的处理。每个从节点已经拥有了产生自己快照的所有信息,并且通常使用本地状态生成快照会比从网络中发送和接收快照更加廉价。第二,这样会导致leader的实现更加复杂。例如,leader需要并发的发送快照给从节点,并同时复制新的日志项给从节点,以免阻碍新的客户端请求。

这里还有两个可能影响快照性能的问题。首先,服务器需要决定在什么时候生成快照。如果服务器生成快照过于频繁,会浪费磁盘带宽和资源;如果快照生成太过罕见,可能会对系统存储造成风险,并且还会增加重启期间日志应用的时间。一个简单的策略是当日志达到一个固定的大小时生成快照。如果这个大小设置为一个明显大于预期快照大小的值,则磁盘带宽开销将会很小。

第二个性能问题是,写入快照会花费一个可观的时间,我们不希望快快照拖慢正常的操作。解决方案是采用 写时复制(copy-on-write)技术,这样快照的写入就会不会影响接收新请求了。例如,状态机使用函数式数据结构就天然的支持这个,或者是使用操作系统的写时复制(copy-on-write)支持(例如linux的fork)来创建一个整个状态机的基于内存的快照(我们的实现采用了这用方式)。

客户端交互(Client interaction)

这一章描述了客户端如何和Raft进行交互,包括客户端如何找到集群的leader,以及Raft如何支持线性化语意(linearizable semantics)【10】。这些问题是所有一致性系统都需要解决的,而Raft的解决方案比其他系统更加简单。

Raft的客户端将它们的所有请求都发送给leader。当客户端首次启动时,它会随机选择服务器进行连接。如果客户端首选选择的不是leader节点,服务器会拒绝客户端的请求,并提供它知道的最新leader的信息(AppendEntries会包含leader的地址)。如果leader奔溃了,客户端请求会超时,然后客户端再随机选择一个leader进行重试。

我们给Raft的目标是实现线性化语意(每一个操作在其调用和响应之间的某一点上看起来都是立即、准确地执行一次)。但是,根据Raft的描述,一条命令可能会包多次执行:例如,如果leader在提交了一个日志项,但是还没返回给客户端时崩溃了,客户端会使用新leader重试这个命令,这会造成命令被二次执行。解决方案是客户端给每一个命令都增加唯一的序列号。然后,状态机跟踪每个客户端最新的序列号和其相关的返回,如果它收到命令的序列号已经被执行了,那么它会立刻返回存储的请求响应,不会再重新执行请求了。

只读请求可以不写入任何东西到日志里面进行处理。但是,在没有额外措施的情况下,可能会有返回过期数据的风险,因为响应请求的leader可能被另一个新的leader所取代了,而新的leader并不知道新的数据。线性化读取不可能返回过期的数据,Raft需要两个额外的措施来保证,而不是使用日志。首先,leader必须拥有已提交的日志最新的信息。日志完整性属性(Leader Completeness Property)保证了leader拥有所有已提交的日志项,但是在它所属的term刚开始时,它可能并不知道哪些是已提交的(上一个term复制过来的日志项,需要当前term有提交,才能确定为已提交的,否则这个leader奔溃,可能由另一个不含前一个term日志项的节点当选leader,从而导致上一个term的日志项被覆盖)。为了找出哪些是,它需要在当前的term提交一个日志项。Raft通过leader刚起来时提交一个没有操作(no-op)的日志项来解决这个问题。第二,leader在处理只读请求时必须检查自己是否被罢免了(如果最近有新的leader被选举出来了,它的信息可能是陈旧的)。Raft通过在应答只读请求前先发送心跳信息给集群中的大多数机器来处理这个问题。或者leader可以依赖心跳机制来提供一种租赁模式【9】,但是这依赖于计时安全性(假设有有界时钟偏移).

9. 实现与评估(Implementation and evaluation)