一、 Raft算法

123 阅读15分钟

Raft算法

Raft算法将一致性算法分解成了几个关键模块:领导人选举、日志复制、还有安全等。

一致性算法是什么?

一致性算法使得一组机器像一个整体一样工作,基体中所有的节点都保证了数据的一致性,部分节点出现故障也不用担心整个服务崩溃。因此一致性算法在分布式系统中十分常见。

Raft算法的特性:

  • 强领导人:Raft算法中,日志条目只能由领导人发给其他的服务器。大大简化了对复制日志的管理。
  • 领导选举:Raft算法中,使用一个随机计时器来选举领导人,在没有领导人的时候,每个候选者节点在随机的时间后发起投票选举。
  • 成员关系调整:没看明白🤣

引用自:github.com/maemual/raf…

一致性算法的任务是保证复制日志的一致性。服务器上的一致性模块接收客户端发送的指令然后添加到自己的日志中。它和其他服务器上的一致性模块进行通信来保证每一个服务器上的日志最终都以相同的顺序包含相同的请求,即使有些服务器发生故障。一旦指令被正确的复制,每一个服务器的状态机按照日志顺序处理他们,然后输出结果被返回给客户端。因此,服务器集群看起来形成了一个高可靠的状态机。

实际系统中使用的一致性算法通常含有以下特性:

  • 安全性保证(绝对不会返回一个错误的结果):在非拜占庭错误情况下,包括网络延迟、分区、丢包、重复和乱序等错误都可以保证正确。
  • 可用性:集群中只要有大多数的机器可运行并且能够相互通信、和客户端通信,就可以保证可用。因此,一个典型的包含 5 个节点的集群可以容忍两个节点的失败。服务器被停止就认为是失败。它们稍后可能会从可靠存储的状态中恢复并重新加入集群。
  • 不依赖时序来保证一致性:物理时钟错误或者极端的消息延迟只有在最坏情况下才会导致可用性问题。
  • 通常情况下,一条指令可以尽可能快的在集群中大多数节点响应一轮远程过程调用时完成。小部分比较慢的节点不会影响系统整体的性能。

一个 Raft 集群包含若干个服务器节点;5 个服务器节点是一个典型的例子,这允许整个系统容忍 2 个节点失效。在任何时刻,每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人

在通常情况下,系统中只有一个领导人并且其他的节点全部都是跟随者。跟随者都是被动的:他们不会发送任何请求,只是简单的响应来自领导人或者候选人的请求。领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。

第三种状态,候选人,是用来选举新领导人时使用。下图展示了这些状态和他们之间的转换关系:

跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导人。在一个任期内,领导人一直都会是领导人,直到自己宕机了。

Raft算法会把时间分割成任意长度的任期(term),每一段任期都从一次选举开始,每一次选举都会有一个或多个候选人尝试成为领导人,当然,同样存在某一次选举过程所有的候选人获得了相同的票数,此时这一任期就会以没有选举人结束,下一次任期会很快开始。Raft算法保证在一个任期内,最多只会有一个领导人。

任期在 Raft 算法中充当逻辑时钟的作用,任期使得服务器可以检测一些过期的信息:比如过期的领导人。每个节点存储一个当前任期号,这一编号在整个时期内单调递增。

每当服务器之间通信的时候都会交换当前任期号;如果一个服务器的当前任期号比其他人小,那么他会更新自己的任期号到较大的任期号值

如果一个候选人或者领导人发现自己的任期号过期了,那么他会立即恢复成跟随者状态。如果一个节点接收到一个包含过期的任期号的请求,那么他会直接拒绝这个请求

Raft 算法中服务器节点之间通信使用远程过程调用(RPCs)。

领导人的选举

Raft算法采用一个心跳机制来触发领导人的选举。领导人会周期性的向所有跟随者发送心跳包来维持自己的权威, 一旦一个跟随这在一段时间内没有接受到任何消息(选举超时),那么他就会认为系统中没有个用的领导人,然后发起选举以选出新的领导人。

每一次选举过程,追随者会将任期号(term)加一,并切换到候选人状态。他会并行的向其他节点发送请求投票的 RPCs 来给自己投票。一共会出现三种结果:1. 自己成为了领导人;2. 其他节点成为了领导人;3. 这个任期没有人成为领导人。

当一个候选人从整个集群的大多数服务器节点获得了针对同一个任期号的选票,那么他就赢得了这次选举并成为领导人。每一个服务器最多会对一个任期号投出一张选票,按照先来先服务的原则。一旦候选人赢得选举,他就立即成为领导人。然后他会向其他的服务器发送心跳消息来建立自己的权威并且阻止发起新的选举。

在等待投票的时候,候选人可能会从其他的服务器接收到声明它是领导人的消息(RPC)。如果这个领导人的任期号(包含在此次的 RPC中)不小于候选人当前的任期号,那么候选人会承认领导人合法并回到跟随者状态。 如果此次 RPC 中的任期号比自己小,那么候选人就会拒绝这次的 RPC 并且继续保持候选人状态。

第三种可能的结果是候选人既没有赢得选举也没有输:如果有多个跟随者同时成为候选人,那么选票可能会被瓜分以至于没有候选人可以赢得大多数人的支持(超过半数为大多数) 。当这种情况发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。然而,没有其他机制的话,选票可能会被无限的重复瓜分。

关于为何使用随机选举超时时间

Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300 毫秒)随机选择。这样可以把服务器都分散开以至于在大多数情况下只有一个服务器会选举超时;然后他赢得选举并在其他服务器超时之前发送心跳包。同样的机制被用在选票瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,然后在超时时间内等待投票的结果;这样减少了在新的选举中另外的选票瓜分的可能性。

领导人选举这个例子,体现了可理解性原则是如何指导我们进行方案设计的。起初我们计划使用一种排名系统:每一个候选人都被赋予一个唯一的排名,供候选人之间竞争时进行选择。如果一个候选人发现另一个候选人拥有更高的排名,那么他就会回到跟随者状态,这样高排名的候选人能够更加容易的赢得下一次选举。但是我们发现这种方法在可用性方面会有一点问题(如果高排名的服务器宕机了,那么低排名的服务器可能会超时并再次进入候选人状态。而且如果这个行为发生得足够快,则可能会导致整个选举过程都被重置掉)。我们针对算法进行了多次调整,但是每次调整之后都会有新的问题。最终我们认为随机重试的方法是更加明显和易于理解的。

日志复制

领导人针对每一个追随者都维护了一个 nextIndex,表示下一个需要发给追随者的日志条目的索引地址。当某一个节点成为追随者的时候,他会初始化所有的 nextIndex 为自己的最后一个条目的索引加一。在下一次附加日志 RPCs 的时候,一致性检查失败,领导人就会减小 netxIndex 并进行重试,直到某一次领导人和追随者的日志达成一致。此时一致性检查才会成功,此时会将冲突的日志全部删除并加上领导人的日志。所有的操作都是在进行附加日志的一致性检查时完成的。

简单来说:领导人一旦发现追随者的日志与自己的不同(一致性检查失败),那么领导人会找到两者最后达到一致的地方(如果全部都不一致,那么会找到头),删掉追随者那个点之后的所有日志,并追加上自己的日志。

一旦一致性检查成功,就表示追随者的日志与领导人的日志保持了一致。

思考:每次 nextIndex 都只减小一次,有没有优化办法,能够 使得nextIndex每次多减一点呢?

也就是减少一致性检查失败的情况,比如当一致性检查失败的时候,追随者可以返回冲突日志条目的任期号和该任期号对应的最小索引地址,这样,nextIndelx 就可以一次性跳过该冲突任期的所有日志条目。但是这种优化可能是没有必要的,因为失败很少发生,也大可能会有这么多不一致的情况发生。

Raft 能够接受,复制并应用新的日志条目只要大部分的机器是工作的;在通常的情况下,新的日志条目可以在一次 RPC 中被复制给集群中的大多数机器;并且单个的缓慢的跟随者不会影响整体的性能。(有点没懂,记录一下)。

安全性

可能会存在一种情况:如果一个跟随者 A 已经进入了不可用状态,同时领导人已经提交了若干的日志条目,当这个跟随者 A 恢复并被选举为了领导人,那么他就会覆盖这些已经提交的日志条目。所以需要增加一些限制。

选举限制

Raft使用一种方法保证只有拥有之前任期中提交的所有日志条目的追随者才可能成为领导人。简单来说,某一个追随者想要成为领导人,那么他的日志条目中必须拥有之前任期中提交的所有日志条目。

那么这个是怎么判断的呢?

候选人想要成为领导人,那么候选人的日志至少得和大多数服务器节点一样新。在请求投票 RPC 中,包含了候选人的日志信息,投票的人会拒绝日志没有自己新的候选人的投票请求

新的概念:Raft会比较两份日志中的最后一条日志的索引值和任期号。任期号大或者日志比较长的日志是比较新的。

增加了这个选举限制后,在上面的那种情况下,跟随者 A 的日志条目不可能比大多数节点新,因此跟随者 A 不可能成为领导人。

提交之前任期内的日志条目(暂时不知道什么意思,先复制过来)

如同 5.3 节介绍的那样,领导人知道一条当前任期内的日志记录是可以被提交的,只要它被存储到了大多数的服务器上。如果一个领导人在提交日志条目之前崩溃了,未来后续的领导人会继续尝试复制这条日志记录。然而,一个领导人不能断定一个之前任期里的日志条目被保存到大多数服务器上的时候就一定已经提交了。图 8 展示了一种情况,一条已经被存储到大多数节点上的老日志条目,也依然有可能会被未来的领导人覆盖掉。

图 8:如图的时间序列展示了为什么领导人无法决定对老任期号的日志条目进行提交。在 (a) 中,S1 是领导人,部分的(跟随者)复制了索引位置 2 的日志条目。在 (b) 中,S1 崩溃了,然后 S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处。然后到 (c),S5 又崩溃了;S1 重新启动,选举成功,开始复制日志。在这时,来自任期 2 的那条日志已经被复制到了集群中的大多数机器上,但是还没有被提交。如果 S1 在 (d) 中又崩溃了,S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。反之,如果在崩溃之前,S1 把自己主导的新任期里产生的日志条目复制到了大多数机器上,就如 (e) 中那样,那么在后面任期里面这些新的日志条目就会被提交(因为 S5 就不可能选举成功)。 这样在同一时刻就同时保证了,之前的所有老的日志条目就会被提交。

为了消除图 8 里描述的情况,Raft 永远不会通过计算副本数目的方式去提交一个之前任期内的日志条目。只有领导人当前任期里的日志条目通过计算副本数目可以被提交;一旦当前任期的日志条目以这种方式被提交,那么由于日志匹配特性,之前的日志条目也都会被间接的提交。在某些情况下,领导人可以安全的知道一个老的日志条目是否已经被提交(例如,该条目是否存储到所有服务器上),但是 Raft 为了简化问题使用一种更加保守的方法。

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

跟随者与候选人崩溃的情况

跟随者与候选人崩溃的情况比领导人简单一些,在 Raft 中,

参考

哔哩哔哩:动画:Raft算法Leader选举、脑裂后选举、日志复制、修复不一致日志和数据安全

动画网站:Raft 分布式共识算法动画演示 (kailing.pub)

官方介绍中文版:raft-zh_cn