介绍
在Raft算法里,成员变更这一环节,不仅理解起来颇具难度,而且至关重要,它还是Raft算法中唯一经历过优化与改进的部分。早期,实现成员变更运用的是联合共识(Joint Consensus),然而该方法在实际落地时困难重重。于是,Raft算法的作者经过研究探索,提出了一种优化后的新方法——单节点变更(single-server changes) 。
配置
是集群各节点地址信息的集合。比如节点 A、B、C 组成的集群,那么集群的配置就是[A, B, C]集合。
成员变更
在我看来,集群进行成员变更时,最大的风险在于可能同时出现两个领导者。以一个具体场景为例,在执行成员变更操作期间,节点A、B和C之间发生了分区错误。在这种情况下,节点A、B构成了旧配置中的多数派,也就是变更前3节点集群里的多数节点,此时原领导者(节点A)会继续保持领导地位。
而从新配置的角度看,节点C与新加入的节点D、E组成了新配置下的多数派,即变更后的5节点集群中的多数节点,这组节点很可能选举出一个新的领导者(例如节点C)。如此一来,就会出现同一时刻存在两个领导者的局面,这对集群的稳定运行会造成极大的负面影响 。
如果出现两个领导者,这显然违背了“领导者唯一性”原则,必然会对集群的稳定运行产生负面影响。那么,该如何解决这个棘手的问题呢?
我们知道,在启动集群时,配置固定且不存在成员变更的情况下,Raft的领导者选举机制能够确保仅有一个领导者,不会出现多个领导者并存的状况。基于此,有人可能会想,能不能先关闭原集群,再启动新集群呢?比如,先关闭由节点A、B、C组成的集群,然后启动由节点A、B、C、D、E组成的新集群。
但很遗憾,这个方法并不可行。原因在于,每次成员变更都要重启集群,这就意味着在集群变更期间,服务会处于不可用状态,这无疑会严重影响用户体验。不妨设想一下,当你正全神贯注地玩王者荣耀时,屏幕上却时不时弹出一个对话框,提示“系统升级,游戏暂停3分钟”,这样的体验是不是糟糕透顶?
既然这种方法因为严重影响用户体验而根本行不通,那么究竟该如何解决成员变更的问题呢?实际上,最常用的方法是单节点变更。
单节点变更
单节点变更,就是通过一次变更一个节点实现成员变更。如果需要变更多个节点,那你需要执行多次单节点变更。比如将 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 日志项应用后,再执行成员变更请求。可参考 Hashicorp Raft 的源码,也就是 runLeader() 函数中:
noop := &logFuture{
log: Log{
Type: LogNoop,
},
}
r.dispatchLogs([]*logFuture{noop})
小结
- 成员变更过程中存在的主要问题是,当进行成员变更操作时,有可能在新旧配置中分别形成两个“大多数”的情况。这种情况会致使集群内同时出现两个领导者,从而破坏了Raft算法中领导者唯一性的基本原则,进而对集群的稳定运行产生不良影响。
- 单节点变更方法巧妙地利用了“每次仅变更一个节点,能够避免旧配置和新配置中同时出现两个‘大多数’”这一特性,以此来顺利实现集群的成员变更操作。
- 由于联合共识的实现方式较为复杂,在实际应用中存在较大的实施难度,因此在绝大多数基于Raft算法的实现案例中,例如Etcd、Hashicorp Raft等,都普遍采用了单节点变更的方法。值得一提的是,Hashicorp Raft中关于单节点变更的实现方案,是由Raft算法的创作者迭戈·安加罗(Diego Ongaro)亲自设计的,这一方案具有很高的技术参考价值,为相关领域的研究和实践提供了重要的借鉴。
Raft并非普通的一致性算法,而是一种共识算法,更确切地说,它是Multi-Paxos算法家族的一员,主要解决的是如何就一系列值达成共识的问题。Raft算法的一大优势在于,它能够容忍少数节点出现故障,即便部分节点失效,整个系统仍能维持运转。虽然Raft算法理论上可以实现强一致性,也就是线性一致性(Linearizability),但这需要客户端协议的协同配合才能达成。在实际应用场景中,由于不同场景的特点各异,往往需要在一致性强度与实现复杂度之间进行权衡取舍。以Consul为例,它就实现了三种不同的一致性模型。
- default:客户端向领导者节点发起读操作请求,当领导者确认自身处于稳定状态(即在leader leasing时间范围内)时,会将本地数据返回给客户端;若不处于稳定状态,则返回错误信息。在这种模式下,客户端存在读到旧数据的可能性。例如,当网络分区错误发生时,新领导者可能已经完成数据更新,但由于网络故障,旧领导者未能及时更新数据,也未退位,仍被判定为处于稳定状态,此时客户端访问旧领导者节点就可能获取到旧数据。
- consistent:客户端同样向领导者节点执行读操作,不过领导者需要先与大多数节点确认自己的领导地位,确认无误后才会将本地数据返回给客户端;若无法确认,则返回错误。采用这种方式,客户端读取到的始终是最新数据。
- stale:客户端读取数据的节点不局限于领导者,可从任意节点读取,因此有可能读到旧数据。
在实际工程实践中,Consul的consistent一致性模型通常就能满足大多数场景的需求,并不一定非要追求线性一致性。只要能够确保在写操作完成后,每次读取都能获取到最新值即可。例如,在实现幂等操作时,我们可以使用一个唯一编号(ID)来标识一个操作,并借助一个状态字段(nil/done)来标记操作是否已执行。如此一来,只要保证设置ID对应的状态值为done后,能即时且持续读取到最新状态值,就能防止操作重复执行,从而实现幂等性。总体而言,Raft算法在处理绝大多数场景的一致性问题时表现出色。所以,当你着手设计分布式系统时,我建议优先考虑采用Raft算法。只有当Raft算法确实无法满足现有场景的特殊需求时,再去深入调研其他共识算法。
文章来源: https://study.disign.me/article/202509/2.raft-membership-change.md
发布时间: 2025-02-24
作者: 技术书栈编辑