跳转到内容

调优集群故障切换时间

Aeron® Cluster 的 leader 故障切换时间完全可调 —— 而默认值很慢。开箱配置从一次 leader 丢失中恢复要 花 约 11 秒,其中几乎全耗在一个超时上。你可以把它压到 亚秒级,但这根旋钮是双刃的:拧得太紧, 一个只是短暂沉默的 健康 leader 就会被无缘无故投票下台。本页就讲怎么权衡这笔账。

字节级的选举内部机制 —— term、canvass、投票记录 —— 见 The Aeron Files。 本页是运维视角:该拧什么、能换来什么、又要冒什么险。它与 Leader 放置与优雅让位(新 leader 落在 哪里)互为搭档 —— 本页讲的是它 多快 能到位。

“故障切换时间”到底由什么构成

Section titled ““故障切换时间”到底由什么构成”

故障切换 = 检测(察觉 leader 不在了)+ 选举(投出新 leader)+ 客户端重连(客户端把 ingress 重新指向新 leader)。从 kill 那一刻算起:

检测与选举 串行 进行,所以下限大致是 heartbeat.timeout + election.timeout + relink

属性默认值作用说明
aeron.cluster.leader.heartbeat.timeout10,000 ms检测 —— follower 在收到 leader 最后一次心跳后,等多久才宣告它已死占开箱故障切换时间的约 90%;最大的一根杠杆
aeron.cluster.leader.heartbeat.interval200 msleader 多久发一次心跳必须远小于 timeout(一个窗口内最好有 ≥3–5 拍)
aeron.cluster.election.timeout1,000 ms检测之后的投票/选举阶段在检测 之后 串行进行
aeron.cluster.election.status.interval100 ms选举轮次节奏
aeron.cluster.session.timeout10,000 ms客户端 会话存活与 leader 故障切换无关;决定集群多久驱逐一个沉默的客户端
aeron.cluster.startup.canvass.timeout60,000 ms冷启动 leader canvass与故障切换无关(仅启动时)

默认情况下这些故障切换旋钮一个都没设,所以开箱的 10 s 检测主宰了一切

  • 从真实 leader 丢失中恢复更快。 最直接的收益:我们的测试里从 ~11 s → ~0.85 s(约 13×)。真崩溃 之后,数据面被停顿、被反压的时间更短。
  • 客户端可见的中断更短。 整个故障切换窗口内客户端 ingress 都被反压,窗口越短,吞吐缺口越小,恢复 时要排空的积压也越少。
  • SLA 更紧、更可预期。 如果你必须承诺”在 N 秒内恢复”,调低超时是缩小 N 的唯一办法。
  • 更早释放被占住的资源。 绑在死 leader 上的订阅、会话状态、流控窗口能更快清干净。

heartbeat.timeout 是一个 dead-man’s switch。只要一个 健康 的 leader 沉默超过窗口 —— 无论出于 什么 原因 —— follower 就会宣告它已死并跑一次 真实的选举:一次货真价实的 ~0.85–1.5 s 停顿外加 客户端反压,纯属自找,其实压根没出故障。窗口越紧,它兜住的良性停顿就越多。

会在健康 leader 上触发过紧窗口的停顿来源:

停顿来源典型时长缓解手段
GC 暂停ZGC 亚毫秒;G1/Parallel 或大堆 → 100 ms–数秒低暂停 GC(ZGC)
CPU 调度 / 抢占若共享核则 ms–数百 msisolcpus + nohz_full 绑核
Archive fsync / 磁盘卡顿网络存储上 10–数百 msarchive 放在快速本地存储 / tmpfs
网络抖动 / 心跳丢包µs–ms同 AZ 放置组(低 RTT、无丢包)
VM steal / 吵闹邻居ms–数百 ms裸金属(无 hypervisor steal)
Safepoint / JIT / THP 整理ms–数百 ms(残余尾部风险)
  • interval 与 timeout 之比。 heartbeat.interval 必须远小于 heartbeat.timeout(一个窗口内争取 ≥3–5 拍)。只调低 timeout 却不调 interval,单个 延迟或丢失的包就会触发一次误切换。这是最常见的 误配。
  • 选举反复。 election.timeout 太短时,一次本该多花一点时间的正当选举(canvass、日志追赶)可能自己 就超时重启 → 选举 来回打转,反而让有效故障切换 更长 甚至不收敛 —— 与目标背道而驰。
  • 负载下的 leader 抖动。 高负载下过紧的超时会让 leader 在节点间来回弹跳,每弹一次都逼着客户端重连 并换一个 term。一个反复抖动的集群,可用性可能 不如 一个慢但稳的故障切换。
  • 短 soak 带来的虚假信心。 一次几分钟、零抖动的运行,并不能证明它能连续几天安然无恙 —— 生产环境会 叠加起伏的负载、长时运行才冒头的尾部 GC 事件、以及突发流量。信心需要一次 长时 soak(数十分钟到 数小时),而非一次短促的基准。

调低超时,是把一种失败模式(恢复慢)换成另一种(自找的停顿)。它并 不会 引入正确性隐患:

  • 不丢数据。 Aeron Cluster 的提交基于 quorum(Raft);已提交的数据无论时序如何都能挺过故障切换。见 Cluster 与 Raft 概览
  • 不会脑裂。 新 leader 需要 多数 票,所以一次误选举不可能产生两个 leader。误切换造成的损害纯粹 是一次 可用性抖动 —— 绝不会是数据损坏或双写者。

在一台裸金属机群(Granite Rapids,SNC-3)上实测,800K tps、128B 消息、MTU 8192,采用”负载下 kill leader”的测试和一个 具备故障切换感知的客户端(实现了 EgressListener.onNewLeader 并调用 pollEgress())。基于 Aeron 1.48.11 验证;属性名与默认值通过反射运行中的 jar 确认。

开箱超时 → ~11 s,以及那个客户端陷阱

Section titled “开箱超时 → ~11 s,以及那个客户端陷阱”

用开箱超时时,选举本身是健康的 —— 节点被 kill → 选出新 leader,0 NAK / 0 重传 / 0 错误,幸存 的 follower 什么都没丢(它在 kill 时已完整复制,故晋升无需重新同步;新 leader 记录了 2 个良性的追赶 NAK)。

实测真实故障切换 ≈ 11.0 s —— 三个独立信号相互吻合(新 leader 的 raft-log 发送位置、客户端字节恢复、 客户端 NEWLEADER 事件)。这正好是开箱的 10 s 心跳超时 + ~1 s 选举,与模型预测完全一致。

激进超时 → ~0.85 s,且不设下限

Section titled “激进超时 → ~0.85 s,且不设下限”

heartbeat.timeout=250msheartbeat.interval=50mselection.timeout=300mselection.status.interval=25ms —— Aeron 原样接受了每一个值;不存在强制下限。

试次检测(ms)选举(ms)客户端重连(ms)总计(ms)误切换
13376377648090
23386958088330
33398059029470
中位数3386958088330
  • 稳定在 ~0.85 s —— 比开箱快约 13×,所有客户端都完整恢复。
  • 没能进到 500 ms 以内,这是结构性的: 检测(~338 ms ≈ heartbeat.timeout)与 election.timeout (300 ms)串行 进行 → 在重连之前就已耗掉 ~640 ms 才选出 leader。要压到 500 ms,两项都得缩,而这 会侵蚀抖动余量。
  • 误切换 = 0,横跨约 28,000 个稳态采样 —— election_state=CLOSED,整个 kill 前窗口角色保持不变。 250 ms / 50 ms 在这台机器上不抖动(低暂停 GC、isolcpus、快速本地 archive、同 AZ)—— 这恰恰印证了 上面的告诫:它在 这里 安全。

随着你把超时拧紧,两项代价的变化速率截然不同:

  • 恢复速度的收益在 ~2 s 以下几乎持平 —— leader 故障本就 罕见,1.5 s 与 0.85 s 用户很少能感知到 差别。
  • 误切换风险随拧紧而陡增 —— 这是一份 持续 的敞口(每一次 GC 暂停、safepoint、丢包延迟,每天每一 秒都在)。粗略地说,超时减半,兜住良性停顿而误触发选举的概率就翻倍。

从 2 s → 0.85 s 的边际收益很小,边际风险却很大。这就是 曲线的拐点

档位超时故障切换抖动余量(可容忍的 leader 停顿)何时用
保守开箱(10 s / 1 s)~11 s~10 s(从不抖动)你从不 kill leader / 批处理系统
均衡(推荐)hb 1000 / int 200 / elec 500 / stat 50~1.5 s~1 s —— 覆盖几乎所有 GC/safepoint/调度尾部;对非理想环境也稳健多数生产部署
激进(已测)hb 250 / int 50 / elec 300 / stat 25~0.85 s~250 ms —— 单次 G1 暂停 / THP 整理就可能超过硬性亚秒 SLA、理想机器、且做过 soak 之后

推荐做法:

  1. 默认选均衡的 ~1–2 s 档heartbeat.timeout=1000msinterval=200mselection.timeout=500mselection.status.interval=50ms)。它比开箱快约 7×,保留了激进档约 4× 的抖动余量,即便环境偏离理想也 仍然安全。
  2. 保持 interval ≪ timeout(一个窗口 ≥3–5 拍)。绝不要只调低 timeout。
  3. 只在有硬性 SLA 时才进亚秒级,并且只在一次 数十分钟到数小时的 soak(贴近生产的负载 以及 堆/GC、并统计误切换次数)之后。安全下限由你最坏的真实健康-leader 停顿决定,而非你希望的那个数字。
  4. session.timeout 是独立的 —— 按”集群该多快驱逐一个沉默客户端”来调它,与 leader 故障切换无关。
  5. 上一个具备故障切换感知的客户端pollEgress() + EgressListener)—— 没有它,任何故障切换时间 都毫无意义。

在生产中调低故障切换时间前的检查清单

Section titled “在生产中调低故障切换时间前的检查清单”
  • 用的哪个 GC,它在负载下的最坏暂停是多少?(ZGC ≪ G1/Parallel。)这决定了你的下限。
  • 热线程是否绑核(isolcpus/nohz_full),还是在抢核?
  • archive 往哪儿 fsync —— tmpfs、本地 NVMe、还是网络存储?(磁盘卡顿会触发检测。)
  • 同 AZ / 放置组,还是跨 AZ?(网络抖动会拉宽所需余量。)
  • 裸金属还是 VM?(VM steal 会叠加尾部延迟。)
  • interval 是否 ≤ timeout 的约 1/5?
  • 是否在生产级负载 以及堆/GC 下 soak 足够久,看到了尾部(≥ 数十分钟)?
  • ingress 客户端是否处理 onNewLeader 并轮询 egress?
  • 这个目标是被真实 SLA 撑起来的,还是 1–2 s 其实就已经够好了?

延伸阅读:Leader 放置与优雅让位Cluster Standby 与多 AZ HA 设计、以及 故障处置手册