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…