共计 6306 个字符,预计需要花费 16 分钟才能阅读完成。
简介: 成员变更是一致性零碎实现绕不开的难题,对于晋升运维能力以及服务可用性都有很大的帮忙。本文从 Raft 成员变更实践登程,介绍了 Raft 成员变更和单步成员变更的问题,其中包含 Raft 驰名的 Bug。对于 Raft 成员变更的工程实现上须要思考的问题,本文给出了一些工程实践经验。
一 引言
成员变更是一致性零碎实现绕不开的难题,对于晋升运维能力以及服务可用性都有很大的帮忙。
本文从 Raft 成员变更实践登程,介绍了 Raft 成员变更和单步成员变更的问题,其中包含 Raft 驰名的 Bug。
对于 Raft 成员变更的工程实现上须要思考的问题,本文给出了一些工程实践经验。
二 Raft 成员变更简介
分布式系统运行过程中节点常常会呈现故障,须要反对节点的动静减少和删除。
成员变更是在集群运行过程中扭转运行一致性协定的节点,如减少、缩小节点、节点替换等。成员变更过程不能影响零碎的可用性。
成员变更也是一个一致性问题,即所有节点对新成员达成统一。然而成员变更又有其特殊性,因为在成员变更的过程中,参加投票的成员会发生变化。
如果将成员变更当成个别的一致性问题,间接向 Leader 节点发送成员变更申请,Leader 同步成员变更日志,达成多数派之后提交,各节点提交成员变更日志后从旧成员配置(Cold)切换到新成员配置(Cnew)。
因为各个节点提交成员变更日志的时刻可能不同,造成各个节点从旧成员配置(Cold)切换到新成员配置(Cnew)的时刻不同。可能在某一时刻呈现 Cold 和 Cnew 中同时存在两个不相交的多数派,进而可能选出两个 Leader,造成不同的决定,毁坏安全性。
图 1 成员变更的某一时刻 Cold 和 Cnew 中同时存在两个不相交的多数派
如图 1 是 3 个节点的集群扩大到 5 个节点的集群,间接扩大可能会造成 Server1 和 Server2 形成老成员配置的多数派,Server3、Server4 和 Server5 形成新成员配置的多数派,两者不相交从而可能导致决定抵触。
因为成员变更的这一特殊性,成员变更不能当成个别的一致性问题去解决。为了解决这个问题,Raft 提出了两阶段的成员变更办法 Joint Consensus。
1 Joint Consensus 成员变更
Joint Consensus 成员变更让集群先从旧成员配置 Cold 切换到一个过渡成员配置,称为联结统一成员配置(Joint Consensus),联结统一成员配置是旧成员配置 Cold 和新成员配置 Cnew 的组合 Cold,new,一旦联结统一成员配置 Cold,new 提交,再切换到新成员配置 Cnew。
图 2 Joint Consensus 成员变更
Leader 收到成员变更申请后,先向 Cold 和 Cnew 同步一条 Cold,new 日志,尔后所有日志都须要 Cold 和 Cnew 两个多数派的确认。Cold,new 日志在 Cold 和 Cnew 都达成多数派之后能力提交,尔后 Leader 再向 Cold 和 Cnew 同步一条只蕴含 Cnew 的日志,尔后日志只须要 Cnew 的多数派确认。Cnew 日志只须要在 Cnew 达成多数派即可提交,此时成员变更实现,不在 Cnew 中的成员主动下线。
成员变更过程中如果产生 Failover,老 Leader 宕机,Cold,new 中任意一个节点都可能成为新 Leader,如果新 Leader 上没有 Cold,new 日志,则持续应用 Cold,Follower 上如果有 Cold,new 日志会被新 Leader 截断,回退到 Cold,成员变更失败;如果新 Leader 上有 Cold,new 日志,则持续将未实现的成员变更流程走完。
Joint Consensus 成员变更比拟通用且容易了解,然而实现比较复杂,之所以分为两个阶段,是因为对 与 的关系没有做任何假如,为了防止 和 各自造成不相交的多数派而选出两个 Leader,才引入了两阶段计划。
如果加强成员变更的限度,假如 Cold 与 Cnew 任意的多数派交加不为空,Cold 与 Cnew 就无奈各自造成多数派,则成员变更就能够简化为一阶段。
2 单步成员变更
实现单步的成员变更,关键在于限度 Cold 与 Cnew,使之任意的多数派交加不为空。办法就是每次成员变更只容许减少或删除一个成员。
图 3 减少或删除一个成员
减少或删除一个成员时的情景,如图 3 所示,能够从数学上严格证实,只有每次只容许减少或删除一个成员,Cold 与 Cnew 不可能造成两个不相交的多数派。因而只有每次只减少或删除一个成员,从 Cold 可间接切换到 Cnew,无需过渡成员配置,实现单步成员变更。
单步成员变更一次只能变更一个成员,如果须要变更多个成员,能够通过执行屡次单步成员变更来实现。
单步成员变更实践尽管简略,但却埋了很多坑,理论用起来并不是那么简略。
三 Raft 单步成员变更的问题
Raft 单步成员变更的问题,最驰名的莫过于 Raft 驰名的正确性问题,另外单步成员变更还有潜在的可用性问题。
1 Raft 单步成员变更的正确性问题
Raft 单步变更过程中如果产生 Leader 切换会呈现正确性问题,可能导致曾经提交的日志又被笼罩。Raft 作者(Diego Ongaro)早在 2015 年就发现了这个问题,并且在 Raft-dev 具体的阐明了这个问题[1]。
上面是一个 Raft 单步变更出问题的例子, 初始成员配置是 abcd 这 4 节点,节点 u 和 V 要退出集群, 如果两头呈现 Leader 切换, 就会失落已提交的日志:
图 4 Raft 单步成员变更的正确性问题
- t0:节点 abcd 的成员配置为 C0;
- t1:节点 abcd 在 Term 0 选出 a 为 Leader,b 和 c 为 Follower;
- t2:节点 a 同步成员变更日志 Cu,只同步到 a 和 u,未胜利提交;
- t3:节点 a 宕机;
- t4:节点 d 在 Term 1 被选为 Leader,b 和 c 为 Follower;
- t5:节点 d 同步成员变更日志 Cv,同步到 c、d、V,胜利提交;
- t6:节点 d 同步一般日志 E,同步到 c、d、V,胜利提交;
- t7:节点 d 宕机;
- t8:节点 a 在 Term 2 从新选为 Leader,u 和 b 为 Follower;
- t9:节点 a 同步本地的日志 Cu 给所有人,造成已提交的 Cv 和 E 失落。
为什么会呈现这样的问题呢?根本原因是上一任 Leader 的成员变更日志还没有同步到多数派就宕机了,新 Leader 一上任就进行成员变更,应用新的成员配置提交日志,之前上一任 Leader 从新上任之后可能造成另外一个多数派汇合,产生脑裂,将已提交的日志笼罩,造成数据失落。
Raft 作者在发现这个问题之后,也给出了修复办法。修复办法很简略, 跟 Raft 的日志 Commit 条件相似:新任 Leader 必须在以后 Term 提交一条日志之后,才容许同步成员变更日志。也即 Leader 在以后 Term 还未提交日志之前,不容许同步成员变更日志。
依照这个修复办法,最简略的实现就是 Leader 上任后先提交一条 no-op 日志,而后再同步成员变更日志。这条 no-op 日志能够保障跟上一任 Leader 未提交的成员变更日志至多有一个节点交加,这样能够发现上一任 Leader 的日志是旧的,从而阻止上一任 Leader 从新选为 Leader,进而阻止了脑裂的产生。
对应下面这个例子,就是 L1 入选 Leader 后必须先提交一条 no-op 日志,而后能力开始同步 Cv 和 E,以便能发现 L2 的日志是旧的,从而阻止 L2 入选 Leader。
另一种办法是应用 Joint Consensus 成员变更,没有这样的正确性问题。
2 Raft 单步成员变更的可用性问题
单步成员变更每次只能减少或者缩小一个成员,在做成员替换的时候须要分两次变更,第一次变更先将新成员退出进来,第二次变更再将老成员删除,两头如果如果网络分区,有可能会导致服务不可用。
思考 a、b、c 三个成员部署在三个机房,当初因为 a 产生故障要将 a 替换为同机房的 d。依照单步成员变更,abc 要先变为 abcd,再变为 bcd。
两头经验的 4 节点 abcd 的状态, 有可能在呈现二分的网络分区 (ad|bc) 时导致整个集群不可用。因为 a 与 d 位于同一机房,这种二分网络分区的状况在理论状况中还是不容忽视的。
怎么解决这个问题呢?一种办法是做成员替换的时候,先删除老成员,再退出新成员,即 abc 先变为 bc,再变为 bcd,这样能够防止 abcd 的状态。
另一种办法是应用 Joint Consensus 成员变更,abc 先变为 abc U bcd,再变为 bcd,也不会经验 abcd 的状态。
四 Raft 成员变更的工程实际
Raft 成员变更的实践虽简略,但理论工程实现上还是有很多中央要思考。因为 Raft 单步成员变更有正确性问题及可用性问题,工程上倡议尽量应用 Joint Consensus 成员变更,这里次要探讨一些 Joint Consensus 成员变更工程实现上必须思考的问题。
1 新成员先退出再同步数据还是先同步数据再退出
因为 Raft 须要严格保障程序,而新成员上还没有任何数据,因而新成员退出集群后须要先同步数据能力失常工作。工程实现时就有两种抉择,一种是让新成员先退出再同步数据,另一种是先给新成员同步数据,同步实现后再退出。这两种形式各有利弊。
表 1 新成员先退出再同步数据和先同步数据再退出的优缺点
新成员先退出再同步数据,成员变更能够立刻实现,并且因为只有大多数成员批准即可退出,甚至能够退出还不存在的成员,退出后再缓缓同步数据。但在数据同步实现之前新成员无奈服务,但新成员的退出可能让多数派汇合增大,而新成员临时又无奈服务,此时如果有成员产生 Failover,很可能导致无奈满足少数成员存活的条件,让服务不可用。因而新成员先退出再同步数据,简化了成员变更,但可能升高服务的可用性。
新成员先同步数据再退出,成员变更须要后盾异步进行,先将新成员作为 Learner 角色退出,只能同步数据,不具备投票权,不会减少多数派汇合,等数据同步实现后再让新成员正式退出,正式退出后可立刻开始工作,不影响服务可用性。因而新成员先同步数据再退出,不影响服务的可用性,但成员变更流程简单,并且因为要先给新成员同步数据,不能退出还不存在的成员。
2 成员变更日志应用什么配置
成员变更日志自身是为了扭转成员配置,处在成员配置变更的临界点上,因而成员变更日志应用什么配置就很要害。
表 2 Joint Consensus 成员变更日志应用的成员配置
对于 Joint Consensus 成员变更,成员变更日志应用什么配置是确定的。Cold,new 日志应用联结统一成员配置 Cold,new,须要老成员配置 Cold 和新成员配置 Cnew 两个多数派确认能力提交,Cnew 日志应用新成员配置 Cnew,只须要新成员配置 Cnew 的多数派确认即可提交,但 Cnew 日志也会同步给老成员配置 Cold,次要是为了让 Cold 中不在 Cnew 中的成员主动退出。
3 成员变更日志什么时候失效
成员变更通过成员变更日志来实现,让各成员对成员配置达成统一,但成员变更日志与一般日志不同,并不一定要等到提交后 Apply 失效。
表 3 成员变更日志的失效机会
对于 Joint Consensus 成员变更,成员变更日志什么时候失效是确定的。在 Leader 上开始同步成员变更日志之前就须要失效,在 Follower 上成员变更日志长久化实现后就须要失效。成员变更日志还未提交就先失效了,因而在 Leader 切换后可能会回滚。
4 成员变更期间日志是否须要严格按序提交
思考这样一种状况,成员变更缩小了成员数量,进而减小了多数派汇合,而更小的多数派更容易达成,造成成员变更之后的日志比之前的日志先达成多数派。
依照 Raft 论文中的 commitIndex 的推动算法:
If there exists an N such that N > commitIndex, a majority of matchIndex[i] ≥ N, and log[N].term == currentTerm:
set commitIndex = N
一条日志达成多数派就往前推动 commitIndex 至该日志,如果该日志之前有日志依照老成员配置还未达成多数派,也一并提交了。
这种状况是否会出问题呢?实际上并不会,因为成员变更之后,曾经有日志应用新成员配置提交了,不在新成员配置中的节点不可能再入选 Leader 了,进而不会笼罩之前的日志,因而就算之前的日志依照老成员配置未达成多数派也能够平安的提交。
hashicorp raft 的实现还是严格按序提交的,即只有后面的日志都达成多数派之后能力提交。
5 只有多数成员存活时怎么复原服务
Raft 只能在大多数成员存活的状况下能力失常工作,理论可能会遇到只有多数成员存活的状况,这个时候要怎么复原服务 呢。
因为只有多数成员存活,曾经不能达成多数派,不能写入数据,也不能做失常的成员变更。须要提供一个强制更改成员配置的接口,通过它设置每个成员的成员配置列表,便于从大多数成员故障中复原。
比方只剩一个成员 S1 存活的时候,强制更改成员配置设置成员列表为{S1},这样造成一个只有 S1 的成员列表,让 S1 持续提供读写服务,后续再调度其余节点通过成员变更退出。通过强制批改成员列表,能够实现最大可用模式。
五 单步成员变更的工程实际
单步成员变更尽管不举荐在工程中应用,这里还是总结一下单步成员变更的一些工程实际,供钻研探讨。
1 单步成员变更日志应用什么配置
对于单步成员变更,成员变更日志是应用新成员配置 还是老成员配置 Cnew 呢?实际上单步成员变更日志无论应用新成员配置 Cold 还是老成员配置 Cnew 都不会毁坏 Cold 与 Cnew 的多数派至多有一个节点相交,因而单步成员变更日志既能够应用新成员配置 Cold 也能够应用老成员配置 Cnew,两种形式各有利弊。
表 4 单步成员变更日志应用老成员配置和应用新成员配置的优缺点
单步成员变更日志应用老成员配置 Cold,能够防止单步成员变更的正确性问题,因而能够省略掉 Leader 上任后的 no-op 日志,同时在减少成员时可能只须要更小的多数派汇合,但在缩小成员时可能须要更大的多数派汇合。
单步成员变更日志应用新成员配置 Cnew,须要 Leader 上任后先提交一条 no-op 日志,以防止单步成员变更的正确性问题,同时在缩小成员时可能只须要更小的多数派汇合,但在减少成员时可能须要更大的多数派汇合。
单步成员变更日志不论应用新成员配置还是老成员配置,最好都同步给新老成员配置中的所有成员,这样在减少成员时能够让新成员迟早收到告诉,在缩小成员时也能够让被删除的成员收到告诉而主动退出。
Raft 论文中单步成员变更日志应用新成员配置 Cnew,etcd 中单步成员变更日志应用老成员配置 Cold。
2 单步成员变更日志什么时候失效
表 5 单步成员变更日志的失效机会
对于单步成员变更,如果成员变更日志应用新成员配置,则与 Joint Consensus 成员变更一样,Leader 上开始同步成员变更日志之前就须要失效,在 Follower 上成员变更日志长久化实现后就须要失效。如果成员变更日志应用老成员配置,实践上只须要在下一次成员变更开始之前失效即可,但理论为了让新退出的节点尽快开始服务,个别在成员变更日志提交后就失效。
Raft 论文中单步成员变更日志应用新成员配置 Cnew,本地长久化实现就失效;etcd 中单步成员变更日志应用老成员配置 Cold,提交后再失效。
六 总结
Raft 提供了 Joint Consensus 成员变更和单步成员变更,极大的推动了成员变更在工程中的利用。本文总结了一些 Raft 单步成员变更的问题,以及成员变更的工程实际。Joint Consensus 通用并且不容易踩坑,一阶段成员变更坑比拟多。工程上倡议尽量应用 Joint Consensus 成员变更。
原文链接
本文为阿里云原创内容,未经容许不得转载。