Raft-集群成员变更

32次阅读

共计 5059 个字符,预计需要花费 13 分钟才能阅读完成。

原文地址: https://qeesung.github.io/202…

Raft 集群成员变更

在前面三个章节中,我们介绍了 Raft 的:

  • 领导人选举
  • 日志复制
  • 安全性

上面的讨论都是基于 Raft 集群成员恒定不变的,然而在很多时候,集群的节点可能需要进行维护,或者是因为需要扩容,那么就难以避免的需要向 Raft 集群中添加和删除节点。最简单的方式就是停止整个集群,更改集群的静态配置,然后重新启动集群,但是这样就丧失了集群的可用性,往往是不可取的,所以 Raft 提供了两种在不停机的情况下,动态的更改集群成员的方式:

  • 单节点成员变更:One Server ConfChange
  • 多节点联合共识:Joint Consensus

动态成员变更存在的问题

在 Raft 中有一个很重要的安全性保证就是只有一个 Leader,如果我们在不加任何限制的情况下,动态的向集群中添加成员,那么就可能导致同一个任期下存在多个 Leader 的情况,这是非常危险的。

如下图所示,从 Cold 迁移到 Cnew 的过程中,因为各个节点收到最新配置的实际不一样,那么肯能导致在同一任期下多个 Leader 同时存在。

比如图中此时 Server3 宕机了,然后 Server1 和 Server5 同时超时发起选举:

  • Server1:此时 Server1 中的配置还是 Cold,只需要 Server1 和 Server2 就能够组成集群的 Majority,因此可以被选举为 Leader
  • Server5:已经收到 Cnew 的配置,使用 Cnew 的配置,此时只需要 Server3,Server4,Server5 就可以组成集群的 Majority,因为可以被选举为 Leader

换句话说,以 Cold 和 Cnew 作为配置的节点在同一任期下可以分别选出 Leader。

所以为了解决上面的问题,在集群成员变更的时候需要作出一些限定。

单节点成员变更

所谓 单节点成员变更,就是每次只想集群中添加或移除一个节点。比如说以前集群中存在三个节点,现在需要将集群拓展为五个节点,那么就需要一个一个节点的添加,而不是一次添加两个节点。

这个为什么安全呢?很容易枚举出所有情况,原有集群奇偶数节点情况下,分别添加和删除一个节点。在下图中可以看出,如果每次只增加和删除一个节点,那么 Cold 的 Majority 和 Cnew 的 Majority 之间一定存在交集,也就说是在同一个 Term 中,Cold 和 Cnew 中交集的那一个节点只会进行一次投票,要么投票给 Cold,要么投票给 Cnew,这样就避免了同一 Term 下出现两个 Leader。

变更的流程如下:

  1. 向 Leader 提交一个成员变更请求,请求的内容为服务节点的是添加还是移除,以及服务节点的地址信息
  2. Leader 在收到请求以后,回向日志中追加一条 ConfChange 的日志,其中包含了 Cnew,后续这些日志会随着 AppendEntries 的 RPC 同步所有的 Follower 节点中
  3. ConfChange 的日志被添加到日志中是立即生效(注意:不是等到提交以后才生效)
  4. ConfChange 的日志被复制到 Cnew 的 Majority 服务器上时,那么就可以对日志进行提交了

以上就是整个单节点的变更流程,在日志被提交以后,那么就可以:

  1. 马上响应客户端,变更已经完成
  2. 如果变更过程中移除了服务器,那么服务器可以关机了
  3. 可以开始下一轮的成员变更了,注意在上一次变更没有结束之前,是不允许开始下一次变更的

可用性

可用性问题

在我们向集群添加或者删除一个节点以后,可能会导致服务的不可用,比如向一个有三个节点的集群中添加一个干净的,没有任何日志的新节点,在添加节点以后,原集群中的一个 Follower 宕机了,那么此时集群中还有三个节点可用,满足 Majority,但是因为其中新加入的节点是干净的,没有任何日志的节点,需要花时间追赶最新的日志,所以在新节点追赶日志期间,整个服务是不可用的。

在接下来的子章节中,我们将会讨论三个服务的可用性问题:

  • 追赶新的服务器
  • 移除当前的 Leader
  • 中断服务器

追赶新的服务器

在添加服务器以后,如果新的服务器需要花很长时间来追赶日志,那么这段时间内服务不可用。

如下图所示:

  • 左图:向集群中添加新的服务器 S4 以后,S3 宕机了,那么此时因为 S4 需要追赶日志,此时不可用
  • 右图:向集群中添加多个服务器,那么添加以后 Majority 肯定是包含新的服务器的,那么此时 S4,S5,S6 需要追赶日志,肯定也是不可用的

新加入集群中的节点可能并不是因为需要追赶大量的日志而不可用,也有可能是因为网络不通,或者是网速太慢,导致需要花很长的时间追赶日志。

在 Raft 中提供了两种解决方案:

  • 在集群中加入新的角色 LeanerLeaner 只对集群的日志进行复制,并不参加投票和提交决定,在需要添加新节点的情况下,添加 Leaner 即可。
  • 加入一个新的 Phase,这个阶段会在固定的 Rounds(比如 10)内尝试追赶日志,最后一轮追赶日志的时间如果小于ElectionTimeout, 那么说明追赶上了,否则就抛出异常

下面我们就详细讨论一下第二种方案。

在固定 Rounds 内追赶日志

如果需要添加的新的节点在很短时间内可以追赶上最新的日志,那么就可以将该节点添加到集群中。那要怎么判断这个新的节点是否可以很快时间内追赶上最新的日志呢?

Raft 提供了一种方法,在配置变更之前引入一个新的阶段,这个阶段会分为多个 Rounds(比如 10)向 Leader 同步日志,如果新节点能够正常的同步日志,那么每一轮的日志同步时间都将缩短,如果在最后一轮 Round 同步日志消耗的时间小于ElectionTimeout,那么说明新节点的日志和 Leader 的日志已经足够接近,可以将新节点加入到集群中。但是如果最后一轮的 Round 的日志同步时间大于ElectionTimeout,就应该立即终止成员变更。

移除当前的 Leader

如果在 Cnew 中不包含当前的 Leader 所在节点,那么如果 Leader 在收到 Cnew 配置以后,马上退位成为 Follower,那么将会导致下面的问题:

  • ConfChange的日志尚未复制到 Cnew 中的大多数的节点
  • 马上退位成为 Follower 的可能因为超时成为新的 Leader,因为该节点上的日志是最新的,因为日志的安全性,该节点并不会为其他节点投票

为了解决以上的问题,一种很简单的方式就是通过 Raft 的拓展 Leadership transfer 首先将 Leader 转移到其他节点,然后再进行成员变更,但是对于不支持 Leadership transfer 的服务来说就行不通了。

Raft 中提供了一种策略,Leader 应该在 Cnew 日志提交以后才退位。

中断的服务器

如果 Cnew 中移除了原有集群中的节点,因为被移除的节点是不会再收到心跳信息,那么将会超时发起一轮选举,将会造成当前的 Leader 成为 Follower,但是因为被移除的节点不包含 Cnew 的配置,所以最终会导致 Cnew 中的部分节点超时,重新选举 Leader。如此反反复复的选举将会造成很差的可用性。

一种比较直观的方式是采用 Pre-Vote 方式,在任何节点发起一轮选举之前,就应该提前的发出一个 Pre-Vote 的 RPC 询问是否当前节点会同意给当前节点投票,如果超过半数的节点同意投票,那么才发生真正的投票流程的,有点类似于 Two-Phase-Commit,这种方式在正常情况下,因为被移除的节点没有包含 Cnew 的ConfChange 日志,所以在 Pre-Vote 情况下,大多数节点都会拒绝已经被移除节点的 Pre-Vote 请求。

但是上面只能处理大多数正常的情况,如果 Leader 收到 Cnew 的请求后,尚未将 Cnew 的 ConfChange 日志复制到集群中的大多数,Cnew 中被移除的节点就超时开始选举了,那么 Pre-Vote 此时是没有用的,被移除的节点仍有可能选举成功。顺便一说,这里的 Pre-Vote 虽然不能解决目前的问题,但是针对脑裂而产生的任期爆炸式增长和很有用的,这里就不展开讨论了。

就如下图所示,S4 收到 Cnew 成员变更的请求,立马将其写入日志中,Cnew 中并不包含 S1 节点,所以在 S4 将日志复制到 S2,S3 之前,如果 S1 超时了,S2,S3 中因为没有最新的 Cnew 日志,仍让会投票给 S1,此时 S1 就能选举成功,这不是我们想看到的。

Raft 中提供了另一种方式来避免这个问题,如果每一个服务器如果在 ElectionTimeout 内收到现有 Leader 的心跳(换句话说,在租约期内,仍然臣服于其他的 Leader),那么就不会更新自己的现有 Term 以及同意投票。这样每一个 Follower 就会变得很稳定,除非自己已经知道的 Leader 已经不发送心跳给自己了,否则会一直臣服于当前的 leader,尽管收到其他更高的 Term 的服务器投票请求。

任意节点的 Joint Consensus

上面我们提到单节点的成员变更,很多时候这已经能满足我们的需求了,但是有些时候我们可能会需要随意的的集群成员变更,每次变更多个节点,那么我们就需要 Raft 的Joint Consensus, 尽管这会引入很多的复杂性。

Joint Consensus会将集群的配置转换到一个临时状态,然后开始变更:

  1. Leader 收到 Cnew 的成员变更请求,然后生成一个 Cold,new 的 ConfChang 日志,马上应用该日志,然后将日志通过 AppendEntries 请求复制到 Follower 中,收到该 ConfChange 的节点马上应用该配置作为当前节点的配置
  2. 在将 Cold,new 日志复制到 大多数 节点上时,那么 Cold,new 的日志就可以提交了,在 Cold,new 的 ConfChange 日志被提交以后,马上创建一个 Cnew 的 ConfChange 的日志,并将该日志通过 AppendEntries 请求复制到 Follower 中,收到该 ConfChange 的节点马上应用该配置作为当前节点的配置
  3. 一旦 Cnew 的日志复制到大多数节点上时,那么 Cnew 的日志就可以提交了,在 Cnew 日志提交以后,就可以开始下一轮的成员变更了

为了理解上面的流程,我们有几个概念需要解释一下:

  • Cold,new:这个配置是指 Cold,和 Cnew 的联合配置,其值为 Cold 和 Cnew 的配置的交集,比如 Cold 为[A, B, C],Cnew 为[B, C, D],那么 Cold,new 就为[A, B, C, D]
  • Cold,new 的大多数:是指 Cold 中的大多数和 Cnew 中的大多数,如下表所示,第一列因为 Cnew 的 C, D 没有 Replicate 到日志,所以并不能达到一致
ColdCnewReplicate 结果是否是 Majority
A, B, CB, C, DA+, B+, C-, D-
A, B, CB, C, DA+, B+, C+, D-
A, B, CB, C, DA-, B+, C+, D-

由上可以看出,整个集群的变更分为几个过渡期,就如下图所示,在每一个时期,每一个任期下都不可能出现两个 Leader:

  1. Cold,new 日志在提交之前,在这个阶段,Cold,new 中的所有节点有可能处于 Cold 的配置下,也有可能处于 Cold,new 的配置下,如果这个时候原 Leader 宕机了,无论是发起新一轮投票的节点当前的配置是 Cold 还是 Cold,new,都需要 Cold 的节点同意投票,所以不会出现两个 Leader
  2. Cold,new 提交之后,Cnew 下发之前,此时所有 Cold,new 的配置已经在 Cold 和 Cnew 的大多数节点上,如果集群中的节点超时,那么肯定只有有 Cold,new 配置的节点才能成为 Leader,所以不会出现两个 Leader
  3. Cnew 下发以后,Cnew 提交之前,此时集群中的节点可能有三种,Cold 的节点(可能一直没有收到请求),Cold,new 的节点,Cnew 的节点,其中 Cold 的节点因为没有最新的日志的,集群中的大多数节点是不会给他投票的,剩下的持有 Cnew 和 Cold,new 的节点,无论是谁发起选举,都需要 Cnew 同意,那么也是不会出现两个 Leader
  4. Cnew 提交之后,这个时候集群处于 Cnew 配置下运行,只有 Cnew 的节点才可以成为 Leader,这个时候就可以开始下一轮的成员变更了

其他

自动的成员变更

如果我们给集群增加一些监控,比如在检测到机器宕机的情况下,动态的向系统中增加新的节点,这样就可以做到自动化,增加系统的节点数。

集群动态配置

一般情况下,我们都是使用静态文件的方式来描述集群中的成员信息,但是有了成员变更的算法,我们就可以动态配置的方式来设置集群的配置信息

正文完
 0