RaftPaper:寻一个可被理解的共识算法
周末:摆不烂,卷不动,随便读一篇paper吧
原文:In Search of an Understandable Consensus Algorithm
作者:Diego Ongaro / John Ousterhout —— Stanford University
摘要
Raft是一个用于管理一份被复制的日志的共识算法,它和(multi-)Paxos产出的结果等价,和Paxos一样高效,但它的结构和Paxos不同。一个日志条目被这让Raft比它更加易于理解,并且提供一个更好的构建实际系统的地基。为了便于理解,Raft拆分出了共识的关键元素,比如领导选举、日志复制以及安全,并且它使用了一种更强的一致性来减少我们必须考虑的状态数。从用户研究的结果来看,Raft对于学生比Paxos更加好理解。Raft也包含一个新的机制来改变集群的成员,该机制通过覆盖大多数(overlapping majorities)来保证安全。
1 介绍
共识算法允许一组机器作为一个一致组工作,即使某些成员出错了也能活下来。正是由于这一点,它们在构建可靠的大规模软件系统中成为了关键角色。Paxos在过去几十年里在共识算法的讨论中占有霸主地位:大多数共识算法的实现基于Paxos,或者受其影响,Paxos也成为教授学生共识算法时的主要工具。
不幸的是Paxos实在难以理解,尽管人们已经无数次尝试让它变得更简单,此外,想要支撑实际系统的话,它的架构需要复杂的改变,因此,无论是系统构建者还是学生都为Paxos苦苦挣扎。
在我们深陷Paxos泥潭后,我们开始寻找一种新的共识算法,它可以给系统构建以及教学提供更好的基础,我们的方法可能不太寻常,因为我们的主要目的是可理解性:我们是否可以为实际系统定义一个共识算法,并且使用比Paxos学习起来要明显简单的方式来描述它?此外,我们想要该算法可以促进对于系统构建者极为重要的直觉开发。重要的不仅仅是算法如何工作,而是要清楚的知道它为什么可行。
这项工作的结果就是一个被称作Raft的共识算法,在Raft的设计中,我们应用了特定的技术来提升可理解性,包括分解(Raft分解了leader选举、日志复制以及安全)、状态空间缩减(相对于Paxos,Raft减少了不确定性以及服务器之间可能不一致的成因)。一项来自两个大学的43名学生的用户研究表明,Raft显著地要比Paxos易于理解:在学习了两种算法后,33名学生相比于Paxos能够更好的回答有关Raft的问题。
Raft在很多方面与现存的共识算法相似,但它有多种新特性:
- 强leader:Raft使用一个比其它共识算法更强形式的领导权。举个例子,日志项只会从leader传递到其它服务器上,这简化了复制日志的管理,让Raft更加易于理解。
- Leader选举:Raft使用随机计时器来选举leader,这只会比其它共识算法需要的心跳机制添加少量的机制,同时可以快速简单的解决冲突。
- 成员变更:Raft的修改集群中的server集合的机制使用了一种新的共同共识(joint consensus)方式,在这种机制中,两种不同的配置中的大多数会在迁移过程中重叠,这允许集群在配置变更期间继续正常操作。
我们相信Raft比Paxos和其它共识算法更加优越,无论是对于教育用途还是作为真正实现的基础。它比其它算法更加简单,好理解;它的介绍已经足够满足实际系统的需求;它有很多开源实现,并且被很多公司使用;它的安全属性已经被证实定义并证明;并且它的效率也可以和其它算法搏一搏。
论文中的余下部份介绍了复制状态机问题(第二节),讨论了Paxos的优点和缺点(第三节),描述了我们实现可理解性的一般方法(第四节),展示Raft共识算法(第5到8节),评估Raft(第九节)以及讨论相关工作(第十节)。
2 复制状态机
共识算法通常在复制状态机的上下文中被提出。在这种方式下,一组server上的状态机计算相同状态的相同副本,并且在某些其它server down掉时继续操作。复制状态机被用于解决多种分布式系统中的容错问题,比如具有单一集群leader的大规模系统,如GFS、HDFS以及RAMCloud,通常使用一个独立的复制状态机来管理leader选举以及保存即使在leader崩溃时也必须存活的配置信息,复制状态机的例子包括Chubby以及Zookeeper。
图1:复制状态机结构。共识算法管理一个包含客户端发来的状态机指令的复制日志,状态机进程从日志中执行相同的指令,所以它们产生相同的结果
复制状态机通常使用复制日志实现,就像图1中那样。每一个服务器存储一个包含一个指令序列的日志,它的状态机将会按序执行。每一个(服务器上的)日志包含的指令顺序相同,所以每一个状态机进程执行相同的指令序列。因为状态机是确定性的,所以每一个状态机都计算相同的状态以及相同的输出序列。
保持复制日志的一致性是共识算法的任务,在server上的共识模块接受到客户端的指令,将它添加到自己的日志中,它与其他server上的共识模块交流以确保每一份日志最终包含具有相同顺序的相同的请求,即使某些server异常。一旦命令被正确复制,每一个服务器的状态机进程将以日志顺序来处理它们。结果就是,所有server表现得像一个单一的,高可靠的状态机。
实际系统的共识算法具有如下属性:
- 它们在所有非拜占庭条件下确保安全性(永远不会返回一个不正确的结果),包含网络延迟,分区以及丢包、重复包、乱序包
- 只要大多数server正常,并可以互相通信,也可以与客户端通信的话,系统就完全可用。因此一个典型的5个服务器的集群可以容忍任何两个服务器异常。假定服务器以停止的方式失败,并且它们稍后会从稳定性存储中恢复状态,重新加入集群
- 它们不依赖时间来确保日志一致性:错误的时钟和极端的消息延迟在最坏情况下可能导致可用性问题
- 一般情况下,一旦在单一一轮RPC调用中一个命令被集群中的大多数响应,它就可以完成。少量的慢server不会对系统的总体性能造成影响。
3 Paxos犯了什么错?
在过去的十年里,Leslie Lamport的Paxos协议已经成了共识的同义词:它是在课程里被使用的最广泛的协议,大多数共识的实现使用它作为出发点。Paxos首先定义了一个能够就单个决策达成一致的协议能力,比如单个日志条目的复制,我们称这个子集为单一法令 Paxos(single-decree Paxos)。然后,Paxos组合了该协议的多个实例来实现一系列的决策(multi-Paxos)。Paxos确保安全和活性,并且它支持修改就要成员,它的正确性已经被证明,并且它在常规情况下很高效。
不幸不幸不行不行,Paxos有两个显著的缺点。第一个就是Paxos极其难懂,很少有人能理解它的完整解释,并且要做到这一点需要付出巨大努力。结果就是,有很多用简单术语来解释Paxos的尝试。这些解释专注于单一法令Paxos,即使这样还是困难重重。在对NSDI 2012的参会者的非正式调查中,我们发现即使在经验丰富的研究人员中也鲜有人能够适应Paxos。我们选择自己深入Paxos泥潭,读了多份简化的解释,甚至设计了自己的替代协议后,我们也没能完整的理解它,这个过程持续了一年之久。
我们假设Paxos的不透明来源于它选择了单一法令自己作为基础。单一法令Paxos是难懂且微妙的:它氛围两个阶段,没有简单直观的解释,也无法被独立理解。因为这一点,建立关于单一法令协议如何正常工作的直觉很困难。multi-Paxos的组成规则显著的增加了复杂性和微妙程度。我们相信,就多个决策达成共识的问题(比如一份日志而不是单个条目)可以以另一种更加直接和明显的方式被分解。
Paxos的第二个问题是它没有提供一个对于构建实际实现的好的地基。一个原因是目前还没有被广泛认可的multi-Paxos算法。Lamport的大部份解说都是关于单一法令Paxos的,它描绘了达成multi-Paxos的可能性,但是很多细节是缺失的。已经有很多次充实和优化Paxos的尝试,但是它们之间相差甚远,与Lamport的描绘也相差甚远。Chubby这种系统实现了类Paxos的算法,但是它们大多数情况下的详情尚未公布。
此外,Paxos对于构建实际系统来说是一个糟糕的架构,这也是单一法令分解带来的另一个后果。举个例子,独立选择一组日志条目,然后将它们合并进顺序日志中并没有什么好处,只是增加复杂性。围绕日志来设计系统更加简单高效,新的条目以一种首先的顺序被依次添加。另一个问题是Paxos的核心使用对称的点对点方式(即使它最后提出了一种弱领导权形式作为性能优化),这在只有单一决定会被做出的世界中是有意义的,但是很少有系统使用这种方式。如果一系列决策必须被做出,先选择一个leader,然后让leader协调决策将更加简单快速。
作为结果,实际的系统与Paxos几乎没有相似之处,每一个实现从Paxos开始,发现实现中的困难,然后选择了一个显然不同的架构,这耗时且易出错,而理解Paxos的困难加剧了这个问题。Paxos的公式或许能很好的证明它的正确性,但是实际的实现与Paxos天差地别,这个证明几乎没有什么用。下面是一个来自Chubby实现者的典型评论:
在Paxos算法的描述和真实世界系统的需求之间存在着巨大的鸿沟,最终的系统将基于一个未被证明的协议...
因为这些问题,我们可以做出Paxos无法提供一个教学和系统构建的好的基础,因为共识在大规模软件系统中如此重要,我们决定看看是否能够设计出另一个比Paxos具有更好属性的共识算法,Raft就是该实验的结果。
4 为可理解性而设计
在设计Raft时我们有几个目标:它必须为系统建设提供完整和实践的基础,所以它必须显著的降低开发者的设计工作;它必须在所有条件下都是都是安全的,必须在典型的工作条件下是可用的;对于常规操作它必须是搞笑的。但是最最重要的目标——以及最重要的挑战——就是可理解性,它必须让大多数听众可以舒适的理解算法。此外,它必须可能开发对于算法的直觉,所以系统构建者可以做现实世界中的实现不可避免的扩展。
在Raft的设计中,很多时候我们必须在不同的方式中进行选择,在这些情况下,我们基于可理解性评估所有方式:解释每一个方式有多困难(比如它的状态空间有多复杂,是否存在微妙的含义)以及读者理解该方法和含义的难易程度。
我们认识到这种分析具有高度的主观性,尽管如此,我们使用了两种普遍适用的技术。第一个技术是问题分解的常见方式:我们尽可能的将困难拆分成相对独立的可被解决、解释以及理解的单独部分。比如,在Raft中,我们分割了领导选举,日志复制,安全以及成员变更。
我们的第二个方式是通过降低需要考虑的状态来简化状态空间,让系统更加好理解,尽可能的消除不确定性。特别地,日志不允许有洞,Raft限制了日志彼此之间变得不一致的方式。尽管大多数情况下我们尝试消除不确定性,还是有一些不确定性实际帮助理解了的情况。比如,随机方式引入了不确定性,但是它通过以一种想死的方式来处理所有可能的选择来降低状态空间(“随便选,不重要”),我们使用随机化来简化Raft的领导选举算法。
5 Raft共识算法
Raft是一个管理第2节中提到的复制日志的算法。图2简洁的总结了算法供参考,图3列出了算法的关键属性;这些图中的元素将在本节中的剩余部分讨论。
选举安全性:在一个给定term中至多只有一个leader被推选。5.2节
Leader只追加:一个leader永远不会重写或删除它的log,它只添加新条目 5.3节
日志匹配:如果两份日志都包含一个相同的index和term,那么这两份日志中直到给定index以及之前的所有条目都是相同的 5.3节
Leader完整性:如果一个日志条目在一个给定term中被提交,那么在任何更之后的term中的leader都必须具有这个条目 5.4节
状态机安全性:如果一个server向它的状态机应用了指定index的日志条目,其它的server不可以在相同的index上应用其它的日志条目 5.4.3节图3:Raft保证这些属性中的每一个在任何时候都为true
Raft通过先选择一个leader,然后赋予它管理复制日志的全部责任来实现共识。leader从客户端接受日志条目,将它们复制到其它server上,告诉server现在将日志条目应用到它们的状态机上是否安全。单一leader简化了复制日志的管理,比如,leader可以决定是否在日志中放置新条目,而不用咨询其它server,数据流以一种简单的方式从leader流向其它server。一个leader可以失败,或与其它server断连,在这种情况下,新server将会被选出。
在这种模式下,Raft将共识问题分解成三个相对独立的子问题,将在后面的小节中讨论:
- 领导选举:一个新的leader必须在现有leader fail时被选出
- 日志复制:leader必须从客户端接受日志项,将它们向集群中复制,强制其它的日志与它的保持一致(5.3小节)
- 安全:Raft的关键安全属性是在图3中的状态机安全属性:如果任何一个server应用了一个特定日志条目到它的状态机中,那么不会有其它的server应用一个不同的命令到相同的日志位置(log index)上。5.4小节描述了Raft如何保证这一属性;解决办法在选举机制上引入了额外的限制,将在5.2小节中描述。
在展示完共识算法后,本节将讨论可用性问题以及计时(timing)在系统中的作用。
5.1 Raft基础
一个Raft集群包含多个server,典型是5个,这允许系统容忍两个server的failure。在任何给定时间,server只能是三种状态其中的一个:leader、follower或candidate。在常规情况下只有一个leader并且所有的余下server都是follower。follower是被动的:它们不会主动提交请求,只会简单的回复leader和candidate发来的请求。leader处理所有的客户端请求(如果一个客户端请求一个follower,follower将它重定向到leader)。第三种状态——candidate,被用于选举一个新leader,我们将在5.2节中介绍。图4展示了状态以及状态之间的迁移,下面我们会讨论状态迁移。
图4:服务器状态。followers只回复其它server的请求,如果一个follower接收不到任何交流,它就会变成candidate,并初始化一次选举。一个candidate若接收了整个集群中的大多数的投票,它就会变成leader。leader通常运行到它们fail。
Raft把时间分割到任意长度的terms,如图5所示。terms使用连续的整数计数,每一个term从一次选举开始,那时一个或多个canditate尝试变成leader,我们将会在5.2小节介绍。如果一个candidate在选举中获胜,那么它将在余下的整个term中作为leader。在某些情况下,一次选举可能导致一个分裂的选票(split vote),在这种情况下term将结束,没有leader被选中,一个新的term很快开始,重新进行选举。Raft确保在一个给定term中只有一个leader。
图5:时间被分割成terms,每一个term由一次选举开始,在选举成功后,单一leader掌管集群,直到term结束。有时选举会失败,在这种情况下term直接结束,没有选择出一个leader,term之间的迁移可能在不同的server上在不同时间被观察到。
不同的server可能在不同时间观察到term之间的迁移,并且在一些情况下,一个server可能观察不到一次选举,甚至观察不到整个term。term在Raft中就是一个逻辑时钟的角色,它们允许server检查过时信息,比如旧的leader。每个server保存一个当前term号,随时间单向递增,在server交流时,当前的term将被交换,若一个server当前的term比另一个小,那么它将更新当前的term到新的值。如果一个candidate或leader发现它的term已经过期,它立即转换到follower状态,如果一个server接受到一个具有旧term到请求,它将拒绝请求。
Raft到server使用RPC交流,并且基本的共识算法只需要两种RPC,RequestVote RPC(请求投票)被candidate在选举(5.2节)期间初始化,AppendEntries RPC(添加条目)被leader复制日志条目以及提供某种形式的hearbeat(5.3节)时初始化。第7节中为了在Server之间传输快照,我们将添加第三种RPC。若没能在指定时间内收到响应,Server会重试RPC,并且为了最好的性能,它们并行提交RPC。
5.2 leader选举
Raft使用一个heartbeat机制来触发leader选举。当server启动时,它们将作为follower,只要能收到leader或candidate发来的有效RPC,server就保持follower状态,leader周期性的发送heartbeat(不携带日志条目的AppendEntries RPC)给所有的follower以维持它的地位。如果follower在被称作选举超时的周期内没有接受到任何通信,它就会假设现在没有可用的leader,并开始一轮选举来选出新的leader。
为了开始选举,follower将增加自己的term,并转移到candidate状态,然后它为自己投票,并并行给所有集群中的其它server发送RequestVote RPC。一个candidate保持它的候选状态直到下面三件事其中之一发生:
- 它赢了选举
- 另一个server已经将自己建立为一个leader
- 一段时间过去了,没有胜利者
这些结果将在下面的段落中分别讨论。
如果一个candidate接收到了整个集群中大多数的server在相同term的投票,它就赢了选举。每一个server在一个给定term只能投票给最多一个候选人,以先来先服务的原则(注意:5.4节中将会在投票上加额外限制)。大多数规则确保在特定term中只有一个candidate可以在选举中胜出(图3中的选举安全属性)。一旦一个candidate赢了选举它将变成leader,然后它将发送heartbeat消息给所有其它的server以建立它的特权,也是在阻止新的选举发生。
在等待投票的过程中,一个candidate可能接收到其它宣称自己时leader的server的AppendEntries RPC请求,如果leader的term(在这次RPC中包含)至少和当前candidate的term一样大时,candidate就会承认这个leader是合法的,并返回到follower状态。如果RPC的term比candidate当前的term小,它将会觉此次RPC,并持续candidate状态。
译者:考虑一个leader(称A)和某一个follower(称B)之间由于网络问题导致一段时间内无法联通,B会以一个新的term给集群中所有server发起投票,若这些投票RPC还没到达其它server之前,leader的一个AppendEntries却到达了其它server,会不会造成这条消息在其它server上达成了共识,在B上却被回绝呢。往下看看。
第三种可能的结果是candidate没赢也没输掉选举,如果很多的follower同时成为candidate,投票将被分散,导致没有candidate获得大多数。当这件事情发生,每一个candidate将会超时,并且通过增加它的term,初始化新一轮的RequestVote RPC开启一个新的选举。不管怎样,若没有额外的措施,分裂投票将会无限的重复下去。
Raft使用随机选举timeout来确保分裂投票几乎不可能发生,并且可以被快速解决。为了阻止首次投票就发生投票分裂,选举超时将在固定的间隔内随机选择(比如150到300ms)。这可以打散server,因此大多数情况下只有一个server将会超时,他会在在任何其它server超时前赢得选举并发送heartbeat。相同的机制也被用于处理分裂投票,每一个candidate在一次选举伊始就重启他的随机选举超时时间,并在开启下次选举之前等待超时时间到达。这会减少在新选举中的另一次分裂投票的可能性。9.3节展示了这一方式可以快速地选举一个leader。
选举正好是一个例子,可以用于说明易理解性如何指导我们在不同设计方式之间进行选择。开始的时候我们计划使用一个排名系统:每一个candidate将被分配一个唯一的排名,会在在竞选人之间选择时用到。如果一个candidate发现了另一个candidate具有更高的排名,它将变回follower状态,因为更高排名的候选人更容易赢得下一次选举。我们发现这种方式会有微妙的可用性问题(如果排名较高的服务器失败,排名较低的服务器可能需要超时并重新成为候选服务器,但如果超时过早,就会重置选举leader的进度)。我们多次调整了算法,但是每次调整,新的极端情况都会出现,最终,我们认为随机重试方式更加明显,更加便于理解。
5.3 日志复制
一旦一个leader被选举了,它将开始服务客户端的请求。每一个客户端请求都包含一个要被复制状态机执行的命令,leader将命令作为一个新条目追加到它的日志中,然后发起给每一个其它server并行发起AppendEntries RPC来复制条目,当条目被安全复制(下面会介绍),leader该条目到它的状态机上,并返回执行结果给client。如果follower崩溃或者运行缓慢,或者网络包丢失,leader将无限期的重试Append Entries RPC(甚至在它已经给客户端响应之后),直到最终所有followers都存储了所有的日志条目。
日志被组织成图6的样子,每一个日志条目保存一个状态机命令以及当条目被leader接收时的term编号,term编号在日志条目中杯用于检测日志之间的一致性,以及确保图3中的一些属性。每一个日志项也有一个整数的下表来标记它在日志中的位置。
图6:日志由条目组成,它们顺序排号,每一个条目都包含了它们被创建的term(方块中的数字)以及状态机的一条命令。一个条目被认为是已提交的,如果它的条目可以被正确的应用到状态机中。
leader决定合适将日志条目应用到状态机上是安全的,这样的条目被称作已提交的。Raft保证已提交的条目是持久的并且最终会被所有可用的状态机执行。一个日志条目,一旦被leader创建,并被复制到大多数server中,就认为它是已提交的(例如图6中的条目7)。这也同样提交了在leader的日志中所有之前的条目,包括被前一个leader创建的条目。5.4小节讨论了在leader更改时应用该规则的细节,并展示了这种对提交的定义是安全的。leader跟踪它知道的最大已提交索引,并且在未来的AppendEntries RPC(包括heartbeat)中包含这个索引,以让其它server最终能够发现。一旦一个follower得知一个日志条目已提交,它就会以日志顺序将它应用到自己的本地状态机上。
我们设计了Raft的log机制以维护不同server上的log之间的高层次一致性。这不仅简化了系统的行为,让它变得更加可预测,而且它还是一个确保安全的重要组件。Raft维护了如下属性,它们共同构成了图3中的Log Matching Property:
- 如果在不同日志中的两个条目具有相同的index和term,那么他们存储着相同的命令
- 如果在不同日志中的两个条目具有相同的index和term,那么所有先前的条目也都是相同的
第一个属性遵循了leader在一个term中对于一个给定日志索引只创建一个条目,并且日志条目永远不会改变它们在日志中的位置的事实。第二个属性通过一个简单的,被AppendEntries执行的一致性检查保证。当发送一个AppendEntries RPC时,leader包含在它的日志中紧接着新条目的那个条目的index和term,如果follower在它的日志中找不到一个具有相同index和term的条目,它就拒绝这个新条目。一致性检查作为一个归纳步骤:初始的空日志状态满足Log Matching Property,一致性检查在日志被扩充时保持Log Matching Property,结果就是,每当AppendEntries成功返回,leader都知道follower的日志和它的相同。
在正常操作期间,leader和follower的日志保持同步,所以AppendEntries的一致性检查永远不会失败,然而,leader崩溃可能留下日志不一致(老leader可能并没有完全复制它日志中的所有条目),在有一系列leader和follower崩溃的情况下,这种不一致可能加剧。图7描绘了follower的日志可能和新leader不同的情况,一个follower可能缺少leader上有的条目,也可能有leader上没有的条目,或者两者都发生。日志中缺失的和多余的条目可能跨越多个term。
图7:当顶部的leader上台时,在follower的日志中可能出现(a到f)的任意一种情况。每一个盒子代表一个日志条目,盒子中的数字代表term,一个follower可能缺失条目(ab),可能有多余的未提交条目(cd),或者两种情况都有(ef)。举个例子,情况f可能在如下情况下发生:server在term 2时是leader,添加了多个条目到它的log中,然后在提交它们之前就嗝屁了,很快它就恢复了,变成了term3的leader,又添加了少量的条目到它的log中,在term2和term3的条目被提交之前,server又嗝屁了,在后续的几轮term中它都保持嗝屁状态。
在Raft中,leader通过强制follower重复自己的日志来处理不一致,这意味着follower中冲突的日志条目将被leader日志中的条目覆盖。5.4节将展示当耦合了一种额外限制后,这个操作将是安全的。
为了将follower的日志变得与它的一致,leader必须找出二者的都认同的最后一个日志,删除follower日志中任何在这之后的日志,并发送leader中任何在这之后的日志给follower。所以这些操作都发生在AppendEntries的一致性检查执行的响应中。leader给每一个follower维护了一个nextIndex,这是leader将发送给follower的下一个日志条目的index,当leader首次上任,它初始化所有nextIndex
为它的最后一个日志条目(图7中的11),如果follower的日志与leader的不一致,AppendEntries的一致性检查将在下一个AppendEntries RPC中失败,在失败后,leader递减nextIndex,重试AppendEntries RPC,最终,nextIndex将到达leader和follower的日志匹配的位置。当这发生,AppendEntries将成功并删除任何follower中冲突的条目,添加leader日志中的条目(如果有的话)。一旦AppendEntries成功,follower的日志就和leader的日志一致了,并且在term余下的时间里得到保持。
如果想要,协议可以被优化以减少AppendEntries RPC失败的次数,比如,当follower拒绝AppendEntries请求时,可以包含冲突条目的term,以及它存储的关于该term的首个index,有了这些信息,leader可以递减nextIndex以过滤所有的在那个term中的冲突条目。这样的话,每个有冲突的term需要一个AppendEntries RPC,而不是每一个条目需要一个。在实践中,我们怀疑这种优化没什么毕业证,因为failure不会经常发生,并且很难有大量的不一致条目。
有了这个机制,leader不需要在它上任时做任何特别的东西来恢复日志一致性,他只需要正常开始操作,然后日志就会自动收敛,以响应AppendEntries的一致性检查失败。一个leader永远不会重写或删除在它自己的日志中的条目(图3中的Leader Appen Only属性)。
这个日志复制机制展现了我们在第2节中期待的共识属性:Raft可以接受、复制并应用新的日志条目,只要大多数server都是活的;在通常情况下,一个新的条目可以在一轮对集群中大多数节点的RPC调用中被复制,并且单一的慢follower不会影响性能。
5.4 安全性
前一部分介绍了Raft如何选举leader以及复制日志条目,然而,我们迄今为止介绍的机制还不足以满足确保每一个状态机以相同顺序执行完全相同的指令。举个例子,一个follower可能在leader提交多个日志条目时不可用(只有它没接到这些日志),然后(老的leader挂了,随机的timeout又恰好让)它变成了被选出的leader,它会覆盖要求所有节点的日志向它看齐。结果就是,不同的状态机可能执行不同的命令序列(可能有些已经被提交的日志被删除)。
译者:上面这个例子是我自己举的,因为paper里的那个例子没看懂,原文如下:
For example, a follower might be unavailable while the leader commits several log entries, then it could be elected leader and overwrite these entries with new ones; as a result, different state machines might execute different command sequences
这小节通过给哪些server可以被选为leader加一些限制条件来完成Raft算法,限制条件确保了任意term的leader会包含所有前面的term中已提交的条目(图3中的Leader Completeness属性)。给出了选举的限制后,我们稍后就可以将提交的规则制定的更加精确,最终,我们提供了一个Leader Completness属性的证明,并展示了这如何领导复制状态机的正确行为。
5.4.1 选举限制
在任何基于leader的共识算法中,leader必须最终存储所有的已提交日志条目,在一些共识算法中,如Viewstamped Replication,一个leader甚至可以在它初始不包含所有已提交条目时被选举。这些算法提供了额外机制来识别缺失的条目,并且将它们移动到新leader上,有可能是在选举过程中,也有可能是在之后。不幸的是,这造成了一笔可观的额外机制和复杂性。Raft使用一个更简单的方式来保证所有在之前的term中的已提交条目都会从它当选的那一刻出现在新leader中,不需要传输任何这些条目到leader。这意味着日志条目只沿一个方向流动,那就是从leader到follower,leader永远不会重写它日志中的现存条目。
Raft使用投票机制来阻止一个candidate当选,除非它的日志包含所有已提交条目。一个candidate要想当选,必须联系集群中的大多数,这意味着任何提交的条目肯定在这些(大多数中)的某个server中出现。如果candidate的日志至少和这个大多数中的任何其它server中的日志一样新(“一样新”会在下面被精确定义),这样它便持有了所有已提交的条目。RequestVote RPC实现了这个限制,RPC包含candidate的日志相关的信息,如果投票者的log比candidate的更新,它就会拒绝给它投票。
Raft通过比较index和term来判断两个日志哪一个更新。如果日志的最后一个条目具有不同的term,那么比较晚的那个term的日志将是更新的,如果具有相同的term,那么那个更长的日志是更新的(具有更大index的)。
5.4.2 提交前一个term的条目
就像我们在5.3中介绍的,一个leader知道一旦它当前term中的entry被大多数server保存了,该entry就可以提交了。如果leader在提交entry前崩溃,未来的leader将尝试去完成条目的复制,然而,一个leader无法立即得出在之前的term中的条目可以被提交,即使它已经被大多数server保存了。图8描绘了一个情况,老的条目保存在大多数server上,但仍会被未来的leader覆盖。
图8:一个时序,可以说明为什么一个leader不能使用来自之前term的日志条目来判断能否提交。在(a)中,S1是leader,并在索引2处部分的复制了日志条目。在(b)中,S1崩溃了,S5通过S3和S4以及它自己的投票在term3当选了leader,并在index2处接受了不同的条目。在(c)中,S5崩溃了,S1重启了,被选为leader,继续复制,在这个情况下,来自term2的日志条目已经被复制到了大多数server上,但它还没有提交。如果S1在(d)中崩溃了,S5当选新节点(通过S2、S3、S4的选票),并且使用它在term3中的条目覆盖了别的。然而,如果S1在崩溃前从它当前的term复制了一个条目到大多数server,就像(e),然后这个条目已经提交了(S5不会赢得选举),在这个时候所有之前在日志中的条目都完好的提交了。
为了限制图8中的问题,Raft永远不会通过数副本数来提交前一个term中的日志条目。只有在leader当前term的日志条目才会通过数副本数被提交,一旦一个当前term的条目以这种方式被提交了,那么由于Log Matching Property,所有之前的条目也被提交了。仍有一些情况是leader可以安全的认为老term中的日志可以被提交的(比如所有server都保存了这条entry),但是Raft为了保证简单性使用了更保守的措施。
Raft在提交规则中引入这个额外的复杂性是因为在leader从前一个term复制条目时,原始term号得以保持。在其它共识算法中,如果一个leader重新复制前一个term的条目,它必须使用一个新的term号。Raft的方式让推理日志条目变得简单,因为它们term号随着时间推移在跨log时保持相同。
5.4.3 安全论证
给定完整的Raft算法,我们现在可以更加精确的论证Leader Completeness属性得到保持。我们假设Leader Completeness Property没有得到保持,然后我们做一个反证。
5.5 follower和candidate崩溃
5.6 计时以及可用性
6 集群成员变更
7 日志压缩
8 客户端交互
9 实现以及评估
10 相关工作
总结
本小节是译者通过学习paper进行的总结。
选举子问题
Raft的选举阶段解决的子问题很简单,就是确保集群中只有一个leader,leader fail时选择新的leader。
Raft引入term的概念,term可以看作是任期,每一个任期开始时可能有一个或多个candidate竞选leader,获得大多数票数的那个当选:
- follower按照先来先服务给candidate投票,在一个term中只能给一个candidate投票
- candidate遇到大于等于它当前term的AppendEntries消息时,自动变回follower,因为此时可能已经有其它leader当选或term已经过了
- 为避免投票分裂引发的无限重试,引入随机的选举超时,先超时的那个term++,开启新一轮竞选
- 当candidate当选,周期性并行发送空AppendEntries作为心跳,维持leader地位
日志复制子问题
leader当选后开始接收客户端的日志并复制给其它server,这个阶段要解决的问题是维护多个状态机间日志的顺序性,日志必须是顺序的,没有空洞,没有遗漏,但其它状态机可以比leader进度更慢
leader通过类似数学归纳法的方式进行日志复制,它要确保:
- 其它server日志上给定index的条目,若term和leader相等,则携带的状态机命令相等
- 其它server日志上给定index的条目,若term和leader相等,则该index前面的所有日志条目和leader都相等
这俩条件保证了啥?保证了leader上任后,每一个follower中的日志可以分成两个连续的段(比如[0, 5], [6, 8]两段),第一段能和leader上的匹配,第二段无法匹配,第二段需要被leader的其它日志覆盖掉,这两段都可以为空集。下图列举了几种允许的情况,第一段已经用红色的标出。
第一个条件很好确保,因为一个term只能有一个leader,leader一旦追加了一条日志便不能修改,也就是说index上的日志就定死了。
第二个条件需要AppendEntries RPC的校验阶段支持,当leader RPC调用同步一条数据时,需携带要添加的条目的前一个条目的index和term,follower会比对是否一致,若不一致RPC调用就失败,此时leader需要尝试追加前一个日志条目到follower上,然后follower会再次进行这种校验,直到找到第一个index和term相等的。这不就是数学归纳法,在日志为空时,满足条件,每次追加一条新日志都满足条件,最终满足限制条件。
安全性子问题
Raft算法中,一旦日志被大多数人写入,那么就算提交了,leader可以将它应用到状态机,在后续的AppendEntries RPC中,可以让follower将它应用到状态机。
在安全性子问题得到解决前,现在的Raft还有一个问题,leader现在迫使所有server和它保持一样的日志,太过粗鲁,有可能有一些已经被整个集群认为提交,但leader并不知道的日志,Raft的保障是一个日志一旦被认为已提交就可以安全的应用到状态机,但若它还会被leader刷掉,我们的状态机集群就有可能不再安全,无法向外界表现出像一台单个状态机一样(多个状态机执行的指令序列不同,很可怕)
Raft通过限制谁能成为leader解决这一问题,一个日志一旦被提交,就证明集群中大多数节点都有它,如果我们在竞选时让参选者上报它最大的index和term,每一个投票者将会和自己最后一个日志做比对,若它们具有相同的term,则index更大的更新,若它们具有不同的term,term大的更新,若参选者发现自己的日志比竞选者新,就拒绝给它投票。若参选者缺失一条已提交的日志,那么它必然不会被大多数server投票的。