Raft Part-2 成员变更
前文已经讲解了 raft 的日志复制和选举,raft 的日志复制主要是去掉 mutli-paxos 中的日志空洞,让日志完全匹配,减少了选主以后 leader 的操作。通过控制 leader 被选出来的条件,让 leader 拥有当前 log 中最全的日志做到数据前后一致性和不需要补数据。
quorum
上文已经提到,raft 对外能够保证的高可用为至少一半的机器掉线,而且总的服务需要是技术个。写入的时候需要 major 的服务返回成功才能判定当前写入成功,选举也一样,只有 major 个数的服务返回成功,才能确定成为了 leader。为什么这里会有这个要求呢?
首先输入输入的操作只有两种,读或者写。根据抽屉原理,如果当前有 5 个鸡蛋,放入 4 个盒子,那么肯定有一个盒子有两个鸡蛋。如果我们将 server 看作抽屉,鸡蛋看作读写操作。记当前的服务的 server 个数为 N,对外提供服务的 server 为 A,对外提供写的服务为 WQ,提供读的服务为 RQ。只需要保证
RQ+WQ>A
那么肯定存在一台服务上,读请求能够访问当写请求。而 major 是里面的一个特例。因为在某些场景下我们并不需要整个服务都是我们写入目标,比如当前已经有 N=7 台服务器,我们的数据并不需要放在 major(4)台服务器上,只需要保证集群状态信息,如分片部署在那些服务器上,然后就能够控制我们的数据分片。如:现在有一个 3 个 shard 的日志,我们每个日志打乱成为 segment,只需要在 7 台服务器中记录下 segment 和 shard 的对应关系,就可以在集群中控制每一个 segement 的备份数。这事 particA 的思想。
所以 quorum 其实控制的只是一个写入必须被读取查看到的功能,而 major 是其中的一个特例。后续在写到 raft 的 configuration change 的时候会在来看这个问题。这里提出来只是真的 major 写入的一个思考,即:如果采用 raft 进行日志复制,是否必须将日志写入集群的 major。当然还有个mutli-raft,该思想是在 7 台服务器中部署多个 raft 实例,但是将他们的心跳,append 请求等等进行一个网络级别的合并,减少网络开销,从而提升 raft 性能。
Configuration Change
当我们服务部署完成后,有时候是需要针对集群进行一定的修改,比如替换某个节点,或者对集群进行一个平滑迁移(将集群的数据慢慢迁移到新的节点后,慢慢下线现在的集群),raft 中称之为 configure change。
上图就是一个在集群中新增集群。从原油的{server1,server2,server3},新增{server4,server5}。如果直接将新的节点增加上去,可能会造成集群选举出两个 leader。如上图:存在{server1.server2}在某时刻使用 configureOld 的情况,即集群中有 3 个节点,此时 server1 可以通过获得 server2 的 vote 成为 leader,对应的{server3,server4,server5},使用的 configureNew 进行选举,也可以其中 server 3 可以通过获得 server4,server5 的 vote 成为 leader。于是集群在该时刻可能拥有两个 leader。这是非常危险的。
一次增加一个 node
回看下,上文提到的问题就在于,由于一次性增加了两个 server,导致 major 这个限制没有成功。如果每次只改变一个节点呢?
上文是 raft 作者在大论文中提到的每次变更一个 server 的实现。每次变更一个节点是不会破坏 major 的这个设定的。如 a 场景下,在 4 个 server 中新增一个,那么 major 还是原来的 3,符合抽屉原理,至少有一个重合的 server。需要注意的是,由于每次增加或者删除一个节点,所以在集群中不会存在两个 leader,一个使用旧的 config,一个使用新的 config。所以,单步变更的时候,变更日志是达到就应用,而不是等待当前的 apply 到状态机中。
但是该方案在 2015 年的时候被作者发现存在一个bug。该问题主要存在如:如果当前有{a,b,c},现在新增一个{d},然后删除 a,最后成为{b,c,d}的集群。如果按照一个 server 的算法进行执行,那么就会有一种特殊的情况
服务不可用
-
使用先增加再删除
- {a,b,c}部署在 3 机房,在某个时刻,a 出现问题,一般情况下,都会使用相同机房的机器将它替换掉,这里涉及两个操作,第一个是{add,d},第二个{delete a}。在新增 d 以后,出现了网络隔离{a,d},{b,c}由于此时的 major 为 4/2+1=3,导致当前的集群是不能选出 leader,造成服务不可用。
-
使用先删除在增加,如果将上面的 a 先删除,在增加 b,则不会出现问题。
覆盖数据
C₀ = {a, b, c, d}
Cᵤ = C₁ ∪ {u}
Cᵥ = C₁ ∪ {v}
Lᵢ: Leader in term `i`
Fᵢ: Follower in term `i`
☒ : crash
|
u | Cᵤ F₂ Cᵤ
--- | ----------------------------------
a | C₀ L₀ Cᵤ ☒ L₂ Cᵤ
b | C₀ F₀ F₁ F₂ Cᵤ
c | C₀ F₀ F₁ Cᵥ Cᵤ
d | C₀ L₁ Cᵥ ☒ Cᵤ
--- | ----------------------------------
v | Cᵥ time
+-------------------------------------------->
t₁ t₂ t₃ t₄ t₅ t₆ t₇ t₈
- t₁:
abcd4 节点在 term 0 选出 leader=a, 和 2 个 followerb,c; - t₂:
a广播一个变更日志Cᵤ, 使用新配置Cᵤ, 只发送到a和u, 未成功提交; - t₃:
a宕机 - t₄:
d在 term 1 被选为 leader, 2 个 follower 是b,c; - t₅:
d广播另一个变更日志Cᵥ, 使用新配置Cᵥ, 成功提交到c,d,v; - t₆:
d宕机 - t₇:
a在 term 2 重新选为 leader, 通过它本地看到的新配置Cᵤ, 和 2 个 followeru,b; - t₈:
a同步本地的日志给所有人, 造成已提交的Cᵥ丢失.
由于已经提交的 Cᵥ 被覆盖了,所以违反了 raft 的安全性。
对此,raft 的作者提出解决办法。首先,出现上面的问题的原因
a)集群原本是偶数个
b)并发执行多次变更
b)集群中新的在进行 config 修改的过程中出现了 leader change。即 leader 从 a-->d->a。导致了部分数据在提交后被上一任的 leader 恢复后修改。
其实个人觉得发生的问题和 raft 中关于配置生效的时机有一定的关系,raft 并不是等到 config 的 log 被 commit 以后才生效,而且是一旦接受到新的配置立即使用。为什么可以这么做,在论文中 raft 作者提到:
As stated above, servers always use the latest configuration in their logs, regardless of whether that configuration entry has been committed. This allows leaders to easily avoid overlapping configuration changes (the third item above), by not beginning a new change until the previous change’s entry has committed. It is only safe to start another membership change once a majority of the old cluster has moved to operating under the rules of Cnew. If servers adopted Cnew only when they learned that Cnew was committed, Raft leaders would have a difficult time knowing when a majority of the old cluster had adopted it. They would need to track which servers know of the entry’s commitment, and the servers would need to persist their commit index to disk; neither of these mechanisms is required in Raft. Instead, each server adopts Cnew as soon as that entry exists in its log, and the leader knows it’s safe to allow further configuration changes as soon as the Cnew entry has been committed. Unfortunately, this decision does imply that a log entry for a configuration change can be removed (if leadership changes); in this case, a server must be prepared to fall back to the previous configuration in its log.
如果需要等待 commit 以后在执行切换,则 leader 无法感知到何时可以进行日志切换,他必须知道哪些 server 已经 commit 了当前的 Cnew 才能够进行成员变更提交。如果使用看到日志就进行执行最新的 Cnew,则 leader 在获取到 major 的时候,就知道当前的集群已经被提交了。这里其实是两阶段协议,第一阶段,发送 Cnew 到 server 中,当 major 接受到以后,此时执行 commit,然后提交当前的 Cnew 配置,并且执行 Cnew 的操作,比如删除某个节点,某个节点此时就可以正式下线了。
也是因为这个 leader 无法感知到当前的 Cnew 被复制到哪些节点中了,所以才会出现上文中的 Bug,因为不是等待 commit 就使用新的配置,而是 append 以后就使用新的配置,所以会存在 leader 切换以后,原来的配置日志被覆盖的情况,因为 raft 本身是需要在 commit 以后在 apply 到状态机中的,所以这个立马使用其实和 raft 的状态及 apply 的条件不一致,所以作者提出没次 leader 切换就必须提交一次 noop,这个就是隐式提交前期所有的日志文件。保证前一任 leader 回到集群后不会被选举为 leader。
当前 etcd 中采用的方式是另外一种模式,即 commit 以后才生效。后续看 etcd 的代码的时候在详细了解下。(leader 对 follower 进行一个 tack 也是很好做的,因为本来 append 的时候就需要 follower 和 leader 日志进行完全匹配)。
joint consensus
joint consensus 又被称之为联合共识。是成员变化的另外一个解。
联合共识需要保证:
- logEntry 需要复制到 Cnew 和 Cold 中两个集群的数据
- 两个配置中的 server 都有可能被当选为 leader
- 集群无论是 append 还是 election 都必须经过两个配置中的 major 同意。
如 figure4.8 所示,在 Cold,new 的时间点。当前的 leader 收到了 Cnew,就将当前的配置进行合并,即 后续会在使用这个配置进行 major 的判断,即需要 Cold 中的 major 和 Cnew 中的 major 都收到了 append 的日志以后,才进行下一步 Cnew 的提交,提交成功以后,无论 Cold 还是 Cnew中的成员都只能选出一个 leader,如果中途发生 leader 切换,后续是否成功只取决于的日记是否存在当前的 leader 中。
一旦 被成功 commit 以后,就使用 Cnew,并且发送到 的 major 中,最终等到 Cnew被提交以后就执行对于的上下线操作。
该集群变化仍然有一定的问题:
加入的节点为新节点
如果新加入的节点为空节点,没有存放任何数据,由于 raft 的日志完全性,他必须要等待状态机追上 leader 后才能进入 major,否则的话,可能让那个服务不可用,比如在原来的 3 个节点中加入 2 个全新的节点,那么此时 major 为 3,如果原来的 3 个节点挂点任何一个,append 日志的过程中,新加入的节点不断的追加日志,一直无法达成 major,导致写入失败。该问题的解决办法就是先加入一个 learner 节点,让该节点一直处于追加数据的状态,一直到数据被追加上来以后,才能够进行 major
从集群中移除 leader 节点
从当前的集群中移除 leader,则 leader 必须要等到 Cnew 被 commit 以后才能够卸任。这意味着会有一个时间窗口(这个 leader 提交 Cnew 期间),这个 leader 在 管理着一个不包括自己的集群;它在同步 log entries 给其他节点,但自己却并不被算作大多数。 Leader 切换发生在 Cnew 提交之后,因为这是新配置能独立工作的最早时刻( 从 Cnew 开始,肯定能选出一个 leader);在这个时刻之前,有可能只有 Cold 中的某个节点才能被选为 leader。
移除的节点或者网络分区的节点回到集群
如果移除的节点没有收到 shutdown 的命令,那么他可能不断的增大自己的 term 加入回集群,导致现在的集群被打断发生重新选举。发生网络分区的时候也可能出现当前的 server 不断增加自己的 term 选举,最后导致 term 跨越很大一段。开始的问题可以通过 lease 一样的方式,如果当前的节点仍然收到来自 leader 的心跳,则不会给其他人投票。类似的,如果 leader 发现自己和集群能够正常心跳,则不会主动让出自己的 leader。后面的问题是没次发送 vote 请求的时候,需要发送 prevote,只有 prevote 成功以后,才会继续执行自己的 vote 请求,否则不会增加 term 发送 vote 扰乱集群。