跳转到内容

Leader 放置与首选 Leader 控制

在 Raft 集群中,leader 承载每一次写入,因此 leader 位于何处 直接决定延迟。节点跨可用区部署时,位于 远端 AZ 的 leader 会给面向客户端的热路径 —— 客户端 ingress → 共识 → egress 全部经过 leader —— 额外增加 一次跨 AZ 往返。本页要解决的正是:如何让 leader 留在你指定的可用区,以及故障切换后如何把它迁回,同时 放弃 Aeron 原本的故障切换能力。

Aeron Cluster 没有 transferLeadershipTo,也 没有 选举优先级 —— 但只要对选举施加偏向、再触发 一次优雅让位,就能实现一套生产可用的首选 leader 方案。这套方案及其安全性论证,就是本页的主题。

至于字节级的选举内部机制 —— term、canvass、投票记录、以及在链路上承载 end-of-stream 的帧标志 —— 请参阅 The Aeron Files。本页只谈运维视角:你有哪些控制手段、如何安全地补齐缺失 的那一环、以及为此付出的代价。

真实部署的热路径上通常运行 两个 集群化服务 —— 一个 OMS 集群 和一个 撮合引擎(ME)集群 —— 一笔订单沿 OMS leader → ME leader 流转。跨集群流量全部是 leader 之间的通信,因此这一跳的延迟取决于 两个 leader 是否处在同一可用区。在最低延迟的配置下 两个 leader 同处一个 AZ,于是 OMS→ME 这一跳留 在 AZ 内,p50/p99 都能保持紧凑。

现在 ME leader 发生故障。Raft 会从幸存的 ME 成员中选出新的 ME leader —— 这场选举由协议决定(超时与投票), 而非 由可用区决定。新 ME leader 可能落在 与 OMS leader 不同的 AZ,从那一刻起,每笔订单都要在 OMS→ME 这一跳 上跨越一次 AZ 边界 —— 即便 OMS leader 自身从未移动。

这正是隐患所在:仅仅一次 ME 故障切换,在 OMS leader 纹丝未动的情况下,就悄悄把最繁忙的跨服务调用变成了 一次跨 AZ 往返。表面上一切正常 —— 集群健康,也没有数据丢失 —— 但每笔订单从此都要为这次跨 AZ 往返付出 延迟代价,直到你把 ME leader 迁回为止。

Leader 归属:Aeron Cluster vs SOFAJRaft

Section titled “Leader 归属:Aeron Cluster vs SOFAJRaft”

两种实现在”运维方能对 leadership 施加多少 原生 控制”上有所不同。

能力SOFAJRaftAeron Cluster
显式转移 leadership有 —— Node.transferLeadershipTo(PeerId) 通过 TimeoutNowRequest 把 leadership 交给指定 peer无原生转移 API
选举优先级 / 偏好有 —— 每节点 ElectionPriority,越高越优先,0 = “永不当 leader”无原生优先级 —— 但可实现,通过注入一个有偏向的 Context.random()(见下文)
把 leadership 钉在某 AZ/节点优先级 + 转移有偏向的选举 + 一个让位触发器(本页)
按需触发选举transferLeadershipTo 触发一次定向选举优雅关闭(或 resign)leader 会触发一次老 leader 赢不了的即时选举

“首选 leader 且保留真实故障切换” —— 让 leadership 偏向某个选定节点, 若该节点确实已死,集群 仍要正常故障切换,并且 该节点恢复后还能把 leadership 迁回 —— 可以拆解为两个相互独立的问题:

  • 第一部分 —— 决定选举中 谁获胜 零核心改动即可解决。
  • 第二部分 —— 在现任 leader 仍然健康时 触发 一次交接。 没有纯注入式的手段可用;你要在两个运维 选项之间做取舍。

第一部分 —— 用注入的 Context.random() 偏向获胜者

Section titled “第一部分 —— 用注入的 Context.random() 偏向获胜者”

选举一旦开始,每个已追平的 follower 都会成为候选者,并等待一段 随机 的提名延迟,取自 [0, election-timeout ÷ 2) 的均匀分布 —— 默认 aeron.cluster.election.timeout 为 1 s,即 [0, 500 ms) 的窗口。最先触发的候选者领取新 term 并获胜(投票者会拒绝任何日志落后于己的候选者)。这段 每节点的提名延迟是集群 Random唯一 消费方,而 Context.random(Random) 是一个现成的公开 setter —— 因此你可以注入这样一个 Random:在首选节点上返回 0,在其余节点上返回正常随机值:

// 同一份代码部署到每个节点;各自根据自己的 member id 算出偏向。
final class PreferredLeaderRandom extends java.util.Random {
private final boolean preferred;
PreferredLeaderRandom(final boolean preferred) { this.preferred = preferred; }
@Override public double nextDouble() {
return preferred ? 0.0 : super.nextDouble(); // preferred → 最短提名延迟 → 最先触发
}
}
ctx.random(new PreferredLeaderRandom(ctx.clusterMemberId() == PREFERRED_ID));

第一部分决定 选举开始后由谁获胜。而要把 leadership 从一个 健康 的现任 leader 上移走(例如首选 AZ 恢复后将其收回),就必须有某个动作去 发起 那场选举。Aeron 没有提供受支持的、纯注入式的”让位但继续 服务”调用,因此你要在下面两个选项中择一。

选项 1 —— 外部重启(零核心改动,推荐默认)

Section titled “选项 1 —— 外部重启(零核心改动,推荐默认)”

干净地重启现任 leader 的 consensus-module 进程(SIGTERM 或关闭内嵌应用)。干净关闭会断开 leader 的日志发布,从而在 follower 侧触发 end-of-stream → 一次即时的优雅选举(无需干等 10 s 心跳超时), 离场的 leader 则被排除在快速路径的 unanimity 计数之外,于是幸存者能迅速定夺。随后,第一部分那个有偏向 的 random 会把胜局导向首选节点 —— 前提是它已追平。再重启被关闭的节点,它会以 follower 身份重新 加入。

  • 优点: 一行 Aeron 代码都不用改;沿用久经考验的优雅路径;安全性由既有校验保证;跨版本升级无需维护 任何东西。
  • 缺点: 这是一次 完整的 leader 重启,而非原地让位 —— 动作更重,且老 leader 在重启并重新加入 期间有一段短暂的不可用窗口。还需要一个外部编排者来决定 何时 重启、重启哪个 节点。

选项 2 —— 原地 RESIGN(对 Aeron 核心的最小改动)

Section titled “选项 2 —— 原地 RESIGN(对 Aeron 核心的最小改动)”

Aeron 没有内建的”保持节点存活”的自愿让位,但可以复用既有的优雅机制加一个:新增一个 RESIGN 控制开关,让 leader 在 进程不退出 的情况下进入一次优雅选举(复用同一条 end-of-stream 路径、同样的 gracefulClosedLeaderId 排除、同样的安全校验)。这是一处小而局部的改动 —— 大致是一个新开关值、一个 仅供 leader 调用的 resign 方法、以及一个处理分支 —— 但它 属于对 Aeron 核心的 fork

  • 优点: 真正的原地让位 —— 无需重启进程,不可用窗口极小;由既有的 control-toggle 计数器驱动(与 SUSPEND/SNAPSHOT 一致);同样优雅,也同样受安全校验保护。
  • 缺点: 你从此要维护一个 fork —— 带来升级负担、可能与上游的 toggle 改动冲突,且新路径的正确性 须自行负责。不受上游支持。
如果你需要……就选
不引入 fork;运维可以容忍一次 leader 重启选项 1(外部重启 + random 偏向)
原地让位、不重启;愿意维护一个小 fork选项 2RESIGN 开关)

两个 选项中,第一部分都负责提供获胜者偏向,而 Raft 的安全校验保证落后的首选节点永远选不上。第二 部分真正要做的唯一决定,就是 重启 vs. 小 fork

组合起来:多 AZ 热路径下的 leader 固定

Section titled “组合起来:多 AZ 热路径下的 leader 固定”

这一切的具体动因,是一个 以 leader 为热路径的多 AZ 部署。由此引出两条运维需求:

  1. 固定 —— 正常情况下,leader 应当是选定 AZ 内的那个节点,好让面向客户端的路径留在 AZ 内。
  2. 恢复后收回 —— 若选定 AZ 失效,集群必须故障切换到某个幸存 AZ(可用性优先);待选定 AZ 的节点恢复 健康后,leadership 应迁 它。收回是针对一个完全健康的 leader 的 稳态 操作 —— 这正是为什么 需要一个 触发器(第二部分),仅凭有偏向的 random 无法完成。

下面是一个建议的控制回路(两个第二部分选项均适用):

  • 仅在首选节点已追平时才触发。 在它落后时触发只会浪费一个 term(安全校验会拒绝它,转而由另一个 幸存者赢下过渡期)。触发前,先将目标的 logPosition 与 leader 的 commitPosition 做一次预检。
  • 加入迟滞 / 退避。 绝不能让一个反复抖动的 AZ 引发 leadership 来回易主 —— 每次交接都是一次短暂的热 路径中断。
  • 就”收回”这个场景而言,选项 2 更合适。 收回是一项例行、且可能频繁的操作;用原地让位来做,可以避免 每次都重启一个健康的 leader。选项 1 依然可用且无需 fork,但”为一项例行操作去重启健康 leader”代价更重, 而且每次重启都意味着一整轮重新加入与追赶。

有了第一部分的偏向,“随机赛跑”的图景就变了:

  • 首选节点存活且已追平 → 它确定性获胜。 它抽到的提名延迟为 0,最先触发,并通过每一道安全校验。 这是正常、也是预期的情形。
  • 首选节点宕机或落后 → 交还给普通 Raft。 它通不过候选资格校验,于是由某个幸存、且更新的成员在寻常 的 [0, 500 ms) 赛跑中获胜 —— 也就是真实的故障切换,行为一如既往。
  • 瞄准一个 AZ 比瞄准某个具体节点更容易。 在常见的 2+1 布局中(两个热路径 AZ 的候选者都在、被错放的 leader 位于远端区),任何 一个胜者都能让放置恢复正常,即使不施加偏向也是如此 —— 偏向只是进一步让那个 具体节点的当选变得确定。

若不施加偏向、仅靠反复的优雅让位,在 k 个同样追平的 follower 中,某个具体目标每轮的当选概率约为 1/k (3 节点集群中即为五五开)—— 所以有偏向的 random 正是把”反复重试直到命中”变为”一次即命中”的关键。

已提交数据:不会。 一个条目只有在 quorum(⌊n/2⌋+1)都追加之后才算提交,而投票者会拒绝任何日志落后 于己的候选者 —— 因此任何新 leader 都持有全部已提交条目(这是标准的 Raft 安全性论证)。一次优雅关闭(或 RESIGN)会执行一套有序的收尾 —— 在关闭日志发布之前先设定 commit 位置、并将 archive recording 排空 —— 因此已提交条目在交接前就已持久落盘。参见 Cluster 与 Raft 概览

在途、未提交的数据:可能被丢弃,须由客户端自行处理。 老 leader 已追加但尚未提交的条目,可能在新 term 中被截断。选举期间客户端会话会断开;AeronCluster 客户端会收到 new-leader 事件,并以同一个 session id 重连,但它 不会 自动重发任何内容。请把未确认的请求视为结果未知,并 幂等地重发。运维 上:把收回操作选在流量低谷时段执行,若负载允许,先静默或排空 ingress;否则要确保新 leader 就绪后,客户端 会主动对账那些未确认的订单。

延伸阅读:调优集群故障切换时间(选举 多快 完成)、 Cluster Standby 与多 AZ HA 设计、以及 客户端-集群通信(new-leader 事件与客户端重连)。