解读共识算法Raft

共识算法的特征

共识算法主要有三个特征:

  1. 共识算法可以保证在任何非拜占庭情况下的正确性。(可以把拜占庭情况理解为节点发送错误命令,存储不可靠)。通常来说,共识算法可以解决网络延迟、网络分区、丢包、重复发送、乱序问题,无法解决拜占庭问题(如存储不可靠、消息错误)。
  2. 共识算法可以保证在大多数机器正常的情况下集群的高可用性,而少部分的机器缓慢不影响整个集群的性能。大多数这个概念很好理解,比如有三个节点,我们可以容忍一个节点宕机。如果集群有五个节点,我们就可以容忍两个节点宕机。那么如果集群有四个节点,我们只能容忍一个节点宕机,因为四个节点中的三个才能构成大多数。
  3. 不依赖外部时间来保证日志的一致性。这一点既是共识算法的优势,因为共识算法不受硬件影响,不会因外部因素造成错误。但也造成了一些限制,让共识算法受网络影响很大,在异地容灾场景下,共识算法的支持性比较差。

1、领导者选举

1.1 成员身份

在raft算法中,每个服务器节点的状态都处于领导者(Leader)、跟随者(Follower)和候选人(Candidate) 3 种状态中的一个,注意这里是"之一"。

  • 跟随者:就相当于普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
  • 候选人:候选人将向其他节点发送请求投票(RequestVote)RPC 消息,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
  • 领导者:蛮不讲理的霸道总裁,一切以我为准,平常的主要工作内容就是 3 部分,处理写请求、管理日志复制和不断地发送心跳信息,通知其他节点“我是领导者,我还活着,你们现在不要发起新的选举,找个新领导者来替代我。”

需要你注意的是,Raft 算法是强领导者模型,集群中只能有一个leader

在这里插入图片描述

我们来看上面这张图,任何一个节点启动时都是follower状态,如果它察觉到集群中没有leader的话,它会把自己从follower状态切换到candidate状态,在candidate中经历一次或多次选举,最终会根据选举的结果决定自己切换到leader状态,或者切换会follower状态。如果选举成功,那么它会切换到leader状态,并且在leader状态为客户端提供服务。如果他在leader状态的任期已经结束,或者是他自身发生了宕机或者其他的问题,那么它会切换回follower状态,并且进行下一个循环。

1.2 节点如何通信?

在 Raft 算法中,服务器节点间的沟通联络采用的是远程过程调用(RPC),在Raft中主要有两类RPC:

  1. 请求投票(RequestVote)RPC,是由候选人在选举期间发起,通知各节点进行投票
  2. 日志复制(AppendEntries)RPC,是由领导者发起,用来复制日志和提供心跳消息

日志复制 RPC 只能由领导者发起,这是实现强领导者模型的关键之一

1.3 什么是任期?

Raft 算法中的领导者也是有任期的,每个任期由单调递增的数字(任期编号)标识,比如节点 A 的任期编号是 1。任期编号是随着选举的举行而变化的,这是在说下面几点:

  1. 跟随者在等待领导者心跳信息超时后,推举自己为候选人时,会增加自己的任期号,比如节点 A 的当前任期编号为 0,那么在推举自己为候选人时,会将自己的任期编号增加为 1。
  2. 如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。比如节点 B 的任期编号是 0,当收到来自节点 A 的请求投票 RPC 消息时,因为消息中包含了节点 A 的任期编号,且编号为 1,那么节点 B 将把自己的任期编号更新为 1。

每一段任期从一次选举开始。在某些情况下,一次选举无法选出leader (比如两个节点收到了相同的票数或者没有一个节点投票数超过一半),在这种情况下,这一任期会以没有leader结束(比如下图中的t3时刻就是没有leader的短暂任期);一个新的任期(包含一次新的选举)会很快重新开始。Raft保证在任意一个任期内,最多只有一个leader 。

在这里插入图片描述

Raft 算法中的任期不只是时间段,而且任期编号的大小,会影响领导者选举和请求的处理。

  1. 在 Raft 算法中约定,如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。比如分区错误恢复后,任期编号为 3 的领导者节点 B,收到来自新领导者的,包含任期编号为 4 的心跳消息,那么节点 B 将立即恢复成跟随者状态。
  2. 还约定如果一个节点接收到一个包含较小的任期编号值的请求,那么它会直接拒绝这个请求。比如节点 C 的任期编号为 4,收到包含任期编号为 3 的请求投票 RPC 消息,那么它将拒绝这个消息。

任期的机制就可以非常明确地标识集群的状态。并且通过任期的比较,可以帮助我们确认一台服务器历史的状态。比如说我们可以通过查看一台服务器是否具有t2任期内的日志,来判断它在t2这个时间段内是否出现过宕机。

1.4 选举领导者的过程以及选举的规则

首先,在初始状态下,集群中所有的节点都是跟随者的状态

在这里插入图片描述

Raft 算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。通过上面的图片你可以看到,集群中没有领导者,而节点 A 的等待超时时间最小(150ms),它会最先因为没有等到领导者的心跳信息,发生超时。

这个时候,节点 A 就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票 RPC 消息,请它们选举自己为领导者。

在这里插入图片描述

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

在这里插入图片描述

如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。

在这里插入图片描述

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

在这里插入图片描述

这就是整个选举的过程,选举的结果也有三种,上面只是其中的一种。

1、它获得超过半数选票赢得了选举,成为主并开始发送心跳
2、其他节点赢得了选举,收到新leader的心跳后,如果新leader的任期号不小于自己当前的任期号,那么就从candidate回到follower状态
3、一段时间之后没有任何获胜者,每个candidate都在一个自己的随机选举超时时间后增加任期号开始新一轮投票

在Raft中,选举的规则主要有以下几点:

  1. 领导者周期性地向所有跟随者发送心跳消息(即不包含日志项的日志复制 RPC 消息),通知大家我是领导者,阻止跟随者发起新的选举。
  2. 如果在指定时间内,跟随者没有接收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举。
  3. 在一次选举中,赢得大多数选票的候选人,将晋升为领导者。
  4. 在一个任期内,领导者一直都会是领导者,直到它自身出现问题(比如宕机),或者因为网络延迟,其他节点发起一轮新的选举。
  5. 在一次选举中,每一个服务器节点最多会对一个任期编号投出一张选票,并且按照“先来先服务”的原则进行投票。比如节点 C 的任期编号为 3,先收到了 1 个包含任期编号为 4 的投票请求(来自节点 A),然后又收到了 1 个包含任期编号为 4 的投票请求(来自节点 B)。那么节点 C 将会把唯一一张选票投给节点 A,当再收到节点 B 的投票请求 RPC 消息时,对于编号为 4 的任期,已没有选票可投了。在这里插入图片描述
  6. 日志完整性高的跟随者(也就是最后一条日志项对应的任期编号值更大,索引号更大),拒绝投票给日志完整性低的候选人。比如节点 B 的任期编号为 3,节点 C 的任期编号是 4,节点 B 的最后一条日志项对应的任期编号为 3,而节点 C 为 2,那么当节点 C 请求节点 B 投票给自己时,节点 B 将拒绝投票。在这里插入图片描述

我想强调的是,选举是跟随者发起的,推举自己为候选人;大多数选票是指集群成员半数以上的选票;大多数选票规则的目标,是为了保证在一个给定的任期内最多只有一个领导者。

其实在选举中,除了选举规则外,我们还需要避免一些会导致选举失败的情况,比如同一任期内,多个候选人同时发起选举,导致选票被瓜分,选举失败。那么在 Raft 算法中,如何避免这个问题呢?答案就是前面提到过的随机超时时间

1.5 如何理解随机超时时间?

Raft 算法巧妙地使用随机选举超时时间的方法,把超时时间都分散开来,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况。

除此之外,当前选举阶段没有产生任何leader,这个结论不需要集群中的所有节点对此产生共识,而是通过每个candidate都在等待一个随机选举超时时间后,默认去进入下一个选举阶段。Raft中给出的随机超时时间是150ms~300ms。这意味着如果candidate没有收到超过半数的选票,也没有收到新leader的心跳,那么他就会在150到300毫秒之间随机选择一个时间再次发起选举,也就是增加自己的任期号,再投票。

在 Raft 算法中,随机超时时间是有 2 种含义的

  1. 跟随者等待领导者心跳信息超时的时间间隔,是随机的
  2. 如果候选人在一个随机时间间隔内,没有赢得过半票数,那么选举无效了,然后候选人发起新一轮的选举,也就是说,等待选举超时的时间间隔,是随机的

2、日志复制

在 Raft 算法中,副本数据是以日志的形式存在的,领导者接收到来自客户端写请求后,处理写请求的过程就是一个复制和应用(Apply)日志项到状态机的过程。

那 Raft 是如何复制日志的呢?又如何实现日志的一致的呢?这些内容是 Raft 中非常核心的内容。

2.1 如何理解日志?

副本数据是以日志的形式存在的,日志是由日志项组成,日志项究竟是什么样子呢?

其实,日志项是一种数据格式,它主要包含用户指定的数据,也就是指令(Command),还包含一些附加信息,比如索引值(Log index)、任期编号(Term)。那该怎么理解这些信息呢?

在这里插入图片描述

  • 指令:一条由客户端请求指定的、状态机需要执行的指令。你可以将指令理解成客户端指定的数据。
  • 索引值:日志项对应的整数索引值。它其实就是用来标识日志项的,是一个连续的、单调递增的整数号码。
  • 任期编号:创建这条日志项的领导者的任期编号。

从图中可以看到,一届领导者任期,往往有多条日志项。而且日志项的索引值是连续的,这一点需要注意。

2.2 如何复制日志?

可以把 Raft 的日志复制理解成一个优化后的二阶段提交(将二阶段优化成了一阶段),减少了一半的往返消息,也就是降低了一半的消息延迟。那日志复制的具体过程是什么呢?

首先,领导者进入第一阶段,通过日志复制(AppendEntries)RPC 消息,将日志项复制到集群其他节点上。

接着,如果领导者接收到大多数的“复制成功”响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的“复制成功”响应,那么就返回错误给客户端。

看到这里,可能会有这样的疑问了,领导者将日志项应用到它的状态机,怎么没通知跟随者应用日志项呢?

这是 Raft 中的一个优化,领导者不直接发送消息通知其他节点应用指定日志项。因为领导者的日志复制 RPC 消息或心跳消息,包含了当前最大的,将会被提交(Commit)的日志项索引值。所以通过日志复制 RPC 消息或心跳消息,跟随者就可以知道领导者的日志提交位置信息。

因此,当其他节点接受领导者的心跳消息,或者新的日志复制 RPC 消息后,就会将这条日志项应用到它的状态机。而这个优化,降低了处理客户端请求的延迟,将二阶段提交优化为了一段提交,降低了一半的消息延迟。

在这里插入图片描述

1、接收到客户端请求后,领导者基于客户端请求中的指令,创建一个新日志项,并附加到本地日志中。
2、领导者通过日志复制 RPC,将新的日志项复制到其他的服务器。
3、当领导者将日志项,成功复制到大多数的服务器上的时候,领导者会将这条日志项应用到它的状态机中。
4、领导者将执行的结果返回给客户端。
5、当跟随者接收到心跳信息,或者新的日志复制 RPC 消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没应用,那么跟随者就将这条日志项应用到本地的状态机中。

不过,这是一个理想状态下的日志复制过程。在实际环境中,复制日志的时候,你可能会遇到进程崩溃、服务器宕机等问题,这些问题会导致日志不一致。那么在这种情况下,Raft 算法是如何处理不一致日志,实现日志的一致的呢?

2.3 如何实现日志的一致?

在 Raft 算法中,领导者通过强制跟随者直接复制自己的日志项,处理不一致日志。也就是说,Raft 是通过以领导者的日志为准,来实现各节点日志的一致的。具体有 2 个步骤。

  • 首先,领导者通过日志复制 RPC 的一致性检查,找到跟随者节点上,与自己相同日志项的最大索引值。也就是说,这个索引值之前的日志,领导者和跟随者是一致的,之后的日志是不一致的了。
  • 然后,领导者强制跟随者更新覆盖的不一致日志项,实现日志的一致。

我带你详细地走一遍这个过程(为了方便演示,我们引入 2 个新变量)

  • PrevLogEntry:表示当前要复制的日志项,前面一条日志项的索引值。比如在图中,如果领导者将索引值为 8 的日志项发送给跟随者,那么此时 PrevLogEntry 值为 7。
  • PrevLogTerm:表示当前要复制的日志项,前面一条日志项的任期编号,比如在图中,如果领导者将索引值为 8 的日志项发送给跟随者,那么此时 PrevLogTerm 值为 4。

在这里插入图片描述

在这里插入图片描述

  1. 领导者通过日志复制 RPC 消息,发送当前最新日志项到跟随者(为了演示方便,假设当前需要复制的日志项是最新的),这个消息的 PrevLogEntry 值为 7,PrevLogTerm 值为 4。
  2. 如果跟随者在它的日志中,找不到与 PrevLogEntry 值为 7、PrevLogTerm 值为 4 的日志项,也就是说它的日志和领导者的不一致了,那么跟随者就会拒绝接收新的日志项,并返回失败信息给领导者。
  3. 这时,领导者会递减要复制的日志项的索引值,并发送新的日志项到跟随者,这个消息的 PrevLogEntry 值为 6,PrevLogTerm 值为 3。
  4. 如果跟随者在它的日志中,找到了 PrevLogEntry 值为 6、PrevLogTerm 值为 3 的日志项,那么日志复制 RPC 返回成功,这样一来,领导者就知道在 PrevLogEntry 值为 6、PrevLogTerm 值为 3 的位置,跟随者的日志项与自己相同。
  5. 领导者通过日志复制 RPC,复制并更新覆盖该索引值之后的日志项(也就是不一致的日志项),最终实现了集群各节点日志的一致。

从上面步骤中你可以看到,领导者通过日志复制 RPC 一致性检查,找到跟随者节点上与自己相同日志项的最大索引值,然后复制并更新覆盖该索引值之后的日志项,实现了各节点日志的一致。需要你注意的是,跟随者中的不一致日志项会被领导者的日志覆盖,而且领导者从来不会覆盖或者删除自己的日志。

只要过半的服务器能正常运行,Raft就能够接受、复制并应用新的且志条且

3、安全性

领导者选举和日志复制两个子问题实际上已经涵盖了共识算法的全程,但这两点还不能完全保证每一个状态机会按照相同的顺序执行相同的命令。这里强调一下顺序,因为日志中的命令应用到状态机的顺序是一定不能颠倒的。很多共识算法为了提高效率,会允许日志乱序复制到非leader的节点,这就会在日志中出现很多空洞,就像下图这样,造成非常多的边界情况需要处理

在这里插入图片描述

Raft为了简化设计,避免对这些边界情况进行复杂处理,在日志复制阶段就保证了日志是有序且无空洞的。但日志复制阶段对于日志顺序的保证,能生效的前提是leader是正常的。如果leader出现宕机,他的后几个日志的状态就有可能出现不正常。这是,新leader是否具备这些不正常的日志,以及怎么处理这些不正常的日志,就是非常关键的了。

所以Raft通过几个补充规则完善整个算法,使算法可以在各类宕机问题下都不出错

这些规则包括:

1、Leader宕机处理:选举限制
2、Leader宕机处理:新leader是否提交之前任期内的日志条目
3、Follower和Candidate宕机处理
4、时间与可用性限制

3.1 Leader宕机处理:选举限制

如果仅仅依靠投票选举子问题中的规则会出现这种情况:
如果一个follower落后了leader若干条日志(但没有漏一整个任期),那么下次选举中,按照领导者选举里的规则,它依旧有可能当选leader。它在当选新leader后就永远也无法补上之前缺失的那部分日志,从而造成状态机之间的不一致。

所以需要对领导者选举增加一个限制,保证被选出来的leader一定包含了之前各任期的所有被提交的日志条目。 这条限制就是前面所说的选举规则的第6条。

RPC中包含了candidate的日志信息,如果投票者自己的日志比candidate的还新, 它会拒绝掉该投票请求。

Raft通过比较两份日志中最后一条日志条目的索引值和任期号来定义谁的日志比较

如果两份日志最后条目的任期号不同,那么任期号大的日志更“”。

如果两份日志最后条目的任期号相同,那么日志较长(日志号更大)的那个更“”。

在这里插入图片描述

(a)中s1是leader,到了(b)中,s1崩溃了,s5通过s3和s4的选票赢得了选举。但到了©中,s5又崩溃了,这时s1重启,并且选举成功,此时日志2已经被复制到了大多数机器上,但还没有被提交。可到了(d)中,s1再次崩溃,s5通过s2、s3和s4的选票能够再次选举成功。为什么s2和s3会投票给s5呢?因为它们的日志号相同,但是s5的任期号更大,这个投票就符合我们所说的选举规则。你可能会觉得这里的日志2已经被复制到了大多数节点,达到了在leader上提交的条件,却仍然被覆盖了,这会不会不安全呢?先别急,我们来看下一个规则。

3.2 Leader宕机处理:新Leader是否提交之前任期内的日志条目

一旦当前任期内的某个日志条目已经存储到过半的服务器节点上, leader就知道该日志条目可以被提交了。

这里要具体说明一下raft中具体的提交过程。

到目前为止,我们在raft中的所有讨论,提交都是一个单点的状态,而非集群的状态。为什么这么说呢?因为在前面日志复制中,我们提到leader收到了超过半数节点的复制成功反馈后,就可以应用日志到自己的状态机中了。这步就是leader中的提交。但这时候follower节点虽然复制到了日志,但还没有应用到自己的状态机上,也就是没有提交。所以说对整个集群来说,提交这个状态是并没有构成大多数的。那么follower是怎么知道自己可以提交呢?AppendEntries RPC中有一个leaderCommit的参数,通过这个参数,follower可以知道leader提交到了哪个日志。从而自己也可以应用到这个日志。你可能会想:那就是最快只有在下个日志发送时, follower才能提交上一个日志喽。那倒也不必,前面我们也提到过,raft的心跳消息也能判断是否能提交日志,因为它内部也有leaderCommit的参数。

此时心细的人可能可能就会问了,那么leader提交和follower提交之间必然会间隔一段时间,那么如果leader提交之后直接返回客户端,在通知follower提交之前,也就是一个心跳的时间之内,如果宕机了,是不是就会出现返回客户端成功,但是事务提交状态却没有在集群中保留下来呢?对于这个问题,我的看法是,raft算法是一个底层算法,本身只是应用实现高可用的一种方法。而与客户端交互本来应该是属于应用端的事情,理论上不是raft该担心的。通常来讲,要避免这个问题,应用会设置一个集群提交的概念,只有集群中超过半数的节点都完成提交,才认为集群提交完成。因为raft的leader可以通过AppendEntries RPC返回的success与否, 判定这个follower是否完成提交。所以说leader可以很容易的判断一个日志是否符合集群提交的条件。

我们回到新leader对老leader任期内日志的处理。如果某个leader在提交某个日志条目之前崩溃了,以后的leader会试图完成该日志条目复制。这里需要注意,是"复制"而非"提交"。这是什么情况呢?通过选举规则的限制,我们知道,一般情况下,新leader是具有老leader复制的日志的。但这些老的日志可能在新leader中还没提交,这时新leader会尝试将这个日志复制给所有其他follower,但它不会提交。为什么这里新leader不能提交呢?还是以上图为例,可能会出现这个情况:( c)到(d)的情况下,那怕s1当选leader,把日志2复制到大多数节点,最终却被日志3覆盖了。也就是没有在集群中提交日志2。如果s1在( c)时提交了日志2,就会出现不一致,因为日志2的任期号是老的,是2。假设( c)中S1重新当选leader,在s1、s2和s3中都把日志2提交了。这时候是不是集群中的大多数节点都提交了。可以认为集群上这个日志2已经提交了吧。也可以返回客户端提交成功了吧。但是这时候s1宕机了,集群重新选举,s5依靠它最高的任期号3,依旧可以拿到s2、s3和s4的选票当选leader,那么它是不是就会把日志2覆盖掉呢?是不是就会进入(d)中的情况呢?这时候就危险了,因为集群中已经提交的日志被覆盖掉了,所以说,raft永远不会通过计算副本数目的方式来提交之前任期内的日志条目。只有自己任期内的日志才能通过计算副本数目来提交,因为可以确认自己当前的任期号是最大的。因此新leader提交是危险的,但复制是安全的,依旧会把老日志复制到所有节点。 那么这些老的日志怎么才能提交呢?那就是等这个新leader在它的任期内新产生一个日志。在这个日志提交时,老leader任期内的日志也就可以提交了。注意,一定要是新任期内的日志提交,而不是复制。因为只有在提交时,新leader才能把自己的leaderCommit设为新任期内日志的日志号。这种方法相当于用一个新leader的一个新日志,把老任期内的日志保护了起来。这样老任期的日志就不会再被覆盖了。当然在,某些情况下,比如新leader可以确认所有的节点都已经复制了老任期内的日志,这时候理论上可以安心的用心跳提交老日志了。但是raft为了简化问题,使用了现在这种保守的做法。

raft中常用的补丁——no-operation

下图中( c)中,日志2已经复制到了大多数节点,但还没提交,所以s5仍可以当上leader,从而把日志2覆盖掉。也就是会变为(d1)中的情况。虽然这种处理不影响raft的正确性,但我们大部分时候都不希望出现这种情况。因为这个日志既然已经被复制到了大多数节点,那么顺势把它提交肯定是最高效的。所以可以附加这个no-op规则。

一个节点当选leader后,立刻发送一个自己当前任期的空日志体的AppendEntries RPC。这样,就可以把之前任期内满足提交条件的日志都提交了。其实这里起到的作用跟一个正常的日志是一样的。但新任期的第一个日志可能早来,可能晚来,而no-op就可以稳定的在第一时间保证这一点。

一旦no-op完成复制,就可以把之前任期内符合提交条件的日志保护起来了,从而就可以使它们安全提交。因为没有日志体,这个过程应该是很快的。

在这里插入图片描述

3.3 Follower和Candidate宕机处理

Follower和Candidate崩溃后的处理方式比leader崩溃要简单的多,并且两者的处理方式是相同的。

如果follower或candidate崩溃了,那么后续发送给他们的RequestVote和AppendEntriesRPCs都会失败。

Raft通过无限的重试来处理这种失败。如果崩溃的机器重启了,那么这些RPC就会成功地完成。

如果一个服务器在完成了一个RPC,但是还没有相应的时候崩溃了,那么它重启之后就会再次收到同样的请求。(Raft的RPC都是幂等的)

3.4 时间与可用性限制

raft算法整体不依赖客观时间,也就是说,哪怕因为网络或其他因素,造成后发的RPC先到,也不会影响raft的正确性。

只要整个系统满足下面的时间要求,Raft就可以选举出并维持一个稳定的leader :

广播时间(broadcastTime)<<选举超时时间(electionTimeout)<<平均故障时间(MTBF)
广播时间和平均故障时间是由系统决定的,但是选举超时时间是我们自己选择的。Raft的RPC需要接受并将信息落盘,所以广播时间大约是0.5ms到20ms,取决于存储的技术。因此,选举超时时间可能需要在10ms到500ms之间。大多数服务器的平均故障间隔时间都在几个月甚至更长。

4、集群成员变更

集群中进行成员变更的最大风险是,可能会同时出现 2 个领导者。比如在进行成员变更时,节点 A、B 和 C 之间发生了分区错误,节点 A、B 组成旧配置中的“大多数”,也就是变更前的 3 节点集群中的“大多数”,那么这时的领导者(节点 A)依旧是领导者。

另一方面,节点 C 和新节点 D、E 组成了新配置的“大多数”,也就是变更后的 5 节点集群中的“大多数”,它们可能会选举出新的领导者(比如节点 C)。那么这时,就出现了同时存在 2 个领导者的情况。这也是经典的脑裂问题。

在这里插入图片描述

如果出现了 2 个领导者,那么就违背了“领导者的唯一性”的原则,进而影响到集群的稳定运行。你要如何解决这个问题呢?也许有的同学想到了一个解决方法。

因为我们在启动集群时,配置是固定的,不存在成员变更,在这种情况下,Raft 的领导者选举能保证只有一个领导者。也就是说,这时不会出现多个领导者的问题,那我可以先将集群关闭再启动新集群啊。也就是先把节点 A、B、C 组成的集群关闭,然后再启动节点 A、B、C、D、E 组成的新集群。

在我看来,这个方法不可行。 为什么呢?因为你每次变更都要重启集群,意味着在集群变更期间服务不可用,肯定不行啊,太影响用户体验了。想象一下,你正在玩王者荣耀,时不时弹出一个对话框通知你:系统升级,游戏暂停 3 分钟。这体验糟糕不糟糕?

既然这种方法影响用户体验,根本行不通,那到底怎样解决成员变更的问题呢?最常用的方法就是单节点变更。

4.1 单节点变更

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

在这里插入图片描述

现在,让我们回到开篇的思考题,看看如何用单节点变更的方法,解决这个问题。为了演示方便,我们假设节点 A 是领导者:

在这里插入图片描述

目前的集群配置为[A, B, C],我们先向集群中加入节点 D,这意味着新配置为[A, B, C, D]。成员变更,是通过这么两步实现的:

  • 第一步,领导者(节点 A)向新节点(节点 D)同步数据;
  • 第二步,领导者(节点 A)将新配置[A, B, C, D]作为一个日志项,复制到新配置中所有节点(节点 A、B、C、D)上,然后将新配置的日志项应用(Apply)到本地状态机,完成单节点变更。

在这里插入图片描述

在变更完成后,现在的集群配置就是[A, B, C, D],我们再向集群中加入节点 E,也就是说,新配置为[A, B, C, D, E]。成员变更的步骤和上面类似:

  • 第一步,领导者(节点 A)向新节点(节点 E)同步数据;
  • 第二步,领导者(节点 A)将新配置[A, B, C, D, E]作为一个日志项,复制到新配置中的所有节点(A、B、C、D、E)上,然后再将新配置的日志项应用到本地状态机,完成单节点变更。

在这里插入图片描述

这样一来,我们就通过一次变更一个节点的方式,完成了成员变更,保证了集群中始终只有一个领导者,而且集群也在稳定运行,持续提供服务。

我想说的是,在正常情况下,不管旧的集群配置是怎么组成的,旧配置的“大多数”和新配置的“大多数”都会有一个节点是重叠的。 也就是说,不会同时存在旧配置和新配置 2 个“大多数”:

在这里插入图片描述

从上图中你可以看到,不管集群是偶数节点,还是奇数节点,不管是增加节点,还是移除节点,新旧配置的“大多数”都会存在重叠(图中的橙色节点)。

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

这里也可以采用前面提到过的no-op补丁,只有当领导者将 NO_OP 日志项应用后,再执行成员变更请求,这样就能防止脑裂。

4.2 多节点变更

集群先切换到一个过渡的配置,称之为联合一致(jointconsensus) ,再切换到新配置,这样我们只需要关注怎样避免在联合一致状态发生脑裂问题就可以了。

而配置信息作为一个日志体包装为一个普通的AppendEntries RPC,发送给所有的follower。

第一阶段,leader发起Cold,new,使整个集群进入联合一致状态。这时,所有RPC都要在新旧两个配置中都达到大多数才算成功。这个限制是联合一致能够避免脑裂问题的核心点。

第二阶段,leader发起Cnew,使整个集群进入新配置状态。这时,所有RPC只要在新配置下能达到大多数就算成功。

和普通日志不同,一旦某个服务器将该新配置日志条目增加到自己的日志中,他就会用该配置来做出未来所有的决策(服务器总是使用它日志中最新的配置,无论该配置日志是否已经被提交)。这意味着Leader不用等待Cold,new和Cnew返回,就会直接使用其中的新规则来作出决策我们假设leader

重点观察是否会产生脑裂现象,假设可以在集群成员变更任何时候宕机,大概有以下几种可能:

①leader在Cold,new未提交时宕机
②leader在Cold,new已提交但Cnew未发起时宕机
③leader在Cnew已发起时宕机

这三个点在下图中用红色箭头标记了出来
在这里插入图片描述

具体解读一下,增加机器时集群成员变更的两个阶段。

在这里插入图片描述

现在有s1、s2和s3三个节点,其中s3是当前任期的leader,这时我们增加s4和s5两个节点,raft会先将它们设置为只读,等到它们追上日志进度后,才会开始集群成员变更,这一点后面会补充说明。然后现任leader s3发起Cold,new,并复制给了s3和s4。这就是刚刚提到有可能出现脑裂问题的情况。注意,这时的s3、s4和s5已经进入了联合一致状态,它们的决策要在新l旧两个配置中都达到大多数才算成功。

第一种情况:leader在Cold,new未提交时宕机
leader在Cold,new未提交时宕机,s1、s2都是老配置,然后超时,开始进行选举,并且可以以两票产生一个老配置的leader。但是在联合一致的状态下。s3、s4和s5中的任意节点必须要在老配置s1、s2和s3,和新配置s1到s5下都拿到超过半数的选票才能当选。因为s1和s2投票给了他们之中的一个节点,所以在老配置中,超过半数的条件是不满足的,这就导致s1、s2和s3中无法再选出一个leader,这样集群成员变更就失败了,但是避免了出现两个leader的脑裂情况,仍然保证了raft的正确性。

这里其实还有一种可能,就是重新选出的新leader具有Cold,new,比如图中s1、s3、s4和s5都复制了Cold,new,但还没有提交。这时选出的新leader一定具有Cold,new,但按照安全性限制,这个新leader无法提交Cold,new,不过可以让它继续发送Cnew,继续进行集群成员变更。这里后续的情况我们等等再讨论

在这里插入图片描述

第二种情况:leader在Cold,new已提交但Cnew未发起时宕机

我们继续,假设s3没有宕机,并且正常复制Cold,new,满足了联合一致条件。比如图中s2、s3和s4都复制了Cold,new,这时老配置中s2和s3超过了半数,新配置中s2、s3和s4也超过了半数,Cold,new就可以提交了。这时有可能出现第二种leader宕机的情况,也就是leader在Cold,new已提交,但Cnew未发起时宕机。选举限制安全性规则决定了选出的新leader一定是具有Cold,new的,也就是符合在两种配置集群中都超过半数的情况。所以这时候已经不存在再出现脑裂问题的可能了。

在这里插入图片描述

这里要说明一下,就像下图一样,集群成员变更的过程中依旧可以对外服务。联合一致状态下,也是可以正常执行命令的,但也需要在两个配置集群中都达到大多数才能提交。

在这里插入图片描述

第三种情况:leader在Cnew已发起时宕机
Cold,new提交后,leader就会发起Cnew,这时leader只要满足新配置中的条件,就可以提交日志。比如说下图中的s3、s4和s5复制了Cnew,Cnew就可以提交了,不用再在s1、s2和s3中达到大多数,这时有可能出现第三种leader宕机的情况,leader在Cnew已发起时宕机。这时已经复制了Cnew的节点会只按新配置选举,没有复制Cnew的节点会按新老配置选举。没有复制Cnew的节点选举成功也会发Cnew。有没有复制Cnew的节点都可能当上leader,但没有复制Cnew的节点选举成功也会发Cnew,这里不会有问题。

在这里插入图片描述

但有一种情况需要专门讨论一下,那就是缩减节点的情况。如图中,由s1到s5缩减为s1、s2和s3。Cold,new仍需要复制到两个集群中的大多数才能提交。
在这里插入图片描述

但Cnew只需要复制到s1、s2和s3中的两个就可以提交了。这时如果leader s3宕机了,Cnew会不会被覆盖呢?不会的,因为处于联合一致性状态的节点,也就是只复制了Cold,new,没有复制Cnew的节点,必须要在两个集群中都达到大多数选票才能够选举成功。而s2和s3不会投票给s1、s4和s5中的任意一个。所以s3宕机了,只有s2才能当选,已经提交的Cnew是不会被覆盖的

这里再补充一下,刚刚说过的,Cold,new的复制满足了在新老配置中都超过半数的条件,但leader宕机,这是新leader无法提交Cold,new ,但继续发送Cnew 的情况。如下图中,leader s3复制了Cold,new到了新老配置的大多数节点,满足联合一致。但s3未提交Cold,new就宕机了,这时s1当选leader。根据安全性规则,我们可以知道s1是不能直接提交Cold,new的,所以s1只能继续复制Cnew 。这时它把Cnew 复制到了s1、s4和s5节点,构成了新配置集群中的大多数,但这是它还是不能提交。因为它没有构成老配置中的大多数,s2和s3并没有反馈,Cold,new的提交规则并没有被满足。这样提交的Cnew 是不安全的。论文中没有给出这种情况的解决方法,在某些实现中,可以强制让Cnew按照联合一致的规则提交,如果当前leader在一段时间内满足不了这个条件,那么它就会自动退位

在这里插入图片描述

这样我们就明确了集群成员变更两阶段的全程。

  • 在Cold,new发起但未提交时,raft集群还未进入联合一致状态。这时leader宕机,可以仅靠老配置选出来的新leader。
  • 一旦Cold,new提交,raft集群就进入了联合一致状态,这时leader宕机,选出的新leader也要符合联合一致的选票规则了。
  • Cold,new提交后, leader就可以发起Cnew,从发起Cnew开始,集群就可以仅靠新配置进行选举和日志复制了。
  • 下图中,红色箭头处,如果是缩减集群的情况下,leader可能自身就是缩减的对象,那么它会在Cnw复制完成后自动退位,这点我们接下来会进行补充说明。

在这里插入图片描述

集群成员变更还有三个补充规则需要说明一下

  1. 新增节点时,需要等新增的节点完成日志同步再开始集群成员变更(防止集群在新增节点还未同步日志时就进入联合一致状态或新配置状态,这时正常命令的日志就要同步到更多的副本,导致新日志难以提交。让新增节点自动完成同步日志很简单,只需要让新节点在同步完成日志前不具有投票权,也不参与计数,也就是处于一个只读的状态即可)
  2. 缩减节点时,leader本身可能就是要缩减的节点,这时它会在完成C~new~的提交后自动退位(在发起Cnew后,要退出集群的leader就会处在操纵一个不包含它本身的raft集群的状态下。这时它可以发送Cnew日志,但是日志计数时不计自身
  3. 为了避免下线的节点超时选举而影响集群运行,服务器会在它确信集群中有leader存在时拒绝Request Vote RPC。(因为Cnew的新leader不会再发送心跳给要退出的节点,如果这些节点没有及时下线,它们会超时增加任期号后发送Request Vote RPC。虽然它们不可能当选leader,但会导致raft集群进入投票选举阶段,影响集群的正常运行。为了解决这个问题,Raft在Request Vote RPC上补充了一个规则:一个节点如果在最小超时时间之内收到了Request Vote RPC,那么它会拒绝此RPC。
    这样,只要follower连续收到leader的心跳,那么退出集群节点的Request Vote RPC就不会影响到raft集群的正常运行了。)

多节点变更的方法需要处理边界的情况很多,实现起来复杂,实际上是有点违背raft的设计初衷的。所以现在大多数对raft的算法的实现,都是基于单节点变更的方法。

五、总结

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

Raft 区分于其他共识算法的三个特征:

Strong leader:在Raft中,日志只能从leader流向其他服务器。这简化了复制日志的管理,使得raft更容易理解。
Leader election:Raft使用随机计时器进行leader选举。这只需在任何共识算法都需要的心跳上增加少量机制,同时能够简单快速地解决冲突。
Membership changes:Raft使用一种共同一致的方法来处理集群成员变更的问题,变更时,两种不同的配置的大多数机器会重叠。这允许整个集群在配置变更期间可以持续正常运行。不过现在大多数都采用单节点变更。

关于Raft的领导者选举限制和局限:

1.读写请求和数据转发压力落在领导者节点,导致领导者压力。
2.大规模跟随者的集群,领导者需要承担大量元数据维护和心跳通知的成本。
3.领导者单点问题,故障后直到新领导者选举出来期间集群不可用。
4.随着候选人规模增长,收集半数以上投票的成本更大。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
raft共识算法是一种分布式一致性算法,用于解决分布式系统中节点之间达成一致性的问题。它主要包含了Leader选举、日志复制和安全性等基本机制。 在raft算法中,节点分为Leader、Follower和Candidate三种状态。初始状态下所有节点都是Follower,然后它们通过相互通信进行Leader选举。选出的Leader负责接收客户端请求并进行日志复制等操作。如果Leader出现故障或无法通信,那么其他节点会重新进行选举,选出新的Leader。 日志复制是raft算法的关键过程,Leader负责将客户端请求记录在日志中,然后将日志复制给所有的Follower节点。Follower节点在接收到Leader的日志之后进行存储,然后发送应答给Leader确认。只有当大多数节点都复制了同一条日志之后,这条日志才算是已提交的。 raft算法还通过逻辑时钟和心跳机制来保证系统的一致性。每个节点都有自己的逻辑时钟,用于识别事件的顺序。Leader节点会定期发送心跳信号给Follower节点,以确保它们的存活状态。 在raft算法中,安全性是非常重要的一部分。它通过限制节点之间的信息交换,避免了“脑裂”等问题的发生。同时,每个节点都有持久性的存储,当节点宕机之后可以通过快照恢复。 总的来说,raft共识算法通过Leader选举、日志复制和安全性等机制,实现了分布式系统中节点之间的一致性。它比Paxos算法更容易理解和实现,因此在实际应用中被广泛使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值