Raft 协议中的成员变更是实现动态集群管理的关键部分。这个过程相较于日志复制、Leader 选举更加复杂,因为它必须保证安全性(避免脑裂)和一致性(日志不丢失)
那么一般啥时候会进行成员变更的呢?一般的场景如下:
- 节点故障宕机 → 移除
- 容量不足 → 扩容
- 节点迁移/替换硬件 → 替换节点
- 滚动升级 → 临时加入新节点
值得一提的是:raft 并不能直接应用节点列表。因为 Raft 要 通过多数派达成共识。
如果各节点对 “谁是才是集群成员” 理解不一致,这就无法达成共识了,进而可能产生脑裂或数据丢失。比如原集群 {A, B, C} 改为 {C, D, E},假如 A 认为配置没变,而 D 认为已经变了,两边都可能以为自己选出的 Leader 是合法的,从而出现 双 Leader(脑裂)情况
联合共识
raft 通过联合共识方式来解决集群成员变更的情况。其核心思想是:在变更过程中,旧配置和新配置是同时存在的。只有旧 + 新两个配置内的成员都确认操作,才能算达成共识。这样在过渡期间,即使某些节点 “只知道旧配置”,另一些节点 “只知道新配置” ,仍能正确识别合法的 Leader,不会发生脑裂。联合共识阶段就是为了确保:只有原集群中的节点才能控制领导权转移,防止新增节点“悄咪咪”摸上位,造成脑裂。
换句话说:在联合共识状态下,一个候选人要想赢得选票,它必须同时获得 ‘旧配置多数派’ 和 ‘新配置多数派’ 的支持
成员变更流程
我们以一个 raft 集群扩容为例,现有的集群成员为 {A, B, C},此时我们需要添加一个新节点 {D} 以扩容集群,最终我们期待的集群成员应该是 {A, B, C, D}
一阶段(初始配置)
此时触发新节点 join 集群的流程
old_members: {A, B, C}
二阶段(联合共识)
进入联合共识阶段,Leader A 写入一条特殊日志:
new_members: {A, B, C, D}
joint_config: true
此时的配置为联合配置,要求:
- 复制日志时:必须被
{A, B, C}和{A, B, C, D}各自的多数派确认 - 投票选 Leader 时:也要新旧配置下的 quorum 都同意
- 日志复制、ReadIndex 都要满足 “双多数”
三阶段(应用联合配置)
当这条日志(即二阶段的日志)被提交后,等到旧配置的 quorum 和新配置的 quorum 都复制了这条日志,系统就进入 “联合共识阶段”
此时所有节点的行为都是:
- 接受来自旧配置成员+新配置成员的投票
- 日志复制也按联合配置进行
集群内的所有成员都知道了变更正在进行
四阶段(提交)
Leader 再写入一条日志,正式切换为新成员配置!
final_members: {A, B, C, D}
joint_config: false
- 这条日志也必须被新配置的多数派提交
- 成功提交后,系统退出联合配置,配置正式生效
现在我们分析一下,在联合共识下触发选举时, A, B, C, D 有哪些节点能成为 Leader,哪些节点不能成为 Leader:
A, B, C 各个节点都有可能当选为 Leader,只要它能获得:
- 从
{a, b, c}中拿到 2 票 - 从
{a, b, c, d}中拿到 3 票
D 一开始一定无法当选为 Leader,这是因为:旧配置中的节点压根不会认可 D(换句话说是不认识 D),甚至可能还没收到新配置日志,就不会给 d 投票
我们可以看到在 old_members={A, B, C} 中没有 D,所以它不可能获得旧配置的 “多数票”,因为旧配置的节点不会给一个“陌生人”投票。除非所有旧节点都已经接受提交了“成员变更日志”,并且已经更新的配置包含了 D,此时才能给 D 投票。但是!联合共识阶段本身就还没完全切换到新配置,所以 D 暂时不能胜任 Leader
此时我们可以总结一下联合共识的必要性:联合共识可让所有节点都记录下 “正在变更” 的状态(即写入联合配置),以保证新旧节点都能参与日志复制,不会出现配置分裂的情况。在这过程中 Leader 宕机也没关系,变更是通过日志推进的,新的 Leader 会从日志中恢复当前配置状态(包括是否处于联合配置阶段)
临时 Leader
TODO…
分区脑裂
TODO…