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。本页只谈运维视角:你有哪些控制手段、如何安全地补齐缺失 的那一环、以及为此付出的代价。
为什么 leader 放置很重要
Section titled “为什么 leader 放置很重要”真实部署的热路径上通常运行 两个 集群化服务 —— 一个 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 施加多少 原生 控制”上有所不同。
| 能力 | SOFAJRaft | Aeron Cluster |
|---|---|---|
| 显式转移 leadership | 有 —— Node.transferLeadershipTo(PeerId) 通过 TimeoutNowRequest 把 leadership 交给指定 peer | 无原生转移 API |
| 选举优先级 / 偏好 | 有 —— 每节点 ElectionPriority,越高越优先,0 = “永不当 leader” | 无原生优先级 —— 但可实现,通过注入一个有偏向的 Context.random()(见下文) |
| 把 leadership 钉在某 AZ/节点 | 优先级 + 转移 | 有偏向的选举 + 一个让位触发器(本页) |
| 按需触发选举 | transferLeadershipTo 触发一次定向选举 | 优雅关闭(或 resign)leader 会触发一次老 leader 赢不了的即时选举 |
首选 leader 方案:分为两部分
Section titled “首选 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));第二部分 —— 触发交接
Section titled “第二部分 —— 触发交接”第一部分决定 选举开始后由谁获胜。而要把 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 | 选项 2(RESIGN 开关) |
两个 选项中,第一部分都负责提供获胜者偏向,而 Raft 的安全校验保证落后的首选节点永远选不上。第二 部分真正要做的唯一决定,就是 重启 vs. 小 fork。
组合起来:多 AZ 热路径下的 leader 固定
Section titled “组合起来:多 AZ 热路径下的 leader 固定”这一切的具体动因,是一个 以 leader 为热路径的多 AZ 部署。由此引出两条运维需求:
- 固定 —— 正常情况下,leader 应当是选定 AZ 内的那个节点,好让面向客户端的路径留在 AZ 内。
- 恢复后收回 —— 若选定 AZ 失效,集群必须故障切换到某个幸存 AZ(可用性优先);待选定 AZ 的节点恢复
健康后,leadership 应迁 回 它。收回是针对一个完全健康的 leader 的 稳态 操作 —— 这正是为什么
需要一个 触发器(第二部分),仅凭有偏向的
random无法完成。
下面是一个建议的控制回路(两个第二部分选项均适用):
- 仅在首选节点已追平时才触发。 在它落后时触发只会浪费一个 term(安全校验会拒绝它,转而由另一个
幸存者赢下过渡期)。触发前,先将目标的
logPosition与 leader 的commitPosition做一次预检。 - 加入迟滞 / 退避。 绝不能让一个反复抖动的 AZ 引发 leadership 来回易主 —— 每次交接都是一次短暂的热 路径中断。
- 就”收回”这个场景而言,选项 2 更合适。 收回是一项例行、且可能频繁的操作;用原地让位来做,可以避免 每次都重启一个健康的 leader。选项 1 依然可用且无需 fork,但”为一项例行操作去重启健康 leader”代价更重, 而且每次重启都意味着一整轮重新加入与追赶。
结果有多确定?
Section titled “结果有多确定?”有了第一部分的偏向,“随机赛跑”的图景就变了:
- 首选节点存活且已追平 → 它确定性获胜。 它抽到的提名延迟为
0,最先触发,并通过每一道安全校验。 这是正常、也是预期的情形。 - 首选节点宕机或落后 → 交还给普通 Raft。 它通不过候选资格校验,于是由某个幸存、且更新的成员在寻常
的
[0, 500 ms)赛跑中获胜 —— 也就是真实的故障切换,行为一如既往。 - 瞄准一个 AZ 比瞄准某个具体节点更容易。 在常见的 2+1 布局中(两个热路径 AZ 的候选者都在、被错放的 leader 位于远端区),任何 一个胜者都能让放置恢复正常,即使不施加偏向也是如此 —— 偏向只是进一步让那个 具体节点的当选变得确定。
若不施加偏向、仅靠反复的优雅让位,在 k 个同样追平的 follower 中,某个具体目标每轮的当选概率约为 1/k
(3 节点集群中即为五五开)—— 所以有偏向的 random 正是把”反复重试直到命中”变为”一次即命中”的关键。
让位会丢数据吗?
Section titled “让位会丢数据吗?”已提交数据:不会。 一个条目只有在 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 事件与客户端重连)。
本站与 Adaptive Financial Consulting Limited 或 Aeron 项目无任何关联,未获其背书或赞助。 Aeron 是 Adaptive Financial Consulting Limited 的注册商标。
Aeron 是 Adaptive Financial Consulting Limited 在英国及其他国家/地区的商标。