写在前面
一直以来, 对Raft协议的理解感觉都没有非常到位, 本着眼过千遍, 不如手过一遍的原则, 利用空闲时间, 就自己把Raft翻译一遍, 加深自己的理解, 也方便其他的同学参考。
计划分三部分:
第一部分: 原文的1 ~ 4章;
第二部分: 原文的第5 章;
第三部分: 原文的6 ~ 10 章
寻找一种可以理解的一致性算法
5 Raft一致性协议
Raft是一个如Session 2 表格描述的管理复制日志的算法。 图2以简练的方式总结了该算法以供参考, 图3 列出了该算法的核心的属性, 这些属性在本部分余下的章节逐个讨论。
Raft通过首先选出一个唯一的leader, 然后给它完全的权限来管理复制日志来实现一致性。 leader 从client端接收日志, 把这些日志复制到其它的服务器上, 并且告诉服务器什么时候把这些日志回放到它们的状态机是安全的。拥有一个leader简化了管理日志复制, 比如, leader可以决定将新的日志放置在日志文件的位置而不需要咨询其他的服务器, 而且数据流能够以简单的方式从leader到其他的服务器。leader可能会发生故障或者与其他的服务器断开连接, 在这种情况下新的leader会被选举出来。
通过leader的方式, Raft把一致性问题分解成为3个相对独立的子问题, 我们在下面的章节会讨论它们:
- Log replication (日志复制):leader必须要从client接收日志, 并把它们复制到集群的成员中, 迫使所有的日志和它自己保持一致(Session 5.3)。
- Safety(安全性): Raft的主要的安全属性是图3 状态机安全属性:如果一个服务器已经在它的状态机上执行了某一条日志, 其他的的服务器在相同的log index不能执行不同的命令。 Session 5.3 描述了Raft如何保证这个属性, 这个方法在选举机制上引入了另一个限制, 会在Session 5.2描述它。
展示了一致性算法之后, 本章会讨论可用性和定时在系统中的作用。
5.1 Raft 基础
一个Raft集群包含好几个服务器, 典型的个数是5个, 这样它能容许系统中有2个服务器发生故障。在任意时间点, 每一个服务器是处于3个状态之一: leader, follower, 或者candidate。 在正常的操作中, 只有一个leader, 其它都是follower。 Follower 是被动的: 它们自己不会发出请求, 而是简单的响应从leader或者candidate的请求。Leader处理所有的client请求(如果client连接到follower, follower会重定向到leader)。 第三种状态, candidate, 被用来选举一个新的leader, 如Session 5.2 所述。图4 显示这些状态以及它们的转换, 这些转换在下面讨论。
Raft 把时间划分成如图5 所示任意长度的term。 term通过连续的整数来标记。 每个term从一个选举开始, 如session 5.2所述, 有一个或多个candidate试图成为leader。 如果一个candidate赢得选举, 在剩下的term, 它就是leader。在一些场景下, 选举会导致脑裂, 在这种情况下, 该term没有leader, 一个新的term(同时一个新的选举)随后会开始。 Raft会确保在一个term里面最多只有一个leader。
不同的服务器可能观察到在不同时间点term之间的状态转换, 在一些场景下一个服务器可能看不到一个选举甚至整个term。 Term在Raft中扮演了逻辑时钟【12】的角色, 并且它们允许服务器删除过时的信息, 例如,旧的leader。 每一个服务器保存了一个当前term的数字, 它是随着时间一直增加的。 当前的term值会在服务器之间通信进行交换, 如果一个服务器的当前term值小于其他的, 它就更新当前的term到表较大的那个数值。 如果一个candadate或者leader发现它自己的term值过时了, 它立刻把自己变成follower状态。 如果一个服务器接收到一个过时的term, 它拒绝该请求。
Raft服务器间的通信使用远程程序调用(RPC), 并且一致性算法只需要两种类型的RPC。RequestVote RPC是由candidate在选举的时候发起(Session5.2), AppendEntries RPC是由leader发起来复制日志以及提供的一种心跳的方式(session 5.3)。 服务器会重新发送RPC如果在时间限制的方式下没有收到响应, 并且为了性能RPC的发送是并行的。
5.2 Leader election(选主)
Raft 使用心跳的机制来触发选主。 当服务器启动的时候, 它们首先是follower, 一个服务器只要能从leader或者candidate收到有效的RPC, 它会一直是follower。Leader给所有的follower发送周期性的心跳(没有携带日志的AppendEntries RPC)来得到leader的权限。如果一个follower在一段称为election timeout的时间内没有收到与Leader的通信, 它认为没有有效的leader, 它会发起一次新的选举来选举新的leader。
要开始一次新的选举, 一个follower会增加它的term值并且把它的状态变为candidate。 然后它开始为自己投票, 然后向集群内的其他服务器同步的发出RequestVote RPC。 在如下3中情况发生的时候,candidate的状态会发生变化:(a)它赢得选举, (b)另外一个服务器把它自己设为leader,(c)一段时间过后没有选举成功。这些结果会在本节下面分别讨论。
如果一个candidate收到集群内有相同term的大多数服务器的选举, 它赢得了选举, 在某一个term内, 每一个服务器最多只能向一个candidate投票, 它依据先到先服务的原则(注意, Session5.4 为投票添加了一个限制)。大多数的原则确保在一个特定的term内最多有一个candidate 能够选举获胜(图3选举的安全属性)。 一旦一个candidate赢得选举, 它变成leader, 然后它向其他的服务器发送心跳消息来建立它的权限, 并且阻止新的选举。
第三种可能的结果是一个candidate既没有赢得选举也没有输掉选举:如果有很多的follower同时变成了candidate, 脑裂可能出现,导致没有candidate能够获得大多数选票。 当这种情况发生时, 每一个candidate都会超时并且开始新一轮的选举(通过增加term值和发起另一轮RequestVote RPC)。 但是, 没有其他的措施, 这样的脑裂会无限的重复下去。
Raft使用随机选举超时时间来确保脑裂现象很少见并且可以很快被解决。要避免脑裂, 选举超时时间从一个固定的区间(比如, 150-300毫秒)任意选择, 这样扩展服务器的超时时间分布, 在大部分情况下只有一个服务器会超时,它在其他服务器超时之前赢得选举并且发出心跳消息。同样的机制被用来处理脑裂, 每一个candidate在开始选举时设定一个任意的超时时间, 它等待超时时间的到来然后开始新一轮选举,这降低了在新的选举中发生另一个脑裂的可能性。 Session 8.3显示这种方法能快速选出leader。
选举是可理解性如何指导我们在可选的设计中决策的一个例子。 原本我们计划采用一种等级系统: 每一个candidate被赋予唯一的等级, 它被用于candidate之间的比较。 如果一个candidate发现另一个candidate的等级较高, 它会回到follower状态以便等级较高的candidate比较容易在下一次选举中获胜。 我们发现这种方式会产生细小的关于可用性的问题(一个低等级的服务器可能需要时间等待超时, 并且可能又成为candidate如果一个高等级的服务器失败, 如果这个转变太快, 它能重置选主的过程)。我们对这个算法做了几次调整, 但是每一次调整之后会有新的特殊情况出现。 最终我们得出结论随机事件重试的方法更加明显和容易理解。
5.3 Log replication(日志复制)
一旦leader被选举出来, 它开始对客户端请求提供服务, 每一个客户端请求包含一条被复制状态机执行的命令。 Leader把该命令作为一条新的日志添加到日志文件, 接着它向其他的服务器同步的发送AppendEntries RPC, 当这条日志已经被安全的复制(如下描述), leader执行命令, 将结果返回给调用的客户端。如果一个follower挂掉或者运行缓慢或者网络丢包, leader 会重新发AppendEntries RPC(甚至它已经响应了客户端)直到所有的follower实际的保存了所有的日志。
日志被按照图6 所示组织在一起, 当从leader收到日志的时候,每一条日志包含了一个状态机命令和一个term 数字, 日志内的term数字是用来检测不同日志的不一致性来确保图3 所示的属性。 每一条日志还包含一个标识它位置的index整数。
leader决定什么时候执行日志命令到状态机是安全的, 这样的日志条目被称为已提交。 Raft 确保已提交的日志是持久化的, 并且被所有的可用状态的机器执行过。 一旦leader复制一条日志到大多数的服务器上(比如, 图6中的条目7), 一条日志被称为已提交的。这也会提交leader的所有之前的日志条目, 包括前一个leader产生的日志。 Session5.4 讨论几个在leader变化后采用这种规则的细小的问题, 它也显示出采用这种已提交的定义是安全的。 Leader保持追踪要被提交的index的最大值, 并且它包含未来的Append Entries RPC (包含心跳)以便其他的服务器能够找到。一旦一个follower知道一条日志已经提交, 它执行日志的命令到本地的状态机(按照日志的顺序)。
我们设计Raft日志机制来保持不同主机上的日志有一个高等级的相关性。这样不仅简化了系统的行为并且使得行为更容易预测, 而且它是确保安全性的重要模块。Raft维护如下的属性, 它们一起构成了如图3 的日志匹配属性:
- 如果在不同的日志文件内有2个条目有相同的index和term, 它们保存着相同的命令;
- 如果在不同的日志文件内有2个条目有相同的index和term,那么之前的所有条目都是相同的;
第一个属性依据一个leader在一个给定的日志index给定的term下,最多创建一条日志, 并且日志的位置在文件内从来不会改变的事实。第二个属性由一个简单的由AppendEntries执行的检查来确保。 当发送一个AppendEntries RPC, leader会包含index和term在日志里面, 日志文件立刻前移到新的条目。如果一个follower没有在自己的日志里面找到相同index和term的条目, 那么它就拒绝新的日志条目。 这种一致性检查采用诱导的步骤: 最初的空的日志满足日志匹配属性, 每当日志文件增加它保持日志匹配属性的一致性检查。因此, 无论何时Appendentries 返回成功, leader知道follower的日志跟它的日志知道昂前条目位置都是一样的。
在正常的操作中, leader和follower的日志保持一致, 因此,AppendEntries一致性检查从来不会失败。但是, leader挂掉会留下不一致的日志(旧的leader有可能没有完全复制它的日志)。 这些不执行可能由一系列的leader和following故障组成, 图 7 显示了follower和leader的日志不同的形式。 一个follower的日志可能缺失一些存在于leader的日志条目, 它也可能包含leader不存在的额外的日志条目, 或者另种状况都存在。 日志文件内缺失的或者额外的日志条目可能延续多个term。
在Raft里, leader通过强制follower复制它自己的日志来处理不一致性, 这意味着follower的相冲突的日志被leader的日志覆盖掉。 Session 5.4显示附加一个额外的限制是安全的。
为了使follower的日志和自己的变得一致, leader必须找到两个日志文件一致的最新的条目, 删除掉从那个点之后的任意的日志条目, 并把leader从那个条目之后的所有日志条目发送给follower, 这些操作都发生在响应通过AppendEntries RPC的一致性检查。 Leader为每个follower保存了一个nextIndex, 它指的是leader要发给该follower的下一条日志的index, 当一个leader开始工作, 它把所有的nextIndex值设为它的日志的最后一条(图7 的11)。 如果follower的日志和leader的不一致, 在下一次的AppendEntries RPC一致性检查会失败, 在被拒绝之后, leader会减小nextIndex的值然后重试AppendEntries RPC, 最终 nextIndex会达到一个leader和follower日志匹配的点。 当这种情况发生, AppendEntries 会返回成功, 它删除所有冲突的日志并且把leader的日志条目(存在的话)发送给follower。一旦AppendEntries 返回成功, follower的日志和leader是相同的, 在剩下的term时间内保持同样的方式。
这个协议可以用减少拒绝AppendEntries RPC的数目的方式来进行优化, 参考【29】获得细节。
用这种机制, leader开始工作的时候不需要采取特别额外的操作来恢复日志的一致性。 它只是开始正常的操作, 日志在响应AppendEntries RPC 一致性检查时自动汇集成一致状态。 Leader从来不会重写或者删除它自己的日志条目(图3 leader只添加的属性)。
这种日志复制机制展示了预期的Session2 描述的一致性属性: Raft 可以接受, 复制以及回放新的日志条目只要大部分的服务器是可到达的, 在正常情况下, 一条新的日志需要一个RPC 就可以复制到大部分的集群成员, 某一个比较慢的follower不会影响性能。
Safety (安全性)
在前面的章节描述了Raft如何选主和日志复制, 然而, 到目前为止, 这些机制还不足以保证每一台机器都按照相同的顺序执行相同的命令。 比如, 在leader提交一些日志的时候某个follower不可用, 然后它可能被选举为leader并且覆盖掉新的日志。 结果, 不同的状态机可能按照不同的顺序执行命令。
本章通过给多个服务器可能被选为主添加限制条件来完成Raft算法。 这个限制确保在某个term内包含在前面的term内已经提交的所有日志(图3的leader完全属性)。 由这个选举的限制, 我们使提交的规则更加精确。最终, 我们展示了leader完全属性的证明草图, 并且显示它如何使得复制状态机的正确行为。
#### 5.4.1 选举限制
在任何leader-based 一致性算法, leader必须最终保存所有的已经提交的日志。 在一些一致性算法, 比如Viewstamped Replication【20】,能够选举出来一个leader, 即便它一开始没有包含所有的已提交的日志, 这些算法包含额外的机制来指示缺失的日志并把它们转移到新的leader, 或者在选举的过程或者随后。不幸的是, 这导致相当多的额外的机制和复杂性。 Raft使用一种比较简单的方法, 该方法确保在选举的时候所有之前已提交的日志在新的leader里, 不需要将那些日志转移到新的leader。这意味着,日志的转移只有一个方向, 从leader到follower, 并且leader从不覆盖已经存在的日志。
Raft 使用选举过程来阻止一个candidate赢得选举除非它包含所有已经提交的日志。一个candidate必须和集群中的大多数连接以便能够被选中, 这意味着每一条已经提交的日志至少在其中的一个服务器内。如果candidate的日志是集群内大多数服务期内最新的(这里, ”最新“ 被精确的定义如下), 那么他将包含所有的已经提交的日志。 RequestVote RPC实现了这个限制: RPC包含了candidate的日志信息, 如果投票者的日志比candidate的更新, 投票者拒绝投票。
Raft通过比较文件的最后一条的index和term来决定哪一个日志文件更新。如果日志最后一条有不同的term, 那么term比较大的日志比较新, 如果日志最后的term一样, 那么哪一个日志更长就比较新。
5.4.2 前面的term中已经提交的日志
如Session5.3 所述, leader知道当前的term的一条日志要被提交一旦该日志被保存在大多数的服务器上。 如果在提交一条日志之前leader挂掉了, 新的leader会尝试完成复制这条日志, 但是, 一个leader无法马上得出结论说这条日志已经被保存到集群的大多数服务器上需要被前一个term提交。 图8 描述了一个这样的状况, 与一条旧的日志被保存到了大多数的服务器上, 但它仍然被新的leader覆盖了。
要去除像图8 的问题, Raft从来不提交有前一个term计数复制的日志,只有来自于当前term的日志, leader才会根据计数副本来提交, 一旦一条日志以这种方式被提交, 那么所有以前的日志由于Log Match Property (日至匹配属性)被提交。有几种情况, leader能够安全的得出结论说旧的日志被提交了(比如 如果该条日志比保存在每一个服务器上), 但是Raft采取了一个更加保守的方法来简化。
Raft在提交日志规则上遭受这个额外的复杂性是因为当leader复制前一个term的日志时, 日志保留原本的term 数字。 在其他的一致性算法, 如果一个新的leader从前一个term复制, 它必须要产生新的term 数字。 Raft的方式使得日志的区别更加简单, 因为日志的term数字不会随时间和日志文件变化。 另外, 相比其他的算法, Raft新的leader从前一个term开始, 发送更少的日志条目(其他的算法在日志被提交之前必须发送大量的日志来重设term 数字)。
5.4.3 安全性的讨论
给定完整的Raft算法, 我们现在可以更精确的讨论Leader Completeness Property(leader完整属性)的内容(这个讨论是基于安全的证据, 参考Session8.2)。 我们假定Leader Completeness Property站不住脚的, 然后我们证明它是矛盾的。 假定term T的leader (leaderT)提交了一个日志,但是该日志没有被存储在以后的leader的term内, 假设最小的term U > T, 它的leader(leaderU) 没有保存该条日志。
- 在选举的时候, leaderU的日志必须已经缺少该已提交的日志(leader从来不会删除或者覆盖日志)。
- leaderT复制日志到集群的大多数, 并且leaderU获得集群内大多数的选票。这样, 至少有一个服务器(投票者)既从leaderT接收该日志,又选举leaderU 如图9所示。这个投票者是矛盾的关键。
- 该投票者必须在投票给leaderU之前已经接收来自于leaderT的已提交的日志, 否则它会拒绝来自于leaderT的AppendEntries 请求(它的当前的term要高于T)。
- 投票者在投票给leaderU的时候它还保存着该日志, 因为每一个介入的leader都包含该日志(根据假定), leader从来不会删除日志, follower只会清除和leader相冲突的日志;
- 投票者授权投票给leaderU, 因此leaderU的日志必须至少和投票者一样新, 这导致两个矛盾中的一个;
- 首先, 如果投票者和leaderU有同样的日志term, 那么leaderU的日志必定至少和投票者一样长,因此它包含投票者的所有的日志。 这是一个矛盾,因为投票者包含已提交的日志而leaderU假定是没有的;
- 否则, leaderU的最后的日志的term一定会大于投票者, 而且也大于T, 因为投票者的最后日志的term至少是T(它包含term T的已提交的日志)。前面的创建leaderU最后日志的leader一定包含该已提交的日志(根据假定)。 那么, 根据Log Matching Property , leaderU的日志也包含该条已提交的日志, 这是一个矛盾。
- 这就完成了这个矛盾。因此, term大于T的leader一定包含所有的在term T内提交的日志;
- Log Matching Property确保未来的leader也会包含间接提交的日志, 如图8(d)的index 2.
给定Leader Completness Property, 我们很容易证明图3的状态机安全属性, 并且所有的状态机按照相同的顺序执行日志(参考【29】)。
5.5 follower和candidate故障
至此, 我们我们一直关注leader故障, follower和candidate故障比leader故障更容易处理, 并且它们两个采用相同的处理方式。如果一个candidate或者follower发生故障, 那么接下来的RequestVotte 和AppendEntries RPC会失败, Raft通过无限重试的方式来处理RPC失败, 如果故障的服务器重启, 那么RPC会成功的完成。 如果一个服务器在完成RPC之后但是在响应之前故障, 它会在重启之后收到同样的RPC。 Raft的RPC是幂等的, 因此重复的RPC是没有害处的。比如, 如果一个follower收到一个AppendEntries请求, 该请求包含已经存在的日志, 它忽略请求中的日志。
5.6 计时和可用性
对于Raft其中一个需求是它的安全性不能依赖于计时:系统一定不能只是因为有些事件发生的比预期比较快或者慢而产生错误的结果。 但是, 可用性(系统以一个计时的方式响应客户端)必须不可避免的依赖于计时。 比如, 如果信息交换的时间长于通常服务器故障的时间, candidate不会一直等待, 没有一个leader, Raft不能进行下去。
leader选举是Raft 计时至关重要的一个方面, Raft能够选举和维护一个稳定的leader只要系统慢如下的计时条件:
broadcastTime ≪ electionTimeout ≪ MTBF
在这个不等式里, broadcastTime是一个服务器同步发送RPC到集群的服务器以及收到响应的平均时间, electionTimeout 是在Session5.2描述的选举超时时间, MTBF是一个服务器故障的平均时间。广播时间应比选举超时时间小一个数量级, 以便leader可靠地发送心跳消息来阻止follower发起选举,给选举超时时间一个给定的随机值的方法, 这个不等式也使得脑裂不可能发生。选举的超时时间应该比MTBF小几个数量级以便系统稳步进行。 当leader故障, 系统会在大约选举超时时间内不可用, 我们希望它只是整个时间很小的一部分。
广播时间和MTBF是底层系统的属性, 然而选举超时时间是我们必须选择的。 Raft RPC通常需要接受者持久化信息到存储系统, 因此广播时间可能在0.5毫秒到20毫秒, 依赖于存储技术。 结果, 选举超时时间可能大致在10毫秒和500毫秒之间。典型的服务器MTBF是几个月或者更多, 很容易满足计时的需求。