二十张图带你一次性学懂Raft算法

Raft算法详解

    • 基础
    • 领导人选举
    • 日志
    • 脑裂问题
        • 单节点变更

基础

什么是Raft算法?

Raft是一种用于替代Paxos算法。相比于Paxos,Raft的目标是提供更清晰的逻辑分工使得算法本身能被更好地理解,同时它安全性更高,并能提供一些额外的特性。Raft能为在计算机集群之间部署有限状态机提供一种通用方法,并确保集群内的任意节点在某种状态转换上保持一致。Raft算法的开源实现众多,
在Go、C++、Java以及 Scala 中都有完整的代码实现。Raft这一名字来源于"Reliable, Replicated, Redundant, And Fault-Tolerant"(“可靠、可复制、可冗余、可容错”)的首字母缩写。[3]

集群内的节点都对选举出的领袖采取信任,因此Raft不是一种拜占庭容错算法,是一种故障容错算法

by 维基百科

用通俗的话来说,Raft算法是在兰伯特 Multi-Paxos思想的基础上,做了一些简化和限制,比如增加了日志必须是连续的,只支持领导者、跟随者、候选人三种状态,在理解和算法实现上都相对容易许多。

除此之外,Raft算法是现在分布式系统开发首选的共识算法,基本上全新的分布式系统选择了Raft算法。所以说掌握Raft算法是很有必要的,我们可以通过Raft算法,来探寻分布式系统的具体实现,也可以得心应手的处理绝大部分场景的容错和一致性需求,比如分布式配置系统,分布式NoSQL存储等等。

如果要用一句话概括Raft算法,我觉得是这样的:

从本质上说。Raft算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致。

节点有哪些状态 ?

一个 Raft 集群包括若干服务器,以典型的 5 服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个:

  • Leader:负责发起心跳,响应客户端,创建日志,同步日志。
  • Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。
  • Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。

在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。

下图展示了这三个节点状态之间的变更情况

二十张图带你一次性学懂Raft算法_第1张图片

在讲解完领导人选举之后,会针对该图做出详细的解释。

什么是日志?

我们可以发现不同角色之间数据交互就是通过一种叫日志的东西来完成的。

日志由日志项组成,日志项是一种数据格式,它主要包含用户指定的数据,也就是指令,还包含一些附加信息,比如索引值,任期编号。

二十张图带你一次性学懂Raft算法_第2张图片

什么是term?

我们发现上图有一个单词叫做term

raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。raft 算法保证在给定的一个任期最少要有一个 Leader。

每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号;如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。

总结:

  • 一个term会有一个唯一的Leader,如果没有选举出来,那么会立马进入下一个term。
  • 节点如果在接受消息的时候发现自己的term比其他人的小,那么该节点会更新到较大的term。
  • 如果一个Candidate或者Leader发现自己的term是较小的那一个,那么会立即退回成Follower(防止假死Leader复活)。
  • 如果一个Follower接收到的请求的term小于自己,那么它会拒绝此次请求(防止脑裂)

PS:(有关脑裂和假死的问题之后会提及,读者先有个印象就可以了)

raft算法通过判断节点的term,来判断一个节点在集群中的数据新旧状态,比如一个节点断线重连过,那么此时它的term应该就是小于其他节点的,此时它就不可能成为Candidate或者Leader,同理,如果一个节点的term是最新的,那么说明它一直都在正常服务,也就有成为Candidate或者Leader的资格。

领导人选举

假设此时集群中有三个节点,以三个节点为例,我用图例的形式先演示一个领导者选举过程

初始状态,所有节点都是跟随者的状态,每个跟随者等待接收来自Leader的心跳信息

二十张图带你一次性学懂Raft算法_第3张图片

当一直没有接收到心跳之后,某个节点等待的时间到达心跳超时时间之后,该节点就会将自己的任期编号加一,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票消息,请它们选举自己为领导者。

并且,Raft算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间是随机的,来防止同时出现多个节点发起选举,导致选举失败。

二十张图带你一次性学懂Raft算法_第4张图片

如果其他节点接收到候选人A的请求投票,在编号为1的任期内,也还没有进行过投票,那么它将选票投给节点A,并增加自己的任期编号。

二十张图带你一次性学懂Raft算法_第5张图片

如果候选人在选举超时时间内赢得了超过一半的选票,那么它就会成为本届任期内新的领导者。如果选举超时时间到了之后,该候选人就会重新在新的term中发起一次新选举。

二十张图带你一次性学懂Raft算法_第6张图片

节点A当选领导者后,将周期性的发送心跳消息,通知其他服务器我是领导者,防止跟随者发起新的选举。

总结:

Raft 使用心跳机制来触发 Leader 的选举。

Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。如果一个 Follower 在一个周期内没有收到心跳信息,就叫做心跳信息超时,然后它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。

为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生:

  • 赢得选举
  • 其他节点赢得选举
  • 一轮选举结束,无人胜出

赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票(N/2+1)(可以防止脑裂),就可以成为 Leader。

在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况:

  • 该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。
  • 该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。

由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。

Raft 使用了随机超时时间来避免上述情况。

包括心跳信息超时时间和选举超时时间两部分。

  • 心跳信息超时时间:Follower等待Leader或者Candidate心跳信息的超时时间,超时时间到了之后,
  • 选举超时时间:Candidate发起选举后的超时时间,如果在时间内未选举出Leader,就会进入下一次选举。

通过随机超时时间这种机制,使得各个服务器发起选举的时机能够分散开来,在大多数情况下只有一个服务器会率先超时;它会在其他服务器超时之前赢得选举。

日志

如何复制日志?

二十张图带你一次性学懂Raft算法_第7张图片

  • Leader 收到客户端请求后,会生成一个 entry,包含,再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。
  • 如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。
  • 如果 Leader 收到了多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以成为这个 entry 是 committed 的,并且向客户端返回执行结果。

但是由于日志复制的时候,为了尽可能优化Raft算法的性能,Raft的作者并没有使用一些类似二阶段提交的分布式事务机制,并不一定会成功,当多数节点返回成功时,Leader就会认为日志提交成功,此时我们再看一下之前那张有关日志的图片

二十张图带你一次性学懂Raft算法_第8张图片

我们可以发现各个节点的日志记录并不相同,这就是由于部分节点日志复制失败导致的。

那么Raft如何保证这种异常状况导致的日志不一致呢?

如何实现日志的一致?

在Raft算法中,Leader通过强制Follower直接复制自己的日志项,来处理不一致的日志。也就是说,Raft算法是通过以Leader的日志为准,来实现各节点的日志一致的。大体上来说有两个步骤。

  1. Leader通过日志复制消息的一致性检查,找到Follower节点上,与Leader相同日志的最大索引值(在这个索引值之前的日志和Leader都相同,这个索引值之后的都不相同)
  2. Leader强制Follower使用Leader的日志覆盖掉上一个步骤找到的最大索引值之后的日志

这样,Follower的日志就和Leader保持完全一致了。

那么Leader如何通过一致性检查找到这个最大索引值呢?

  1. 当Follower接收到日志复制的消息的时候,判断对应的日志记录是否与Leader中一致,如果一致,则成功,否则返回失败
  2. Leader递减要复制的日志项的索引,并发送新的日志项到Follower
  3. 直到日志项记录一致,Follower返回成功,此时Leader就找到了这个日志相同的最大索引值

总结:

  • 在Raft中,副本数据是以日志的形式存在的,其中日志项中的指令表示用户指定的数据
  • 在Raft中,要求日志必须是连续的,日志不仅是数据的载体,并且日志的完整性还影响Leader选举的结果。也就是说,日志完整性最高的节点才能当选领导者
  • Raft是通过以Leader的日志为准,来实现日志的一致的

脑裂问题

脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”,我们都知道,如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。

脑裂通常会出现在集群环境中,比如ElasticSearch、Zookeeper集群,而这些集群环境有一个统一的特点,就是它们有一个大脑,比如ElasticSearch集群中有Master节点,Zookeeper集群中有Leader节点。

而Raft算法也有这个问题,我们一定要确保集群只有一个Leader节点,当网络分区出现问题时,将整个集群分为两个部分,这时就有可能出现脑裂。

那么Raft算法如何避免分区问题下的脑裂呢?

还记得我们之前说到的选举规则吗,只有当一个节点获得的票数>= N/2 + 1的时候,才会成为Leader节点,

当出现分区问题的时候,不同分区中的节点数会出现相等,或者是一边大一边小的情况。

我们讨论一下所有的情况。

假设Leader被分在节点数多的那一边,我们先看下如果是偶数节点。

二十张图带你一次性学懂Raft算法_第9张图片

此时两边节点数一样,此时右边最多选票为三票,不满足 >= N / 2 + 1这个条件,于是整个集群还是只有一个Leader,等待分区问题结束,Leader给Follower同步数据即可。

二十张图带你一次性学懂Raft算法_第10张图片

Leader在节点数多的一方,且节点数为奇数

那么,如果在分区问题中,Leader被分在了节点数量更少的那一方会怎么样呢,此时节点数量超过 N / 2 + 1的分区,就可以选出Leader,我们称旧Leader为假死Leader

假设某个leader假死,其余的followers选举出了一个新的leader。这时,旧的leader复活并且仍然认为自己是leader,这个时候它向其他follower发出写请求也是会被拒绝的。还记得我们之前提到的term吗,当时我们提到了一条规则,如果Follower接收到的请求的term小于本身,就会拒绝这次请求,又因为新的leader产生是在更大的term产生的,所以当假死leader复活的时候,并不会有人理它,它发现自己的term小于Follower的时候,自己就会变成Follower。于是这种情况也不会对系统产生影响。

虽然Raft避免了这种正常情况下的脑裂问题,但是有一种情况还是可能会发生脑裂,当你就需要替换集群中的服务器。如果遇到需要改变数据副本数的情况,则需要增加或移除集群中的服务器。那么当成员变更时,集群成员发生了变化,就可能同时存在新旧配置的 2 个“大多数”,出现 2 个领导者,破坏了 Raft 集群的领导者唯一性,影响了集群的运行。

而解决这个问题,最常用的方法就是单节点变更

单节点变更

单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。比如将 3 节点集群扩容为 5 节点集群,这时你需要执行 2 次单节点变更,先将 3 节点集群变更为 4 节点集群,然后再将 4 节点集群变更为 5 节点集群,就像下图的样子。

二十张图带你一次性学懂Raft算法_第11张图片

而通过这种方法,在正常情况下,不管旧的集群配置是怎么组成的,旧配置的“大多数”和新配置的“大多数”都会有一个节点是重叠的。由于一个节点只能投一次票,如果要在新旧配置产生两个Leader,那么这个重叠的节点必须投两次票,但是这显然不可能,它只能投一次票,它将票投给谁,谁就会成为Leader,也就是说,不会同时存在旧配置和新配置 2 个“大多数”:

二十张图带你一次性学懂Raft算法_第12张图片

二十张图带你一次性学懂Raft算法_第13张图片

需要注意的是,在分区错误节点故障等情况下,如果我们并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现 2 个领导者的情况。

总结:

  • 成员变更的问题,主要在于进行成员变更时,可能存在新旧配置的 2 个“大多数”,导致集群中同时出现两个领导者,破坏了 Raft 的领导者的唯一性原则,影响了集群的稳定运行。
  • 单节点变更是利用“一次变更一个节点,由于存在重叠节点,不会同时存在旧配置和新配置 2 个‘大多数’”的特性,实现成员变更。

Raft是一种强领导者模型共识算法,而强领导者模型会限制集群的写性能,因为写操作只能由Leader来进行,那你想想看,有什么办法能突破 Raft 集群的写性能瓶颈呢?

提示:Kafka、ES

参考文章:

  • 极客时间——分布式协议理论算法详解
  • JavaGuide——Raft算法

你可能感兴趣的:(分布式理论与算法,java,分布式,算法)