跳转到内容

Aeron® Cluster 快照失败与恢复

一个节点在打快照的过程中挂掉了——kill -9、OOM-killer、断电,或者你自己的 onTakeSnapshot 抛出异常。它现在处于什么状态?你要做什么才能把它拉回来?简短的答案让人安心:一次被打断的快照,在设计上就是「不可见」的。节点会从上一个有效快照加 log 回放重新启动,无需任何人工干预。而更完整的答案——留下了什么、什么时候你确实需要 ClusterTool、以及 Aeron Premium 会带来哪些变化——就是本页要讲的内容。

为什么被打断的快照在设计上是安全的

Section titled “为什么被打断的快照在设计上是安全的”

两条设计事实撑起了全部逻辑。下面运维手册里的一切都由它们推导而来。

1. 一次快照是一组本地 archive recording。 SNAPSHOT 动作由 leader 追加到 Raft log 中,因此每个节点都在相同的 log 位置打快照——但每个节点各自独立地把自己的快照写入本地 archive。你的服务在 onTakeSnapshot 中把状态序列化到一个被录制的 publication;consensus module 则单独录制它自己的状态(会话、定时器)。

2. recording.log 条目只在一切完成之后才写入。 一次快照只有在其条目被追加到该节点的 recording.log 文件后才「存在」——而 consensus module 是在所有 service recording 和它自己的 recording 全部写完之后才追加这些条目的,且它自己的条目写在最后(只有当 aeron.cluster.file.sync.level > 0 时文件才会 fsync——默认值为 0)。恢复计划只会用完整、有效的条目集来构建。正如 Aeron 维护者 Dmytro Vyazelenko 所说:“Snapshot is considered complete only if all services complete their snapshot without an error followed by a successful snapshot of the consensus module state. Partial snapshots are ignored when creating a recovery plan.”(快照只有在所有 service 无错误地完成快照、随后 consensus module 状态也成功打完快照之后,才算完成。构建恢复计划时会忽略部分完成的快照。)

在那次最终追加之前的任何时刻杀掉进程,结果都一样:没有条目、没有快照,重启时恢复计划直接用上一个有效快照(若没有则从头做完整 log 回放)。不存在需要修复的「半个快照」状态。

到底会发生什么,取决于快照是怎么死的。consensus module 区分终止性(terminal)与非终止性错误——而故障的方向(打快照 vs. 加载快照)也极为关键。

场景节点行为快照结果运维动作
进程在打快照途中被杀kill -9、OOM、断电)节点停机recording.log 条目——被打断的快照不可见重启节点(情形 A)
onTakeSnapshot 抛异常(序列化 bug 等)节点继续运行;记录错误Service ack NULL → consensus module 放弃整组快照,无条目修复 bug,重新打快照(情形 B)
打快照期间磁盘写满ArchiveException: STORAGE_SPACE)终止性——agent 终止,节点退出无条目释放/扩容磁盘,重启
非正常停机后 archive 起不来straddling page boundary)启动时崩溃循环最后一次写入被撕裂(通常是断电,而非 SIGKILL)ArchiveTool verify + compact情形 A)
启动时加载快照抛异常终止性——AgentTerminationException,节点退出(若反复重试则崩溃循环)最新条目存在但无法加载invalidate-latest-snapshot情形 C)
本地 cluster/archive 数据丢失或不可恢复节点停机清空重放,或从备份重建(情形 D)

有两个特性值得点出,都是「快照是每节点本地动作」的直接结果:

  • 一个节点的失败不会拖垮其他节点。 Service ack 通过本地 IPC 在一个节点自己的容器和自己的 consensus module 之间流动;快照路径上没有任何东西会把失败传播给其他成员。一个 follower 打快照失败,leader 和另一个 follower 手里仍有完好的快照条目——整个过程中集群照常处理流量,因为重要的只是 quorum。
  • 各节点对「存在哪些快照」出现分歧是合法的。 如果 leader 的快照失败而 follower 的成功了,每个节点都从它自己的 recording log 恢复。这是正常现象,不是分叉(divergence)——所有路径都会通过 log 回放收敛到相同状态。

先做诊断。前两条命令把节点的 cluster 目录当作普通文件读取,在节点完全停机时也能用;后两条要连接本地 archive,因此需要该节点的 media driver 和 archive 处于运行状态(仅当只有 cluster 服务崩溃、而非整个进程被杀时才适用):

Terminal window
CLUSTER_DIR=/srv/node0/cluster
CP=aeron-all-1.51.1.jar
java -cp $CP io.aeron.cluster.ClusterTool $CLUSTER_DIR errors # 它为什么死了?(离线)
java -cp $CP io.aeron.cluster.ClusterTool $CLUSTER_DIR recording-log # 存在哪些快照?(type=1 条目,isValid) (离线)
java -cp $CP io.aeron.cluster.ClusterTool $CLUSTER_DIR recovery-plan 1 # 恢复会用什么?(参数 = service 数量;需 archive 运行)
java -cp $CP io.aeron.cluster.ClusterTool $CLUSTER_DIR describe-latest-cm-snapshot # 回放并打印 CM 快照 (需 archive 运行)

然后对号入座。

情形 A:节点在打快照途中被杀——直接重启

Section titled “情形 A:节点在打快照途中被杀——直接重启”

流程:重启进程。就这样。 启动时,节点从它的 recording log 构建恢复计划(被打断的快照不在其中),加载最新的有效快照,从快照位置回放自己的本地 log recording,然后接入 live log——从 leader 的 archive 拉取它错过的任何 term 并追平。你会在日志里看到节点在重新执行应用消息;那正是回放在做它该做的事。

唯一的代价是时间:恢复时长由快照的新旧程度(回放距离)加上追平时间决定。被打断的快照在重启前不需要任何清理。

为什么即使 archive 在写入途中被杀,重启仍然有效:被打断的快照 recording 在 archive catalog 中被留成「打开」状态(stopPosition = -1),下次启动时 archive 会扫描最后一个 segment、算出真实的 stop position,并自动修复该 descriptor。正如一位 Aeron 贡献者所说,“in order to ‘close’ a recording, we just need to start recording again”(要「关闭」一个 recording,我们只需再次开始录制即可)——archive 发现之前的写入者已经消失,就会替你把它关掉。无需手动动 catalog。

可选的清理工作: 被打断的那次尝试会在本地 archive 里留下孤儿 recording(archive 会在重启时修复它们的 stop position,但从不删除它们)。它们只是浪费磁盘,别无他害。要回收空间:找出快照通道(alias=snapshot)上那些 ID 出现在 ArchiveTool describe 里、却不在任何 recording-log 条目中的 recording,用 AeronArchive.purgeRecording 清除它们,再执行 ArchiveTool compact。这套识别流程是我们自行归纳的——Aeron 没有提供官方的「快照失败清理」指南。

偶尔 archive 本身会拒绝启动,节点带着 ArchiveException 崩溃循环。这才是人们真正报告的那个「kill-9 打快照途中把我节点搞坏了」的故事——但它是例外,而非常态,且分为两种截然不同的成因:

  • 最后一次写入被撕裂—— ArchiveException: Found potentially incomplete last fragment straddling page boundary in file: …,运行时还会给出提示 Run 'ArchiveTool verify' for corrective action!。上文那套自动修复分不清「全是零填充的尾部」和「写了一半的帧」,所以它拒绝猜测。这压倒性地是一种断电 / VM 崩溃 / 网络卷的风险,而非单纯的 SIGKILL:在默认的 aeron.archive.file.sync.level=0 下,写入停留在操作系统的 page cache 里,kill -9 会把它完整留给内核去刷盘——只有真正的断电才会丢失它们。修复: 运行 ArchiveTool verify(把有问题的条目标记为 INVALID)再 ArchiveTool compact,然后重启。预防: 设置 aeron.archive.record.checksum=CRC-32C,若必须扛住断电,还要把 aeron.archive.file.sync.level aeron.archive.catalog.file.sync.level 提到 1–2(代价是写入路径延迟——见主机与操作系统参数)。
  • 回放位置不匹配—— ArchiveException: requested replay start position=… is less than recording start position=…。这不是单次被打断的快照造成的;它出现在 log 被截断/清除、而某节点随后需要从一个已不存在的位置回放时。那属于情形 D 的范畴,而非情形 A——见下文。

情形 B:onTakeSnapshot 抛异常——节点仍在运行

Section titled “情形 B:onTakeSnapshot 抛异常——节点仍在运行”

节点还活着、还在处理消息;失败的只是那次快照尝试本身。consensus module 记录了 service=N failed to take snapshot(可通过 ClusterTool errors 查看),并且没有往 recording log 里追加任何东西。

  1. 查看节点上的 errors,拿到你服务代码抛出的真实异常。
  2. 修复 bug。 会抛异常的 onTakeSnapshot 几乎总是应用层的序列化缺陷——和那种在抛异常时导致静默快照分叉的是同一类 bug。
  3. 部署修复后重新打快照ClusterTool <leader-cluster-dir> snapshot)。在那之前集群照常运行,但它的恢复回放窗口会不断增长——旧快照意味着漫长的重启。

情形 C:已完成的快照损坏——使其失效

Section titled “情形 C:已完成的快照损坏——使其失效”

这是快照确实提交了(条目存在,isValid=true)、但节点在加载它时挂掉的情形——snapshot ended unexpectedlyonStart 中的反序列化异常、崩溃循环。尽管协议已完成,快照仍是坏的;恢复必须被指向上一个快照。

  1. 停掉节点。 invalidate-latest-snapshot 直接写 recording.log 文件;它不会与运行中的 consensus module 协调。
  2. 使其失效:
    Terminal window
    java -cp $CP io.aeron.cluster.ClusterTool $CLUSTER_DIR invalidate-latest-snapshot
    # → invalidate latest snapshot: true
    这会在最新快照的 consensus-module 条目以及它所有的 service 条目上翻转 invalid 位,于是恢复回退到上一个快照——若一个都没剩,则做完整 log 回放。再运行一次可回退两个快照。
  3. 重启,并为更长的回放预留时间。

如果是逻辑性损坏(一个被固化进快照内容里的序列化 bug),那么每个节点上都存在同一个坏快照——停掉所有节点,在每个节点的 cluster 目录上都执行 invalidate,再一起重启它们。这套「全部节点、停机状态下」的流程正是 Aeron 自己的系统测试所做的;官方文档没有明说,因此把测试当作参考依据。

情形 D:本地数据不可恢复——清空或重建

Section titled “情形 D:本地数据不可恢复——清空或重建”

当 archive 或 cluster 目录本身的损坏已超出快照链能覆盖的范围时,OSS 给你两条路,区别在于 leader 上是否还留着完整的 log

  • 完整 log 仍在 → 清空该节点的 cluster 和 archive 目录,让它空着启动。节点重新加入,leader 从位置 0 把 log 回放给它(一个 Aeron 系统测试正好验证了这一点)。注意事项:OSS 在 2023 年移除了动态成员管理,所以替换节点必须复用已死成员的 endpoint;快照不会被复制给一个空的 OSS 节点,因此这次全历史回放必须在你当前的代码版本下具备确定性;而且从零回放在一个成熟系统上可能耗时数小时。
  • log 已被截断/清除到某个快照 → 一个空节点无法重新加入;它所需要的回放已经不存在了。从 Cluster Backup 重建(触发一次新快照,让备份节点复制它,把它的 archive + recording.log 拷到替换节点上),或者从一个健康且已停机的对等节点拷贝数据目录。Martin Thompson 的指导:一个丢失了存储的节点「需要让 leader 从头把 log 回放给它」——否则你就需要一份备份,而「ClusterBackup 提供了做这件事的基础机制」。

这也是为什么 seed-recording-log-from-snapshot——那个把节点的 recording log 重定基到其最新快照、以便清除旧 log recording 的工具——必须谨慎使用:在 leader 上清除了 log,你就为将来每一次重建都断掉了情形 D 的第一条路。

OSS vs. Premium:故障语义相同,运维选项不同

Section titled “OSS vs. Premium:故障语义相同,运维选项不同”

Aeron Premium 处理一个在打快照途中被杀的节点,方式有何不同吗?没有。 Premium 构建在开源 cluster 内核之上;快照提交协议、recording-log 失效模型、以及上文那套「终止性 / 非终止性」错误划分,都是同一份代码路径。公开的 Premium 文档没有描述任何不同的快照失败处理方式——而且快照完整性工具(invalidate-latest-snapshotvalidate-recording-logdescribe-latest-cm-snapshot)全都属于 OSS。(Premium 内部实现在订阅之后,我们只能就公开记录的内容作证——但公开的一切都指向同一个内核。)

Premium 改变的是故障周围的一切——你多久打一次快照、一个好快照能从哪里来、以及你能多快重建:

OSSPremium (Cluster Standby 等)
被打断的快照语义无 recording-log 条目;重启从上一个快照恢复完全一致——同一个 consensus-module 内核
谁来打快照每个节点,包括 leader——一次集群级别的暂停Standby 快照:在不参与投票的 standby 节点上打;集群从不暂停
恢复时的快照来源仅本地 archive本地,外加重启中的节点会探测到更新的 standby 快照,并在加载前从 standby 复制一份——或用 replicate-standby-snapshot 预取
陈旧/已失效的快照链回退到更早的快照 + 更长的回放一个更新的 standby 快照可以掩盖坏掉的本地链;invalidate-latest-snapshot 也会使 standby 快照条目失效(自 1.47.0 起)
快照保留 vs. log 清除手动纪律(seed-recording-log-from-snapshot + archive 清除)Data Retention Regulator 策略保留最近 N 个有效快照以及恢复所需的 log
快照健康监控ClusterTool errors、计数器(见监控与告警)Aeron Insights (beta) Prometheus 指标——有效快照数、最近快照时间戳、快照时长阈值超限
重建一个已死成员ClusterBackup + 手动拷数据、静态 endpointstandby 节点原地转换(transition-as-member)——无需从零回放
  • 恢复时间(可用性) —— 被打断的快照在故障发生时不花任何代价;账单是在重启时以回放长度的形式到来的,而回放长度由最后一个有效快照的新旧程度决定。每一次失效操作(情形 C)都会在此基础上加码:回退一个快照,就多一段回放窗口。
  • p99 / 最大延迟 —— 在 OSS 中,重新打一次失败的快照意味着热路径上又一次 stop-the-world 暂停;在繁忙的撮合引擎上,这一暂停会正正落在最大延迟上。正是这一点——而非故障语义——把生产级交易系统推向 standby 快照
  • 吞吐量 —— 不受快照失败本身影响;当一个 follower 的快照尝试死掉时,集群仍在继续提交。真正要盯的是磁盘写满这一终止性情形,它会把节点一并带走(若发生在 leader 上,还会触发一次选举)。