raft算法浅析

Posted bloomingTony

tags:

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

raft作为工业级的分布式一致性算法,的确比paxos容易理解,看过论文后基本就可以将其实现。

raft算法包括三个基本组件:主节点选举、日志复制、安全性问题;


选主(leader-election):

要实现一致性算法,有两种方式:有主方式和无主方式;无主方式(non-leader)各个节点完全一样,都能接受客户端的写请求,并能够将写请求同步到其他节点;有主方式存在一个leader节点负责与客户端通信,其他节点只能与leader通信,leader节点接受客户端写请求,并将写请求同步到其他节点;raft采用的是有主方式,只有一个leader总控全局,不需要关心多个节点同时写时的冲突问题。

raft需要解决的是正确稳定地选举一个leader,而无须担心不同服务器之间的冲突;raft算法的复杂之处在于leader发生变化时,系统各节点会处于不一致状态,后续的leader需要对这些不一致状态进行清理。

任期和节点状态的概念就不讲了,大家可以谷歌下,下面来看下选主流程:


总述

Raft 必须保证在任何时候只能有一台服务器作为集群的leader。各个节点启动时的初始状态均为follower,flollower是完全被动的,不会主动发起任何请求,只是被动的相应candidate和leader的网络请求。但是每个follower都会维护一个随机的选举超时electionTimeout(150-300ms),如果超时时间内未收到leader节点的心跳包(表明leader节点身份,维护leader权威);follower节点会转变为candidate节点,发起选主流程;选出一个新的leader;所以当集群启动时,所有的服务器都是作为跟随者的,没有领导者,所以它们都会等待这段超时,然后它们都会开始进行选举。


选主流程:

当服务器开始进行选举的时候,它所做的第一件事情就是增加当前的任期号,创建一个比之前使用过的任何值都要大的新任期号(任期号是一个非常重要的概念,它直接决定选举的成败,因此节点任期号是需要在磁盘进行持久化存储的)。随后,服务器将状态从follower转变为candidate,为了让自己成为leader,它需要征求其他follower节点的意见,接收来自于大多数服务器的投票。candidate要做的第一件事情就是给自己投票,然后它会给其他所有服务器发送投票请求的远程调用(RequestVote RPC),通常这些请求是并行发出的。如果它没有获得响应,它就会持续发送重试的请求,直到获得响应为止。

最终会出现以下几种情况:

第一、在大多数情况下,candidate能够获得大多数的投票,并能够顺利的转变为leader,发送leader心跳包维护自己在任期内的权威。

第二、有可能存在多个canditate同时选举的情况(election timeout碰撞),导致一部分选票投给了candidateA一部分投给了candidateB,大家选票都未过半,未选举出主节点;增加任期号继续下一轮的选举,直到选出leader节点。

这里会存在以下几种异常情况,看下raft协议如何处理:


第一、如果两个candidate持有相同的任期号同时发起选举,集群会不会选出两个leader(脑裂冲突)?

第二、如果两个candidate持有不同的任期号同时发起选举,因为网络延迟等原因并且任期号小的candidate率先升级为leader节点,任期大的candidate也变为leader节点,raft如何将两个不同任期号的leader到收敛同一任期号?(raft有相关机制解决,后面会有相应的讲解)

第三,candidate节点发起选举时,集群中已有leader节点,candidate节点的任期号不大于当前leader的任期号,raft如何处理?(raft有相关机制解决,后面会有相应的讲解)

第四、因为网络抖动原因,follower可能会以为leader节点失效,转变为candidate并发起选举,并且candidate节点的任期号大于当前leader的任期号,candidate大概率会成为leader节点,对于这种干扰,raft如何处理?(目前raft貌似没有相关机制)


安全和可用性(safty and liveness):

安全是指一个任期内最多有一个candidate胜出成为leader节点,不存在多节点脑裂冲突;raft通过如下机制保证这事,每个节点在一个任期内只投一次票,并将投票信息持久化到磁盘,即使服务器崩溃恢复后也能恢复到之前的状态,否则会出现同一任期内给不同节点投票,产生多个leader,造成脑裂冲突。因为每台服务器只能进行一次投票,而且每个候选者都必须获得多数票,也就可以发现,不可能出现两个候选者同时获胜的情况。因此上述异常1是不可能出现的。

可用性是指保证有一定的获胜者,这样系统不会一直处于没有leader的状态。当出现相同任期号的candidate时,会存在分票的情形导致无法选出leader,在超时之后(假设发生超时碰撞),又进行新一轮的选举又再次出现分票,所以从理论上说这样的状态可以无限循环下去。raft通过采用随机超时机制降低follower同时发生election timeout成为candidate的几率,降低选举分票发生的概率,保证可用性。


日志复制(log-replication):

日志格式:

每台服务器不管是follower还是leader,都会存在一个日志副本。日志被分为多个记录(Entries),raft记录在日志中由日志下标索引标识其位置;记录内包含了两个主要信息:首先,是供状态机执行的指令,指令的具体格式根据具体的使用场景确定;其次是生成该条记录的任期号,任期号单调递增;Entries由下标索引和任期号唯一标识。鉴于日志的重要性,日志是需要被持久化到磁盘的,确保服务器崩溃后能够恢复当前状态。如果记录被大多数服务器复制,leader则认为该条记录处于已提交状态,并将该条记录发送给状态机执行,返回给客户端写入成功的响应。如下图:

记录7-3已被大多数服务器复制,因此处于提交状态;记录8-3只被两台服务器复制,因此处于未提交状态。


日志复制流程:

普通操作比较简单,客户端将命令发送给leader,leader首先将命令写入它自己的日志中,然后向所有其他的follower发送 AppendEntries 的远程调用。当leader收到大多数的节点返回的写入成功的响应时,leader认为该记录具备了提交的条件,并将将记录的命令发送给本地状态机执行,同时会将结果返回客户端,并发送AppendEntries请求告知其他的follower该记录已提交,follower收到提交请求时,会在本地的状态机中执行该记录的命令,保持与leader一致。这其实是典型的两阶段提交操作。


日志一致性保证:

为确保raft日志在服务器之间的高度一致性,raft提供了两种保证:第一、日志记录的下标索引以及任期号的组合可以唯一标识一条日志记录。也就是说如果有两条记录的索引是一样的,任期号也是一样的,那么就可以保证它们所存储的命令也是相同的。

第二、还能保证在这条记录之前的所有记录都能相互匹配。所以任期号和索引的组合可以唯一标识整个日志的起始至该点的位置。如果某条记录是已提交的,那么其所有前序的记录都应该处于已提交状态。


日志一致性检查:

当leader节点调用Append Entries RPC时,请求里除了包含新创建的日志记录还要包括当前的日志记录,follower接收到Append Entries请求时,检查当前的日志记录与leader的日志记录是否一致(任期号和下标索引是否一致),如果一致则接收新纪录请求,如果不一致则拒绝该请求。

raft算法浅析

如上图第一个RPC请求携带leader当前的日志记录(4-2)和新添加日志记录(5-3)发送给follower,follower检查(4-2)跟当前的日志记录相同,接受该添加日志记录的请求。

第二个RPC请求携带leader当前的日志记录(4-2)和新添加日志记录(5-3)发送给follower,follower检查(4-2)跟当前的日志记录(4-1)不一致,拒绝该添加日志记录的请求。

该检查过程可以看做是一个归纳的步骤,正是有了这种日志一致性检查,才能保证上述一致性保证里的第二条保证。


安全性问题(safty problem):

背景:

当领导者发生变更时,新领导者面对的状态不一定是干净的,因为前一领导者可能在它完成复制同步之前就已经崩溃了,当 Raft 处理这个问题时,它在新的领导者被选出之前,不会有任何特别的操作,不会存在一个独立清理过程,清理过程是在普通操作过程中发生的。原因是当新领导者被选出后,某些服务器可能还处于宕机的状态,不可能立刻对它们的日志进行清理,必须能有操作恢复它们,而且在这些机器重新加入集群之前可能会要等待很长一段时间,所以就必须对系统进行设计,要求普通操作最终能让所有的日志达成一致状态。为了达成这个目标,Raft 始终会认为领导者的日志总是正确的,所以对于所有领导者,它们必须时刻的让跟随者的日志与自己保持一致,但同时还是有可能出现在领导者未完成任务就崩溃的情况,所以就会出现一个又一个的新领导者。所以,在极端扭曲的状态下,日志记录会无限堆积并出现混乱的状态,就如下图所示的那样:

raft算法浅析

为了简单起见,上图中只显示了下标索引位置以及任期号,没有显示具体的命令信息。

当服务器 S4、S5 在任期 2、3、4 时是领导这,但是由于某些原因,它们无法完成对其他服务器(S1、S2、S3)上日志的复制同步,然后它们崩溃了,系统在一段时间内处于分隔状态,服务器 S1、S2、S3 在任期 5、6、7 内成为领导者,但同时也无法与服务器 S4、S5 进行通信,要求它们进行相应的清理操作。这就会出现上图中所示的状态,日志完全是混乱的。这里的关键在于 S1、S2、S3 的索引 1-3 以及 S4、S5 的索引 1-2 区域。这些都是已提交状态的记录,所以我们必须保留它们,但其他的日志记录都是未提交的,所以到底是保留还是丢弃它们并不重要。我们还没有将它们传入状态机,也没有客户端得到了这些命令的执行结果。所以它们都是可以丢弃的。


例如,假设服务器 S4 是任期 7 的领导者,而且它可以与其他所有服务器通信,那么它最终会让集群里其他服务器上的日志与它自己的保持一致,并删除那些与之冲突的记录。在介绍领导者是如何让其他服务器上日志与之保持一致前,首先需要介绍两个概念:正确性(Correctness)和安全性(Safety)。我们是如何知道系统的行为是正确的?如何知道它们没有丢失一些重要信息?因为这里可以看到,为了让集群回到一致的状态,有些日志记录会被丢弃。我们是如何安全地做到这点的?


安全性的要求:

为保证已被leader提交的日志记录不被后续的leader修改或者删除,raft实现了这样的一个安全属性,一旦领导者决定某个特定记录已提交,那么该条记录会出现在它所有未来领导者的日志记录中,并且也处于已提交状态。

到目前为止,对raft的描述是不足以实现这个安全属性的,下面看下raft是如何实现的:


(1)选举规则调整---挑选最好的leader:

最好的leader是有最新提交记录的节点,但如何确定该节点的记录是最新提交的记录呢?办法只有一个,将该节点变为candidate,通过发送VoteRequest,并携带该节点最新提交的日志记录下标索引和任期号,与其他节点比较即可。当一个候选者发起投票请求,它会包括自身的日志记录信息,位置索引 index 以及该记录的任期号 term 。当响应投票的服务器接收到请求,它会将候选者的日志信息与自己的日志信息进行比较,如果投票者的日志更完整,那么它会拒绝投票(lastTerm v > lastTerm c)|| (lastTerm v == lastTerm c) && (lastIndex v > lastTerm c)。结果是赢得选举的服务器可以保证比大多数投票者有更完整的日志记录。

挑选最好的节点的言外之意就是:前驱leader已提交的记录,后继leader必须也认定该记录已提交。


(2)日志不一致修复

通过选举规则(1)我们保证了安全性,提交的记录不会丢失。那么我们如何让follower的日志与leader保持一致呢?看下图日志不一致的场景:

follower可能丢失日志:a-10  b-5 e-8;也可能会有不同的日志:d-11  f-4  c-6

leader要做的就是丢弃所有不同的日志,将follower的日志与自己保持一致;


要想恢复到一致状态,领导者会为每个跟随者维护一个状态变量,这个变量称为 nextIndex ,这个变量存储日志的下一条记录的下标位置索引,服务器会把这个位置发送给跟随者(如上图所示,nextIndex = 11)。当一台服务器成为领导者后,它会将 nextIndex 值设置成当前日志记录的下一位置。所以在上面的例子中,任期 7 的领导者的最后一条记录的索引位置是 10 ,那么它会将 nextIndex 设置成 11 。领导者会根据 AppendEntries 调用发现一致性问题,因为当跟随者接收到 AppendEntries 调用时,都会进行检查。这个检查就可以发现所有的问题。所以当下一次领导者想要与跟随者进行通信时,它都会包括下标位置索引(10)以及任期号(6)作为请求的参数。当选为领导者后,下一次请求也有可能是以心跳检测的方式发送的,心跳检测与 AppendEntries 调用的方式一样,只是没有新值创建,但还是包括一致性检查的。所以当消息到达跟随者(a)后,它会将接收到的下标位置索引与任期与自己的日志信息进行比较,并没有匹配的记录,所以它会拒绝 AppendEntries 请求,当领导者收到拒绝的响应之后,它的响应很简单,它要做的只是将 nextIndex 减 1 ,所以这个值就变成了 10 。如此逐一减少,直到最终 nextIndex 为 5 的时候,领导者再次发送请求的信息会包括下标位置索引(4)以及任期号(4),这时它与跟随者(a)当前的日志记录信息是相匹配的,所以这时跟随者会接受 AppendEntries 请求,并追加记录 5-4 。直到领导者将跟随者的日志记录填充完整。相似的过程也会在跟随者(b)上出现。当 nextIndex 减少到 4 时,领导者会包括下标位置索引(3)以及任期号(1)作为请求的参数,并修正跟随者(b)上的日志记录。

这个过程还需要注意一点,当跟随者接收来自于领导者的替换请求时,它会将后续的日志记录截断并删除后续的所有日志记录,在上述的例子中,如果领导者发送请求(4-4),nextIndex = 4 ,这时跟随者的记录为 4-2 ,是不一致的,这时它不仅会将 4-2 覆盖,同时还会删除剩余的所有记录,因为在不一致的记录后也都是不一致的记录。

现在对领导者发生变更的情况作个小结。总体上需要解决两个问题:一个是需要保证系统的安全性,第二个是一旦新的领导者开始行使权利,它要做的事情就是使所有跟随者上的日志记录与自身保持一致,AppendEntries 的一致性检查会为我们提供所有的信息。


(3)任期号传递收敛多leader状态


旧leader有可能并不是真的死了。例如出现了网络的隔离,将leader与集群内其他服务器分隔,那么剩下的服务器会等待选举超时,并选举一个新leader,那么问题来了,如果旧leader又重新恢复连接怎么办?这个旧领导者并不知道已经重新进行了选举,也不知道新领导者的存在。所以这时它还会试图以领导者的身份继续运行,它还会与跟随者进行通信,并试图让其他跟随者与自己的日志记录保持一致,我们必须阻止这个事情的发生。

可以使用任期号来防止这种情况的出现。因为每个 RPC 请求都包括发送者的任期号,当 RPC 接收时,接受者会将其与自己的任期号相比较,如果不匹配,则会更新那些过期的记录。所以如果发送者的任期比接收者的要老,那么就表示发送者是过时的,这时接收者会立即拒绝 RPC 请求,并将包括了接收者任期信息的响应发送回发送者,这样当发送者接收到响应时就会意识到,它的任期号是过期的,此时它就会停下并作为follower继续运行(leader状态收敛),同时它还会更新自己的任期号,并与其他服务器保持一致。反之,如果接收者的任期号更老,如果这时接收者不是跟随者,那么它也会停下,并作为跟随者,而且更新它自己的任期号。略微不同的是接收者不会拒绝 RPC ,它会接收 RPC 请求。


这里比较有趣的是选举过程会导致任期号的更新,即当候选者请求投票并与大多数服务器发生通信后,它会将自己的任期号随着 RPC 请求发送出去,这样所有的接收者都会更新自己的任期号,并与候选者保持一致,所以当新领导者被选出后,集群里的多数服务器都会更新到这个任期号。这也就意味着,一旦选举完成,被罢免的领导者是无法提交新记录的,因为它需要与至少一台服务器进行通信,这样它就能发现自己的任期号更老,这时它就会停止领导者的行为并作为跟随者继续运行。

还有一些比较典型的场景,这里不作更多的讨论,但可以用任期号传递来处理所有类似的问题,比如轩主流程中的第二、第三种异常场景。


以上是关于raft算法浅析的主要内容,如果未能解决你的问题,请参考以下文章

Raft协议简析

一致性协议浅析:从逻辑时钟到Raft

Raft算法

分布式共识算法——Raft算法(图解)

分布式共识算法——Raft算法(图解)

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