Raft论文

Posted crazstom

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Raft论文相关的知识,希望对你有一定的参考价值。


5.  Raft共识算法

Raft通过选举leader来实现一致性,然后leader拥有完全的能力来管理复制日志。leader从客户端获取日志条目,复制到其他的服务器中,告诉他们什么时候应用这个日志到状态机是安全的。leader这个角色简化了复制日志的管理。例如,leader可以在不咨询其他服务器的前提下添加新的日志条目,以及数据只从leader流向其他服务器。如果一个leader宕机或者失联了,一个新的leader就会被选举。

Raft把一致性问题分解为三个独立的部分:

  • leader选举。当现有的leader失败后,新的leader必须被选举产生。

  • 日志复制。leader必须接收客户端的日志条目然后通过集群复制,强制其他服务器的日志和leader的一样。

  • 安全性。如图3所示。如果一个服务器应用了一条日志,那么其他的服务器应用的相同条目的日志内容就是一样的。

State:

  1. 所有服务器上的持久化状态,在回复RPC之前更新持久化存储

    • currentTerm:服务器知道的最近任期,当服务器启动时初始化为0,单调递增

    • votedFor:当前任期中,该服务器给投过票的candidateId,如果没有则为null

    • log[]:日志条目;每一条包含了状态机指令以及该条目被leader收到时的任期号

  2. 所有服务器上的易失性状态

    • commitIndex:已知被提交的最高日志条目索引号,一开始是0,单调递增

    • lastApplied:应用到状态机的最高日志条目索引号,一开始为0,单调递增

  3. leader上的易失性状态,在选举之后重新初始化

    • nextIndex[]:针对所有的服务器,内容是需要发送给每个服务器下一条日志条目索引号(初始化为leader的最高索引号+1)

    • matchIndex[]:针对所有的服务器,内容是已知要复制到每个服务器上的最高日志条目号,初始化为0,单调递增

Raft论文(二)

AppendEntries RPC,同时leader发起,用来复制日志条目或者发送心跳:

  1. 参数

    • term:leader的任期号

    • leaderId:用来让follower把客户端请求定向到leader

    • prevLogIndex:紧接新条目之前的日志条目索引(当前最大的日志条目索引)

    • prevLogTerm:prevLogIndex的任期

    • entries[]:储存的日志条目(如果某条目是空的,它就是心跳;为了提高效率可能会发出不止一条日志)

    • leaderCommit:leader的commitIndex

  2. 结果

    • term:当前任期,用来让leader更新自己

    • success:如果follower包含的日志匹配参数汇总的prevLogIndex和prevLogTerm,返回true

  3. 接收者的实现

    • 如果参数term小于接收者的currentTerm,返回false

    • 如果参数的term和prevLogTerm相等的的日志中不包含prevLogIndex的条目,返回false

    • 现有条目与新条目(索引相同但任期不同)发生冲突,删除当前及以后的所有条目

    • 添加不在日志中的新条目

    • 如果leaderCommit>commitIndex,设置commitIndex=min(leaderCommit, 上一个新条目的索引)

Raft论文(二)

RequestVote RPC,candidate发起来收集投票:

  1. 参数

    • term:candidate的任期号

    • candidateId:发起投票的candidate的ID

    • lastLogIndex:candidate的最高日志条目索引

    • lastLogTerm:candidate的最高日志条目的任期号

  2. 结果

    • term:服务器的当前任期号,让candidate更新自己

    • voteGranted:如果是true,意味着candidate收到了选票

  3. 接收者实现

    • 如果参数中的term<接收者的currentTerm,返回false

    • 如果服务器中的votedFor是null或者参数中的candidateId,而且candidate的日志至少和接收者的日志一样新(up-to-date),获得选票

Raft论文(二)

服务器的规则:

  1. 所有的服务器

    • 如果commitIndex>lastApplied:增加lastApplied,应用log[lastApplied]到状态机

    • 如果RPC的请求或者回复中的term T>currentTerm:设置currentTerm=T,转为follower

  2. Follower

    • 回复来自candidate和leader的RPC

    • 如果经过了选举超时(election timeout)还没有收到当前leader的AppendEntries或者candidate的投票请求:转为candidate

  3. candidate

    • 增加currentTerm

    • 为自己投票

    • 重设选举定时器

    • 给所有的服务器发送RequestVote RPC

    • 转为candidate之后,开始选举:

    • 如果收到大多数服务器的选票:成为leader

    • 如果收到新leader的AppendEntries RPC:转为leader

    • 如果经过了选举超时(选举定时器到达了):开始一个新的选举

  4. leader

    • 如果成功:更新follower的nextIndex和matchIndex

    • 如果因为日志的不一致导致失败:leader减小nextIndex然后重试

    • 在选举之前:发送空的AppendEntries RPC(心跳)给所有的服务器,一段时间后重复发送来防止选举超时的发生

    • 如果收到了客户端指令:将新的条目添加到本地日志中,在应用到状态机之后返回结果

    • 如果最大的日志索引>=follower的nextIndex:发送从包含nextIndex开始的日志条目的AppendEntries RPC:

    • 如果存在一个N,N>commitIndex,大部分的matchIndex[i]>=N。而且log[N].term==currentTerm,设置commitIndex=N

Raft论文(二)

选举安全:在给定的期限内,最多只能选举一位leader。§5.2

只能leader添加日志:leader永远不会覆盖或删除其日志中的条目;§5.3

日志匹配:如果两个日志包含具有相同索引和任期的条目,则在给定索引之前的所有条目的内容都是相同的。§5.3

leader完整性:如果某一任期内一条日志被提交,则该条目将出现在之后任期leader中。§5.4

状态机安全性:如果服务器已在其状态机应用给定索引日志条目,则其他任何服务器都不会在该索引处应用其他内容的日志条目 §5.4.3

图3:Raft保证这些属性中的每一个始终都是真实的。章节编号指示每个属性的讨论位置

5.1.  Raft基本内容

一个Raft集群包括少数服务器;5是典型的数量,这允许系统可以容忍两个服务器的失败。任何时候,服务器是leader、follower和candidate三个中的一种状态。在正常操作中,只有一个leader,而其他所有服务器都是follower。follower是被动的:他们自己不发出请求,而只是响应leader和candidate的请求。leader处理所有客户请求(如果客户联系follower,则follower将其重定向到leader)。candidate,用于选举新的领导者。

Raft论文(二)

图4:服务器状态。follower仅响应来自其他服务器的请求。如果follower没有收到任何通信,它将成为candidate并发起选举。从整个集群的大多数中获得选票的candidate将成为新的leader。leader通常一直运行到宕机。

Raft将时间划分为一个个任意长度的任期(term),如图5所示。任期用连续的整数编号。每个任期开始一次选举(election),其中一个或者更多的candidate试图成为leader。如果candidate在选举中获胜,则在该任期剩余时间中将担任leader。在某些情况下,选举将导致投票分裂。在这种情况下,任期将以没有leader的情况结束;新的任期(新的选举)会很短。Raft保证一个任期内最多只有一个leader。

Raft论文(二)

图5:时间分为多个任期,每个任期以选举开始。选举成功后,由一位leader管理集群直到任期结束。假如选举失败,在这种情况下,该任期没有选择leader就结束了。任期之间的过渡可能会在不同时间在不同服务器上被观察到。

不同的服务器可能会在不同时间观察到任期的切换,并且在某些情况下,服务器可能不会观察到选举甚至整个选举过程。任期充当Raft中的逻辑时钟[14],它们允许服务器检测过时的信息,例如旧的leader。每个服务器存储一个当前任期号,该任期号随时间单调增加。每当服务器进行通信时,都会交换当前任期号(currentTerm);如果一台服务器的currentTerm小于另一台服务器,则它将其currentTerm更新为较大的值。如果candidate或leader发现其任期已过时,它将立即恢复为follower。如果服务器接收到具有过期任期编号的请求,则它将拒绝该请求。

Raft服务器使用远程过程调用(RPC)进行通信,并且基本共识算法仅需要两种类型的RPC。RequestVote RPC由candidate在选举期间启动,而AppendEntries RPC由领导者启动用来复制日志条目并提供一种形式的心跳。第7节添加了第三个RPC,用于在服务器之间传输快照。如果服务器未及时收到响应,服务器将重试RPC,并且它们并行发出RPC以获得最佳性能。

5.2.  leader选举

Raft使用心跳机制来触发leader的选举过程。当服务器开始工作,它们成为follower。只要服务器收到来自leader或者candidate有效RPC,他就会保持follower状态。leader向所有follower发送周期性的心跳信号(不携带日志条目的AppendEntries RPC),以维护其领导地位。如果follower在称为选举超时(election timeout)的时间段内没有任何通信,则它假定没有可行的leader,并开始竞选新leader。

为了开始一次选举,follower增加它的当前任期号,然后转换为candidate状态。它为自己投票,而且给集群中所有的服务器并行发出RequestVote RPC。一个candidate继续保持它的状态直到有以下一种情况发生:

  1. 它赢得了选举

  2. 另一个服务器成为了leader

  3. 没有leader产生

在一个任期内,如果收到大多数服务器投票,candidate就赢得了选举。每个服务器在一个任期中最多给一个candidate投票,而且是基于先来先服务原则(5.4节添加了投票的额外限制)。这个大多数规则保证了最多一个candidate可以赢得某一任期的选举(图3的选举安全性)。一旦一个candidate赢得了选举,它就成为了leader。它给集群中所有的服务器发送心跳以建立它的地位,同时也阻止了新的选举发生。

当等待投票的时候,candidate可能收到另一个服务器声明已经成为leader的AppendEntries RPC。如果这个leader的任期不低于这个candidate的任期,这个candidate就承认leader的正当性然后成为follower。如果这个RPC中的任期比candidate的任期小,这个candidate就就拒绝这个RPC,然后继续保持candidate状态。

第三种可能的结果是:没有candidate赢得或者输掉这个选举。原因就是多个follower同时成为candidate,投票会分散给各个candidate,从而没有candidate获得了大多数的票。当这种情况发生,每个candidate会等到超时然后增加它的任期号开始新一轮的选举,并发出RequestVote RPC。然而,没有额外的措施的话,分裂投票的情况还会出现。

Raft使用随机的选举超时(randomized election timeout)来解决分裂投票。为了防止分裂投票,选举超时在一定的范围(150-300ms)内随机获得。这会把所有的服务器分散开来,因此在大多数情况下,只有一个服务器会经历完选举超时。它在其他服务器到达选举超时之前赢得选举和发送心跳。每个candidate在选举开始时都会重新新的选举定时器,并等待该定时结束后再开始下一次选举;这减少了新选举中再次分裂投票的可能性。第9.3节显示,这种方法可以迅速选举出一位leader。

5.3.  复制日志

一旦一个leader被选举产生,它就开始处理客户端请求。每个客户请求包含了状态机指令。leader将该指令作为新条目添加到日志中,然后发起并行AppendEntries RPC给所有的服务器来复制这条日志。当该条目已经被大多数服务器复制,leader应用该指令到它的状态机,然后返回执行的结果给客户端。如果follower宕机或者运行缓慢,又或者丢包了,leader继续重复发送AppendEntries RPC(即使它已经回复了客户端)直到所有的follower的日志和leader的相同。

Raft论文(二)

图6:日志由条目组成,这些条目按顺序编号。每个条目包含创建它的任期(每个框中的数字)和状态机指令。如果可以安全地将该条目应用于状态机,则认为该条目被提交。

日志按照图6所示的方式管理。每条日志条目储存一条状态机指令以及leader收到该指令时的任期。日志条目中的任期号用来检测日志的不一致来保证图3中的一些特性。每个日志条目还有一个数字索引表示它在日志中的位置。

leader决定什么时候应用日志条目到状态机是安全的;这样的话该条目就是被提交(committed)。Raft保证提交的条目被持久化并且最终被所有的状态机执行。一旦leader创建了一个条目并复制到大部分的服务器中,这个条目就是被提交了(committed)(图6中的条目7)。这还将提交leader日志中的所有先前条目,包括之前leader创建的条目。5.4节讨论了leader变更后应用此规则时的一些细微之处,并且提交的定义是安全的。leader记录它要提交的最高索引,并将该索引包括在未来的AppendEntries RPC(包括心跳)中,以便其他的服务器知晓。follower得知日志条目已提交后,便将该条目应用于其本地状态机(按日志顺序)。

我们设计了Raft日志机制,以维护不同服务器上的日志高度一致性。这不仅简化了系统的行为并使其更加容易预测,而且是确保安全性的重要组成部分。Raft维护以下属性,它们共同构成图3中的Log Matching属性:

  • 如果两个在不同日志的条目有相同的索引和任期,那么他们储存同样的指令

  • 如果两个在不同日志中的条目有相同的索引和任期,这两个日志中该索引之前的条目都是相同的

第一个属性来自以下事实:在一个任期中,leader最多在指定索引处创建一个条目,并且日志条目从不改变其在日志中的位置。第二个属性由AppendEntries执行的简单一致性检查保证。发送AppendEntries RPC时,leader在其中包括其日志中下一条新条目之前的索引和任期(当前已提交的最大索引和任期)。如果follower在其日志中找不到具有相同索引和任期的条目,它拒绝该条目。一致性检查是一个归纳步骤:日志的初始空白状态满足LogMatching属性,并且每次扩展日志时,一致性检查都会维持Log Matching属性。结果,只要AppendEntries成功返回,leader就会知道follower的日志和自己的新条目之前的日志完全相同。

正常运行中,leader和follower的日志保持一致,所以AppendEntries的一致性检查不会失败。但是,leader宕机会导致日志不一致(上任leader可能没有完全复制它的日志)。这些非一致性可能通过一系列的leader或者follower的宕机复合而成。图7描述了follower可能和新leader的日志不一致的情况。一个follower可能没有leader的某些条目,又或者它有的条目现任leader没有,又或者两个都出现。日志中缺少和多余的条目可能跨越多个任期。

Raft论文(二)

图7:当最高leader掌权时,在follower日志中可能会出现任何情况(a–f)。每个框代表一个日志条目;框中的数字是其任期。follower可能缺少条目(a–b),可能有多余的未提交条目(c–d)或两者(e–f)。例如,如果该服务器是任期2的leader,则可能会发生方案(f),在其日志中添加了几个条目,然后在提交任何条目之前崩溃了;它迅速重启,成为任期3的leader,并在其日志中添加了更多条目;在提交第2项或第3项中的任何一项之前,服务器再次崩溃并保持关闭状态。

在Raft中,leader通过强制follower复制leader的日志来解决不一致问题。这意味着follower中冲突的条目会被leader的日志覆盖。5.4节会再加上一个限制条件保证安全性。

为了使follower的日志与自己的日志保持一致,leader必须找到两个日志中相同的最新日志条目,然后删除follower日志中该条目之后的所有内容,然后将leader中该条目之后的所有条目发给follower。所有这些操作用来响应AppendEntriesRPC执行的一致性检查。leader为每个follower维护一个nextIndex,这是leader将发送给该follower的下一个日志条目的索引。当leader刚选举出来时,它将所有nextIndex值初始化为其日志中的最大索引之后的索引(图7中的11)。如果follower的日志与leader的日志不一致,则下一个AppendEntries RPC中的一致性检查将失败。失败之后,leader递减nextIndex并重试AppendEntries RPC。最终nextIndex将到达leader和follower日志匹配的点。至此,AppendEntries将成功执行,这将删除follower日志中的所有冲突条目,并从leader的日志中添加附加条目(如果有)。一旦AppendEntries成功,follower的日志将与leader的日志保持一致,并且在本任期的其余部分中将保持这种状态。

如果需要,可以优化协议以减少拒绝的AppendEntries RPC的数量。例如,当拒绝一个AppendEntries请求时,follower可以在其中包括冲突条目的任期以及该任期下第一条储存的条目索引。有了这些信息,leader可以递减nextIndex来绕过该任期中所有冲突的条目。每个具有冲突条目的任期都需要一个AppendEntries RPC,而不是每个条目一个RPC。实际上,我们怀疑这种优化是否必要,因为失败很少发生,并且不太可能出现许多不一致的条目

通过这种机制,leader掌权时无需采取任何特殊措施即可恢复日志的一致性。它刚刚开始正常运行,并且响应AppendEntries的一致性检查失败,日志自动收敛。leader永远不会覆盖或删除其自己的日志中的条目(图3中的Leader Append-OnlyProperty)

这种日志复制机制展现了第2节中描述的一致性属性:只要大多数服务器都工作,筏可以接受,复制和应用新的日志条目。通常情况下,可以通过一轮RPC将新条目复制到大多数集群中;一个慢速追随者不会影响性能。

5.4.  安全性

前面的部分描述了Raft如何选举leader并复制日志条目。但是,到目前为止描述的机制还不足以确保每个状态机以相同的顺序执行完全相同的命令。例如,follower可能leader提交日志的时候宕机,之后它可能当选新leader,用新的条目覆盖这些日志;结果,不同的状态机可能会执行不同的命令。

本部分通过限制可以选举leader的服务器来完成Raft。这个限制保证任何任期leader包含之前所有提交的日志(图3的leader Completeness Property)。选举条件限制使提交日志规则更加精确。最后,我们为Leader Completeness属性提供了证明草图。

5.4.1.  选举限制

在任何基于leader的共识算法中,leader存储所有提交的日志条目。在某些共识算法中,例如Viewstamped复制算法,即使最初并未包含所有已提交的条目,也可以选择一个leader。这些算法包含其他机制,可以识别丢失的条目并将其发送给新的leader。不幸的是,这导致相当大的附加机制和复杂性。Raft使用了更简单的方法,它保证上任leader所有提交的条目在新leader中都存在,不需要把这些条目传递给新leader。这意味着日志条目只有一个流向,从leader到follower,而且leader从不修改它已存在的日志。

Raft使用投票过程来阻止没有所有提交日志的candidate赢得选举。candidate必须联系集群的大多数才能被选举,这意味着每个提交的条目都必须至少存在于这些服务器中的一个里。如果candidate的日志与大多数服务器的日志(在下面精确定义了"up-to-date")一样新(up-to-date),则它将保存所有提交的条目。RequestVote RPC实施了此限制:RPC包含有关candidate日志的信息,如果follower自己的日志比candidate的日志新,则拒绝投票。

Raft通过比较日志中最后一个条目的索引和任期来确定两个日志中哪个是最新(up-to-date)。如果日志中的最后一个条目具有不同的任期,则带有较新任期的日志将是最新的。如果日志以相同的任期结尾,则更大索引的日志是最新的。

5.4.2.  提交上一任期的日志条目

如5.3节描述,一旦一条日志被大多数的服务器储存,leader就知道该条目被提交了。如果一个leader还没有提交一条日志就宕机了,之后的leader会尝试完成该条目日志的复制。然而,一个leader不能马上断定上一任期的日志是否提交了,即使被大多数服务器复制了。图8描述了一种情景,原来的日志条目被储存在大多数服务器中,但是仍然可以被新leader修改。

Raft论文(二)

图8为一个时序图,显示leader为何无法使用旧任期中的日志条目来执行提交。在(a)中,S1是leader,并且向其他服务器部分复制了索引2处的日志条目。在(b)中,S1崩溃;S5被选为任期为3的leader,S3,S4以及他自己投的票,并且在索引为2的地方接受了不同的条目。在(c)中S5崩溃; S1重新启动,被选为leader,并继续复制。此时,任期2中的日志条目已在大多数服务器上复制,但是尚未提交。如果S1像(d)中那样崩溃,之后S5被选为leader(来自S2,S3和S4的投票),并用任期3中的条目覆盖该条目。但是,如果S1在宕机之前从当前任期把日志条目复制给了大多数服务器,如(e),那么S5的条目2是无法提交的(S5无法赢得选举)。此时,日志中的所有先前条目也将被提交。

为了消除类似图8中的问题,Raft不使用统计副本的方式来提交上一任的日志条目。只有当前任期的日志条目才会通过统计副本来提交;基于Log Matching属性,一旦当前任期的日志条目以这种方式提交之后,之前的所有条目也都间接地提交了。在某些情况下,leader可以得出结论:之前的日志已经提交(例如,该条目存储在每个服务器上),但是Raft为简化起见采取了更为保守的方法。

当leader复制之前任期里的日志时,会为保留原始的任期号, 这使提交日志的规则引入额外的复杂性。在其他的一致性算法中,如果一个新的leader要重新复制之前的任期里的日志时,它必须使用当前的任期号。Raft 使用的方法更加容易辨别出日志,因为它可以随着时间和日志的变化对日志维护着同一个任期编号。另外,和其他的算法相比,Raft 中的新leader只需要发送更少日志条目(其他算法中必须在他们被提交之前发送更多的冗余日志条目来为他们重新编号)。

5.4.3.  安全性论证

有了完整的Raft算法,Leader Completeness属性可以被更精确的论证(这个论证基于9.2节的安全保证)。假设Leader Completeness Property不成立,然后推导出矛盾。假如任期T的leader提交了一条日志,但是这个日志没有被未来任期的leader储存。例如任期U>T的leader(   )没有储存这个日志条目。

  1.    在选举的时候日志中没有该条目(leader不会删除或者覆盖它自己的日志条目)

  2.    把这条日志复制到集群大部分服务器上,   收到了集群大多数服务器的投票。因此,至少一个服务器不仅接收了   的日志条目,还给   投票了,如图9所示的S3。这个服务器就是矛盾出现的关键。

Raft论文(二)

图9:如果任期T的leader S1在它的任期提交了一条日志,S5是新任期U选举的leader,那么肯定有一个S3既接受了日志又给S5投票。

  1. 服务器在给   投票之前必须接受   通过AppendEntries提交的日志条目;否则它会拒绝   的AppendEntries请求,因为它当前的任期可能比   要高

  2. 服务器投票给   时,仍然存储该条目,因为每个leader都包含该条目,leader从不删除条目,而follower仅在与leader发生冲突时才删除条目。

  3. 服务器给   投票,所以   的日志中必须已经和服务器的一样新(up-to-date)。这会导致两个矛盾中的一种。

  4. 首先,如果服务器的上一任期号和   相同,那么   的日志必须至少与服务器的日志一样新,因此   的日志包含选民日志中的每个条目。这是自相矛盾的,因为选民中有提交的条目,而   在假设中没有该条目。

  5. 另外,   的最高日志条目的任期必须大于服务器的任期。而且它大于T,因为服务器的最高日志的任期至少为T(包含T任期中提交的日志条目)。创建   的最高日志条目的前任leader必须已在其日志中包含提交的条目(假设)。然后,通过Log Matching属性,   的的日志还必须包含已提交的条目,这是矛盾的。

  6. 这样就有了所有矛盾。因此,所有任期T之后的leader必须包含任期T中提交的所有日志条目。

  7. Log Matching属性保证未来的leaders间接包含提交的日志条目,如图8的索引为2的条目。

通过Leader Completeness属性,可以证明图3中的State Machine Safety属性,这个特性的内容是:如果服务器已将给定索引的日志条目应用于其状态机,则其他任何服务器在该索引处都不会应用不同的日志条目。服务器将日志条目应用于其状态机时,其日志必须与通过该条目的leader的日志相同,并且必须提交该条目。现在考虑任何服务器应用给定日志索引的最低任期。Log Completeness属性可确保所有更高任期的leader都将存储相同的日志条目,之后任期中应用该索引处条目的服务器将应用相同的值。因此,State Machine Safety属性成立。

最后,Raft要求服务器以日志索引的顺序应用条目。结合State Machine Safety属性,这意味着所有服务器将以相同的顺序将完全相同的日志条目集合应用于其状态机。

5.5.  follower和candidate宕机

到目前为止,我们只关注leader的失败。follower和candidate崩溃比leader崩溃更容易处理,并且它们都以相同的方式处理。如果follower或者candidate崩溃,则将来发送给它的RequestVote和AppendEntries RPC将失败。Raft通过无限期重试来处理这些故障;如果崩溃的服务器重新启动,则RPC将成功完成。如果服务器在完成RPC之后但在响应之前崩溃了,则重新启动后它将再次收到相同的RPC。Raft的RPC是幂等的,因此不会造成问题。例如,follower收到一个AppendEntries请求,内容是已经保存的日志条目,它会忽略这些条目。

5.6.  时间和可用性

我们对Raft的要求之一是安全性不得依赖于运行的时间顺序:系统不得仅由于某些事件比预期的发生得更快或更慢而产生错误的结果。但是,可用性(系统及时响应客户端的能力)必然取决于时间。例如,如果信息传递花费的时间比服务器两次宕机之间稳定运行的时间要长,candidate没有足够的时间选举;没有稳定的leader,Raft无法执行任何操作。

leader选举是Raft最重要的方面,时间至关重要。只要系统满足timing requirement,Raft将能够选举和维持稳定的leader:

broadcastTime<<electionTimeout<<MTBF

在这个不等式中,broadcastTime是服务器与集群中的每个服务器并行发送RPC并接收其响应所花费的平均时间;electionTimeout是5.2节中描述的选举超时;MTBF是单个服务器两次故障之间的平均时间。broadcastTime应比electionTimeout小一个数量级,以便leader可以可靠地发送心跳消息,而且防止follower开始选举;考虑到使用随机方法实现选举超时,也使分裂投票变得不太可能。electionTimeout应比MTBF小几个数量级,以使系统稳定运行。当leader崩溃时,该系统将在大约electionTimeout时间内不可用;我们希望这只代表总时间的一小部分。

broadcastTime和MTBF是底层系统的属性,而elelctionTimeout是我们必须选择的东西。Raft的RPC通常需要接收者将信息持久化,因此broadcastTime可能从0.5毫秒到20毫秒不等,具体取决于存储技术。因此,electionTimeout可能在10毫秒至500毫秒之间。典型服务器的MTBF一般是几个月甚至更长,很容易就满足了timing requirement。

6.  集群成员变化

到目前为止,我们一直假设集群的配置(参与共识算法的服务器集合)是固定的。实际上,有时需要更改配置,例如在服务器失败时替换服务器或更改复制程度。尽管可以通过使整个集群脱机,更新配置文件,然后重新启动集群来完成此操作,但这将使集群在转换期间不可用。此外,如果有任何手动步骤,则可能会导致操作员出错。为了避免这些问题,我们决定自动进行配置更改,并将其合并到Raft共识算法中。

为了确保配置更改机制的安全,在过渡期间不能可以同时选举两个leader。不幸的是,任何将服务器直接从旧配置切换到新配置的方法都是不安全的。不可能一次自动切换所有服务器,因此集群在过渡期间可能会分成两个独立的多数服务器集合(参见图10)。

Raft论文(二)

图10:直接从一种配置切换到另一种配置是不安全的,因为不同的服务器将在不同的时间进行切换。在图10中,群集从三台服务器增长到五台。不幸的是,在某个时间点上,可以为同一任期选举两个不同的leader,一个leader是从原来的配置(   )产生的,另一个是新配置(   )产生的。

为了确保安全,变更配置必须使用两阶段方法。有两种方法可以实现。例如,某些系统(例如[22])使用第一阶段来禁用旧配置,因此它无法处理客户端请求;然后第二阶段启用新配置。在Raft中,集群首先切换到过渡配置,称为joint consensus;提交joint consensus后,系统便过渡到新配置。joint consensus将新旧配置结合在一起:

  • 日志条目被复制到配置中的所有服务器上

  • 新旧配置下的任何服务器都可能作为leader提供服务

  • (针对于选举和提交日志条目)达成的一致分别需要在两种配置上获得大多数的支持

joint consensus允许各个服务器在不同时间切换配置,而不会降低安全性。此外,joint consensus允许集群在整个配置切换期间继续为客户端请求提供服务。

Raft论文(二)

图11:配置切换的时间线。虚线表示已创建但尚未提交的配置条目,实线表示最新的已提交配置条目。 leader首先在其日志中创建   条目,并将其提交给"   "(一个多数   服务器集合和另一个多数   服务器集合)。 然后,它创建   条目并将其提交给   的多数服务器集合。在时间上,      无法同时独立做出决策。

集群配置使用复制日志系统中的特殊条目进行存储和通信;图11说明了配置更改过程。当leader收到从   到切换配置的请求时,它将用于joint consensus(图中的   )的配置储存成一条日志,然后使用之前描述的机制复制该条目。一旦服务器将新的配置添加到其日志中,它在以后的所有决策都会使用该配置(服务器始终使用其日志中的最新配置,无论该条目是否被提交)。这意味着leader将使用   中的规则来确定何时提交   日志条目。 如果leader崩溃,一个使用   或者   配置的新leader是否能被选举出来,取决于这个获胜的candidate是否收到了   。 无论如何,   配置无法在此期间做出单方面决定。

一旦   被提交了,   或者   都不会在未经对方批准的情况下做出决定,并且Leaded Completeness属性确保只有具有   日志的服务器才能被选为leader。对于leader而言,现在创建描述   的日志条目并将其复制到集群是安全的。同样,该配置将在每台服务器收到之后生效。当按照   规则提交新配置时,旧配置就没用了,并且可以关闭不在新配置中的服务器。如图11所示,      都没有时间做出单方面决策。 这样可以保证安全。

还有另外三个要解决的问题。 第一个问题是新服务器可能最初不存储任何日志条目。 如果在这种状态下将它们添加到集群,则它们可能需要一段时间才能和其他服务器的日志一样,在此期间可能无法提交新的日志条目。为了避免可用性问题,Raft在配置更改之前引入了一个附加阶段,在该阶段中,新服务器以无表决权的成员的身份加入集群(leader向其中复制日志条目,但多数情况下不考虑它们)。一旦新服务器赶上了集群的其余服务器,配置更改就可以如上所述进行。

第二个问题是集群leader可能不是新配置的一部分。 在这种情况下,leader一旦提交了   下的日志条目,便会下台(返回到follower状态)。这意味着会有一段时间中,一个leader管理一个不包含自身的集群(当它提交   日志的这段时间);它复制日志条目但是不把它自己算作集群的多数集合。当提交   时,将发生leader转换,因为这是新配置最开始独立运行时间点(始终可以从   配置中选择一个leader)。在这之前,可能只有来自   的服务器可以被选为leader。

第三个问题是,已移除的服务器(不在   中的服务器)会破坏集群。 这些服务器不会收到心跳,因此它们将经历选举超时并开始新的选举。然后,它们将发送带有新任期号的RequestVote RPC,这将导致当前leader恢复为follower状态。 最终将选出一位新的leader,但是被删除的服务器将再次超时,并且该过程将重复进行,从而导致可用性降低。

为了防止出现此问题,服务器在认为当前leader存在时会忽略RequestVoteRPC。 具体来说,如果服务器在听到的当前leader的最小选举超时内收到了RequestVote RPC,则不会更新其任期或给它投票。这不会影响正常选举,因为每台服务器至少等待一次最小选举超时才开始选举。但是,它有助于避免因移除服务器而造成的中断:如果leader能够对其集群发出心跳信号,那么它将不会因更长的任期号而被淘汰。

7.  日志压缩

在正常运行期间,Raft的日志会不断增长,但在实际系统中,无法无限制地增长。随着日志的增长,日志会占用更多空间,并且需要花费更多时间进行恢复。如果没有任何机制丢弃日志中累计的过时信息,最终将导致可用性问题。

快照是最简单的压缩方法。在快照进行过程中,整个当前系统状态都会写入稳定存储的快照(snapshot)中,在快照点之前的日志都会被丢弃。Chubby和ZooKeeper都使用快照,该章剩余部分都用来描述Raft的快照。

也可以采用增量方法进行压缩,例如日志清和日志数合并。这些操作一次仅处理少量数据,因此它们会随着时间的推移更均匀地分布压缩负载。他们首先选择一个累积了许多删除和覆盖对象的数据区域,然后再从该区域更紧凑地重写活动对象并释放该区域。与快照设置相比,这需要其他机制并引入了复杂性,这种方式就是通过对整个数据集进行操作来简化日志。尽管清除日志需要对Raft进行修改,但是状态机可以使用与快照相同的接口来实现LSM树。

图12:服务器用新快照替换其日志(索引1至5)中已提交的条目,该快照仅存储当前状态(在此示例中为变量x和y)。快照中包含的“最后包含的索引:last included index”和“最后包含的任期号:last inculded term”用于将快照放置在条目6之前的日志中。

图12显示了Raft中快照的基本思想。每个服务器独立进行快照,仅覆盖其日志中的已提交条目。大多数工作都通过状态机将其当前状态写入快照。Raft在快照中还包含少量的元数据:“最后包含的索引(last included index)”是快照替换的日志中最后一个条目的索引(状态机已应用的最后一个条目),“最后包含的任期(last included term)”是该条目的任期。保留这些条目是为了支持快照之后的第一个日志条目的AppendEntries一致性检查,因为该条目的检查需要先前的日志索引和任期。为了启用集群成员变更(第6节),快照还包括last included index指向的日志使用的配置。服务器完成快照写入后,它可能会删除last indcluded index之前的所有日志以及任何先前的快照。

尽管服务器通常通常独立执行快照,但leader有时仍必须将快照发送给落后的follower。当leader已经丢弃了需要发送给follower的下一个日志条目时(因为leader的快照中包括了该条日志),就会发生这种情况。幸运的是,这种情况在正常操作中不太可能出现:一个和leader保持同步的follower应该已经有这条日志了。然而,一个异常缓慢的follower或者加入集群的新服务器可能不存在该日志条目。让这个follower的日志和leader一样新的方法就是通过网络发送快照。

InstallSnapshot RPC:通过leader调用,发送快照的分块给follower。leader将分块按顺序发送。

  1. 参数

    • term:leader的任期

    • leaderId:follower可以重定向客户端

    • lastIncludedIndex:快照替换了该索引之前且包括该索引的所有日志

    • lastIncludedTerm:lastIncludedIndex的任期

    • offset:分块在快照文件中的字节偏移

    • data[]:快照分块从offset开始的字节流

    • done:如果这是最后一块分块,为true

  2. 结果

    • term:follower的当前任期,leader可以用来更新自己

  3. 接收者实现

    • 如果参数的term<接收者的currentTerm,立即回复

    • 如果是第一块分块(offset是0)就建立一个新的快照文件

    • 从offset开始写数据到快照文件中

    • 如果done不是false,回复并等待更多的数据分块

    • 保存快照文件,丢弃已存在的更小索引号的快照文件(也就是之前的快照文件)

    • 如果现有的日志条目索引和任期与快照包括的最后一条日志相同,就保留其后的日志条目并进行回复

    • 丢弃整个日志

    • 使用快照文件重置状态机(并加载快照中的集群配置)

图13:InstallSnapshot RPC的总结。快照被分成多个块进行传输;每个块都给follower一种leader的生命迹象,因此它可以重置其选举计时器。

leader使用一个新的InstallSnapshot RPC给落后很多的follower发送快照;具体看图13。当一个follower收到该RPC发送的快照,它必须决定对当前的日志做什么操作。通常,快照将包含接收者日志中尚未包含的新信息。在这种情况下,follower将丢弃其整个日志;它全部被快照取代,并且可能有与快照冲突的未提交条目。相反,如果接收到的快照是自己日志的前面部分由于重新传输或错误操作),则快照所覆盖的日志条目将被删除,但快照之后的条目仍然有效并且必须保留

这个快照方法背离了Raft的强硬leader原则,因为follower在不知道leader的情况下也可以执行快照。然而,这种背离是值得的。虽然leader可以避免在达成共识时发生决策冲突,但快照时已经达成共识,因此决策不会冲突。数据仍然是从leader流向follower,只是follower可以重组它们的数据。

我们考虑了另一种基于leader的方法,其中只有leader会创建快照,然后再将此快照发送给其每个follower。但是,这有两个缺点。首先,将快照发送给每个follower会浪费网络带宽并减慢快照过程。每个follower已经具有生成自己的快照所需的信息,对于服务器而言,从其本地状态生成快照通常比通过网络发送和接收快照要便宜得多。其次,leader的实现将更加复杂。例如,leader需要将快照同时发送给follower,同时向集群复制新的日志条目,以免阻止新客户端的请求。

还有另外两个影响快照性能的问题。首先,服务器必须决定何时进行快照。如果服务器过于频繁地进行快照,则会浪费磁盘带宽和能源。如果快照太不频繁,则有耗尽其存储容量的风险,并且会增加重新启动期间重放日志所需的时间。一种简单的策略是在日志达到固定大小(以字节为单位)时执行快照。如果将此大小设置为明显大于快照的预期大小,则用于快照的磁盘带宽开销将很小。

第二个性能问题是编写快照可能要花费大量时间,我们不希望这样做会延迟正常操作。解决方案是使用写时复制技术,以便可以接受新的更新而不会影响快照的写入。例如,使用功能性数据结构构建的状态机自然支持这一点。或者,可以使用操作系统的写时复制支持(例如Linux上的fork)来创建整个状态机的内存快照(我们的实现使用这种方法)。

8.  客户端交互

本节描述客户端如何与Raft交互,包括客户端如何找到集群leader以及Raft如何支持线性化语义[10]。这些问题适用于所有基于共识的系统,并且Raft的解决方案与其他系统类似。

我们对Raft的目标是实现线性化的语义(每个操作似乎在调用和响应之间的某个时刻立即执行一次,且恰好一次)。但是,到目前为止,Raft可以多次执行命令:例如,如果leader在提交日志条目之后、回复客户端之前宕机,客户端会和一个新的leader重试该指令,导致指令被重复执行。该解决方案是让客户端为每个命令分配唯一的序列号。然后,状态机将跟踪为每个客户端处理的最新序列号以及相关的响应。如果收到序列号已执行的命令,它将立即响应而无需重新执行该请求。

只读操作可以直接执行,而无需在日志中写入任何内容。但是,如果没有其他措施,这将存在返回陈旧数据的风险,因为响应该请求的leader可能已被它不知道的新leader暂停了。串行读取不会返回陈旧的数据,并且Raft在不使用日志的前提下,需要采取两个额外的预防措施来保证该特性。首先,leader必须掌握提交条目中的最新信息。Leader Completeness Property可确保leader拥有所有已提交的条目,但是在任期开始时,它可能不知道是哪些条目。为了找出这些条目,它需要在其任期中提交一个条目。Raft通过让每个leader在其任期开始时在日志中提交一个空白公开消息(no-op)来处理此问题。其次,leader必须在处理只读请求之前检查它是否是leader状态(如果选择了一个新的leader,其信息可能会过时)。Raft通过让leader在回复该只读请求之前先与集群大多数服务器交换心跳来处理此问题。或者,leader可以依靠心跳机制来提供一种形式的租约[9],但这将依赖于时序以确保安全性。

后续

后面的部分就是Raft的相关指标了,和Raft主要内容关系不大。

以上是关于Raft论文的主要内容,如果未能解决你的问题,请参考以下文章

Raft协议--中文论文介绍

菜鸟读Raft论文

Raft论文

Raft论文学习笔记

分布式一致性算法:Raft 算法(Raft 论文翻译)

Raft算法论文(部分)