【翻译】Raft 共识算法:全貌

转载请注明出处:https://www.cnblogs.com/morningli/p/16768025.html

Raft 基础

raft集群由若干server组成,典型的集群包含5个server,这样可以允许两个server发生故障。这些server处于下面三种状态的一种:leader、follower、candidate。在正常运行过程中,只会有一个leader,其余的都是follower。follower不接受客户端请求,只会响应leader和follower的请求。leader处理所有的客户端请求(如果客户端发送请求给follower,follower会把请求重定向到leader)。candidate是用来选举一个新的leader的状态。状态转换见下图。

raft将时间分隔成不同长度的任期(term)。term是连续的数字。每个term由election开始,在election中一个或者多个candidate会尝试成为leader。如果candidate在election中获胜,在该任期剩余时间他都作为leader进行服务。在一些场景下一次election会产生split vote。这种情况下term会在没有leader的情况下结束;一个新的term(通过一次新的election)会很快开始。raft保证在某个term最多只有一个leader。

不同的server有可能会在不同的时间观察到term的转换,一些场景一个server可能观察不到一次election甚至整个term。term在raft中充当一个逻辑时钟(logical clock),这样可以让server检测到过期信息,比如说过时的leader。每个server存储一个current term,他是单调递增的。current term可以通过server间通信进行交换;如果一个server的current term比其他的更小,他会更新自己的current term到更大的那个值。如果一个candidate或者leader发现他的term意见过期了,他会立马转换到flower状态。server会拒绝掉带有过期的term的请求。

raft server间通信事业远程过程调用( remote procedure calls,RPCs),基本的共识算法只需要两种RPCs。RequestVote RPCs会在election中由candidate发起,AppendEntries RPCs由leader发起来复制日志和维持心跳。后面会增加第三种RPCs来传输snapshot。server如果没有接收到响应会及时地重试RPCs,并且会并行地发送RPCs来获得更好的性能。

leader 选举

raft使用心跳机制来触发leader election。server启动后都是follower。一个server只要接收到由leader或者candidate发出的有效的RPCs,他会一直维持在follower状态。leader周期性地发送心跳(没有日志内容的 AppendEntries RPCs)给所有follower来维持自己的权力。如果一个follower超过一段时间没有接收到通信,称为 election timeout,那么它认为没有可用的leader,并开始一次election来选举一个新的leader。

开始election之前,flower自增自己的current term并转换到 candidate 状态。然后给自己投票并且并行发送 RequestVote RPCs给集群中的其他server。一个candidate一直维持在该状态,直到发生以下事件:

  1. 赢得了这次election
  2. 另一个server已经建立为leader
  3. 经过了一段时间没有server胜出

一个candidate如果在同一个term内获得了集群中大多数server的投票,那么它在这次的election胜出。每个server在一个term内最多只会投票给一个candidate,根据先到先得( first-come-first-served)的规则。大多数的规则保证了在一个term内最多有一个candidate赢得这次election。一旦一个candidate赢得一次election,它成为leader。然后他会发送心跳给所有其他server来建立他的权力,防止一次新的election。

一个candidate在等待投票的过程中有可能会接收到其他server发送的AppendEntries RPC ,声明它成为了leader。如果这个leader的term(包含在它的RPCs中)至少跟这个candidate的 current term相等,这个candidate承认这个leader是合法的兵器转换到follower状态。如果在RpCs中的term比该candidate的current term小,那么这个candidate拒绝这个RPC并保持candidate状态。

第三种可能的结果是候选人在选举中既不赢也不输:如果很多flower在同一个时间成为了candidates,投票有可能会被分割导致没有candidate获得大多数投票。当这样的情况发生后,每个candidate会超时并且通过自增自己的term并发起另一轮RequestVote RPCs开始一次新的election。然而如果没有额外的措施,选票分割会无限重复。

raft使用随机election超时来保证选票分割很少发生并且可以很快解决。首先,election超时是从一个固定的区间(比如150~300ms)随机选择的。通过这样的方式分散了server,大多数情况下只会有一个server超时;它会在其他server超时之前赢得这次election并发送心跳。相同的机制会用来处理选票分割。每个candidate会在选举开始之前重新开始随机election超时,并在开始下次选举之前等待这个超时时间过去;这样会减少在新的election发生另一次选票分割的可能性。

日志复制

leader被选举出来后会开始服务客户端请求。每个客户端请求都包含一个要由复制状态机执行的命令。leader吧这个命令添加到他的日志作为一个新的记录(entry),然后并行地发送 AppendEntries RPCs给其他server复制这个记录。当这个记录被安全的复制(下面会有描述),leader应用这个记录到它的状态机并返回给客户端结果。如果follower崩溃、运行慢或者网络分组丢失了,leader会一直重发 AppendEntries RPCs(及时在响应了客户端之后)知道所有的follower最终保存了所有的日志记录。

日志是按照上图展示的一样组织的。每个日志记录保存一个状态机命令以及这个命令被leader接收时的term。日志记录中的term用啦检测日志间的不一致并保证raft的不变量。每个日志记录也包含一个index表示在日志中的位置。

leader决定什么时候应用一个日志记录到状态机是安全的;这样的记录称为commited。raft保证commited的记录是持久化的并最终会被所有的可用状态机执行。一旦一个日志记录被创建他的leader复制到了大多数server,这个日志记录是committed的。这也commit了leader日志之前的所有记录,包括以前的leader创建的记录。leader一直追踪已知被提交的最高的index,并会在以后的AppendEntries RPCs包含这个index(包括心跳包)以便其他server最终发现。一旦follower学习到一个记录是 committed的,他会应用这个记录到本地的状态机(按照日志的顺序)。

raft 日志机制是设计来保持不同server上的日志之间的高度一致性。这样不仅简化了系统的行为另它更容易被预测,而且是保证safety的重要组件。raft保持下面的属性,这些属性共同构成了 Figure 3 中提到的 Log Matching 属性。

  • 如果在不同日志的两个记录有相同的index和term,那么他们存储了相同的命令。
  • 如果在不同日志的两个记录有相同的index和term,那么所有前面的记录都是相同的。

第一条属性来自于一个leader在给定的index和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崩溃的组合情况。 Figure 7 介绍了follower有可能跟新的leader不一致的几种方式。一个follower有可能会缺失在新leader的记录,可能会有新leader不存在的记录,或者两者都有。在日志中缺失和多出的记录也许会横跨多个term。

在raft中,leader通过强制follower复制自己的日志来处理不一致。这表示在follower的日志会被leader的日志中的记录覆盖。后面会展示通过增加一些限制这样做是安全的。

为了让follower的日志跟自己的相同,leader必须找到两个日志中最后一个相同的记录,删除follower在这个点后面的记录,发送给follower所有leader日志上这个点之后的记录。所有这些操作都是为了满足AppendEntries RPCs的一致性检查。leader为每一个follower保持一个nextIndex,表示leader将会发送给这个follower的下一个日志记录的index。当leader上任时,它会初始化所有的nextIndex为它的日志最后一个记录后一个的index(Figure 7 中为11)。如果follower的日志跟leader的不一致, 下一次 AppendEntries RPCs的AppendEntries一致性检查会失败。失败后leader会自减nextIndex然后重试 AppendEntries RPC。最后nextIndex会到达一个leader和follower的日志匹配的点。当找到这个点时,AppendEntries会成功,并移除follower上所有冲突的日志并把leader的日志添加到后面(如果有的话)。一旦 AppendEntries成功,follower的日志与leader的是一致的,并且他会在这个term的剩余时间维持一致。

通过这样的机制,当leader上任时不需要任何特殊的操作来恢复日志一致性。它只需要开始正常的运行,日志会在对AppendEntries一致性检查失败的响应中自动收敛。leader从不覆盖或者删除它的日志中的记录( Figure 3 中的 Leader Append-Only Property)。

这样日志复制机制展示了理想的共识属性:只要大多数server是在线的,raft可以接收,复制,应用新的日志记录;在正常情况下一个新的记录会通过一轮RPCs复制给集群中大多数的server;单独一个慢的follower不会影响性能。

安全性

前面描述的这些机制还不足以保证每个状态机在完全相同的顺序执行完全相同的命令。举个例子,一个follower可能会在leader提交了若干个记录的时候不可用,然后被选举成了新的leader并使用新的记录覆盖了这些记录;结果是,不同的状态机可能会执行的不同的命令序列。

这个章节通过添加一个决定谁会被选举成为leader的限制来完善raft算法。这个限制保证在任何给定的term,leader都包含前一个term的所有记录(Figure 3 的 Leader Completeness Property )。鉴于这个选举的限制,我们稍后使提交的规则更加精确。最后,展示了Leader Completeness Property的证明草图,并展示了它如何导致复制状态机的正确行为。

选举限制

raft使用一个简单的方式来保证从选举的那一刻起,所有前一个term已经提交的记录会出现在新的leader,不需要传输这些记录给leader。这表示日志记录只会从leader流向follower,leader绝不会覆盖它日志中已经存在的记录。

raft使用投票程序来防止一个没有包含所有已提交记录的candidate赢得election。一个candidate为了选举必须与集群中大多数server进行通信,这意味着每一个已提交的记录至少会存在于这些server中的一个。如果这个candidate的日志至少跟其他大多数server中的日志一样新,那么它包含了所有的已提交记录。RequestVote RPC 实现了这样的限制:RPC包含了candidate的日志信息,voter 会在它自己的日志比这个candidate更新时拒绝投票。

raft通过比较日志中最后一个记录的index和term来决定两个日志哪个是更新的。如果两个日志最后一个记录的term不同,更大的term认为是更新的。如果最后一个记录的term相同,那么更长的日志是更新的。

前一个term的记录的提交

一个leader知道它自己的term的记录只要被存储到大多数server中就是committed的。如果leader在提交一个记录之前崩溃了,后来的leader会努力完成这个记录的复制。然而,当前一个term的记录被存储到大多数server,一个leader不能马上得到结论这个记录是committed。 Figure 8介绍了一个场景,一个老的日志记录已经存储到大多数的server,但仍会被未来的leader覆盖。

为了消除类似 Figure 8 的问题,raft从不通过计算副本数来提交以前的term的记录。只有leader当前term的记录通过这样的方式提交,所有前面的记录会因为 Log Matching Property被间接提交。有一些场景leader可以安全地得到结论一个老的日志记录是commited的(举个例子,如果这个记录存储在所有的server),但是raft为了简单起见使用了更保守的方式。

raft 在提交规则中产生了这种额外的复杂性,因为当leader 从之前的term复制记录时,日志记录会保留其原始的term。在其他共识算法中,如果一个新的leader复制前一个term的记录,它必须使用新的term。raft的方法使得对日志记录的推理更容易,因为它们随着时间和跨日志保持相同的term。此外,与其他算法相比,raft 中的新leader发送的先前term的日志记录更少(其他算法必须发送冗余日志记录以重新编号,然后才能提交)。

安全性论证

在给出了完整的raft算法之后,我们限制可以更加精确地讨论 Leader Completeness Property。我们假设不存在 Leader Completeness Property,然后证明存在矛盾。给定term T的leader(leaderT)在它的term内提交了一个日志记录,但是这个记录不存在未来的某些term。假设最小的term U > T,该term的leader(leaderU)没有保存这个记录。

  1. 这个已提交的记录在leaderU选举的时候一定不在它的日志中(leader不会删除或者覆盖记录)
  2. leaderT复制这个记录到集群中的大多数server,leaderU接受到集群中大多数server的投票。所以至少有一个server(“the voter”)既接受了leaderT的记录也投票给了leaderU,跟Figure 9展示的一样。这个voter是推出矛盾的关键。
  3. 这个voter一定是在投票给leaderU之前接受leaderT的committed记录;否则他会拒绝leaderT的这个 AppendEntries请求(它的 current term 会比T更大)。
  4. 这个voter在给leaderU投票的时候仍保存这个记录,因为根据假设每一个中间的leader都包含了这个记录,follower只有在跟leader存在冲突的时候才会删除记录。
  5. 这个voter给leaderU投票,所以leaderU的日志至少跟这个voter的日志一样新。这导致了两个矛盾其中的一个。
  6. 第一种情况,如果这个voter和leaderU最后一个记录的term相同,那么leaderU的日志必须至少跟这个voter一样新,所以它的日志包含在voter日志中的每一个记录。这是一个矛盾,因为根据假设voter包含了leaderU没有的 committed 记录。
  7. 另一种情况,leaderU的最新的日志term肯定比voter更大。而且,它比T大,因为voter的最新的日志term至少是T(它包含了term T的记录)。根据假设,创建了leaderU最后一个日志记录的前面的leader一定在它的日志里包含了这个已提交的记录,这是一个矛盾。
  8. 这里完成了矛盾的推理。因此,所有比T大的term的leader都包含了所有在term T 提交的记录。
  9. Log Matching Property 保证了未来的leader也会间接包含已提交的记录。

最后,raft要求server按照日志的index顺序应用记录。结合 State Machine Safety Property,这意味着所有的server将会准确地按照同样的顺序应用相同的记录到它们的状态机中。

follower和candidate崩溃

follower和candidate崩溃比leader崩溃处理起来更简单。如果一个follower或者candidate崩溃,将来发送给它的RequestVote和AppendEntries RPCs会失败。raft通过无限地重试来处理这种失败;如果崩溃的server重启,RPC会成功完成。如果server在完成 RPC 并在响应之前崩溃,那在它重启后会重新收到相同的RPC。raft RPCs是独立的,所以这种场景不好导致问题。举个例子,如果follower收到一个AppendEntries请求包含了已经存在在日志中的记录,它会忽略新请求中的这些记录。

时间和可用性

raft的一个要求是安全性不能够依赖于时间:系统不能仅仅因为某些事件发生得比预期快或慢而产生错误的结果。然而,可用性(系统及时响应客户的能力)不可避免地依赖时间。举个例子,如果消息交换比服务器故障间隔时间长,候选人将没有足够长的时间来赢得选举。没有一个稳定的leader,raft将无法工作。

leader选举是 raft 中时间最为重要的方面。只要系统满足下面的时间要求,raft可以选举并维持一个稳定的leader:

broadcastTime << electionTimeout << MTBF 

在这个不等式中,broadcastTime是一个server并发地发送RPCs到集群中每个server并接收到响应的平均时间;electionTimeout是前面讲到的选举超时;MTBF是一个server两次故障间的平均时间。广播时间应该比选举超时小一个数量级,这样leaser可以可靠地发送心跳消息防止follower开始选举;通过随机化选举超时时间的方法,这个不等式也使得选票分割的情况变得不可能。选举时间应该比MTBF小几个数量级,这样系统可以稳定运行。当leader崩溃,系统会在大约选举超时的时间内不可用;我们希望这仅仅占用总时间的很小一部分。

广播时间和MTBF是系统的属性,但是选举超时是我们必须选择的。raft的RPCs通常要求接受者持久化消息到稳定的存储,这样广播时间有可能在0.5ms到20ms的范围,取决于使用的存储。这样导致选举超时很可能是在10ms到500ms的范围。通常server的MTBFs是几个月以上,很容易满足时间要求。


参考:
https://github.com/maemual/raft-zh_cn

posted @ 2022-10-09 20:48  morningli  阅读(78)  评论(0编辑  收藏  举报