跳转到内容

故障模式运行手册:客户端、运维与资源

调优让集群变快。而这份运行手册则是在出问题时让它依然保持正确。本手册涵盖三类故障,它们有一个共同特征:很少表现得像基础设施故障。它们看起来更像是客户端重试、配置手误、以及悄无声息的资源压力——而恰恰是这些故障,团队往往忘了提前规划。

请把这三个小节当作一份分诊指南来读。每一节都先说明会出什么问题、它会让你付出什么代价,以及具体该怎么应对。

客户端与集群的边界,是可靠性最常被误解的地方。有三种情形最为常见。

情形数据丢失风险可用性影响恢复复杂度补救措施
leader 选举期间客户端断连未提交的消息短暂(需重连)低——客户端重试逻辑实现客户端侧的 leader 发现与自动重连;跟踪 correlation ID;重连后重试未确认的消息
重复消息投递(客户端重试)无(但会重复处理)低——实现幂等性在集群化服务中通过 correlation ID / 序列号去重来实现幂等
入口处消息丢失(提交前 leader 崩溃)未提交的消息短暂低——客户端超时 + 重试客户端通过超时检测到缺失的出口响应;将消息重试到新 leader

有三种模式能把这些情形从事故变成无关紧要的小插曲。

1. correlation ID 跟踪。 每条发往集群的消息都应包含一个唯一的 correlation ID。客户端跟踪哪些 ID 已被确认。重连之后,重试所有未确认的 ID。

2. 服务代码中的幂等性。 集群化服务必须能优雅地处理重复消息:

// In onSessionMessage():
if (processedCorrelationIds.contains(correlationId)) {
// Already processed — send cached response
return;
}
// Process and record
processedCorrelationIds.add(correlationId);

3. leader 发现。 客户端应实现自动 leader 发现——当被某个 follower 重定向时,无需人工干预即可重连到 leader。

完整的端到端订单流——每一层的可靠性

Section titled “完整的端到端订单流——每一层的可靠性”

一笔订单在被撮合之前要穿过多个层级。每一层的边界都是一个潜在的消息丢失点。在每一跳上要问的关键问题是:谁负责重试——是发送方,还是我们可以依赖上游源头重新发送?

方面详情
协议HTTPS / WebSocket(对外)
重试责任方客户(发送方)
确认机制HTTP 响应(同步)或 WebSocket ack 帧
超时与重试客户设置请求超时;若未收到 HTTP 200 / ack,客户必须重发
幂等键客户必须在每次请求中包含一个客户端生成的 clientOrderId(幂等键)
为何由发送方重试API Gateway 是无状态的——它并不知道客户原本想发送什么。只有客户知道原始请求,才能重发

设计要点:

  • API Gateway 应使用 clientOrderId 尽早拒绝重复请求(在一个短生命周期的去重缓存中查找,或转发给 OMS 去重)。
  • 限流和鉴权在这里完成——来自客户的重试同样受相同的限流规则约束。
  • WebSocket 连接应实现心跳/ping-pong;若连接断开,客户重连并重发任何未确认的订单。
方面详情
协议Aeron 集群入口(API Gateway 充当 OMS 集群的 Aeron 客户端)
重试责任方API Gateway(发送方)——标准的 Aeron 客户端-集群契约
确认机制OMS 集群回送给 API Gateway 的出口响应
超时与重试API Gateway 按 clientOrderId 跟踪每个请求。若在超时内未收到出口 ack,Gateway 重发到 OMS 集群(并处理 leader 发现/重连)
幂等键从客户传递下来的 clientOrderId
为何由发送方重试与其他任何一跳相同的 Aeron 客户端-集群契约。OMS 集群并不跟踪 Gateway 原本想发送什么。在 OMS leader 选举或网络问题期间,消息可能在共识提交之前丢失。只有 Gateway 知道哪些请求尚未完成

设计要点:

  • API Gateway 是连接到 OMS 集群入口的一个 Aeron 客户端——它遵循上文最佳实践中描述的同样的 leader 发现、correlation ID 跟踪和重试模式。
  • OMS 集群的 onSessionMessage() 在处理之前先按 clientOrderId 去重。
  • 由于 OMS 是一个集群,它为订单生命周期提供了持久化、可复制的状态——如果某个 OMS 节点崩溃,订单状态不会丢失(它就在 Raft 日志里)。

③ OMS (Aeron Cluster) → Matching Engine (Aeron Cluster)——集群到集群

Section titled “③ OMS (Aeron Cluster) → Matching Engine (Aeron Cluster)——集群到集群”
方面详情
协议Aeron 集群入口(OMS 集群的状态机充当 ME 集群的 Aeron 客户端)
重试责任方OMS 集群 leader(发送方)——但有一个重要的微妙之处(见下文)
确认机制ME 集群回送给 OMS leader 的出口响应
超时与重试OMS 状态机按 correlationId 跟踪每个转发的订单。若在超时内未收到出口 ack,OMS 重发到 ME 集群
幂等键correlationId(由 clientOrderId 映射而来)——撮合引擎据此去重
为何由发送方重试同样的 Aeron 客户端-集群契约。ME 集群并不知道 OMS 原本想发送什么。只有 OMS 集群(作为 Aeron 客户端)知道哪些订单正在飞往 ME 的途中

设计要点——集群到集群的通信模式:

  • 只有 OMS leader 向 ME 发送。 OMS leader 的状态机创建一个到 ME 集群的 Aeron 客户端会话。follower 不会独立向 ME 发送——否则会导致重复。

  • OMS leader 选举 → 重连到 ME。 当 OMS 集群选举出新 leader 时,新 leader 必须建立一个到 ME 集群的新 Aeron 客户端会话,并重发任何在途的订单(即已提交到 OMS Raft 日志但尚未被 ME 确认的订单)。这样做是安全的,因为:

    • OMS Raft 日志是”已发送内容”的唯一可信源。
    • 在回放时,新的 OMS leader 确切地知道哪些订单需要被转发。
    • ME 集群按 correlationId 去重。
  • 确定性回放至关重要。 OMS 状态机决定将某笔订单转发给 ME 的判断必须是确定性的。发送这个动作本身是一种副作用——在回放期间,OMS 会重放该判断,但必须小心:只有当它是在线的 leader 时(而非 follower 上的日志回放期间)才能真正重新触发发送。一种常见模式:

    // In OMS ClusteredService.onSessionMessage():
    void onNewOrder(long correlationId, Order order) {
    // Deterministic state update (replayed on all nodes)
    orderBook.trackOrder(correlationId, order);
    pendingForward.add(correlationId, order);
    // Side effect: only the live leader actually sends to ME
    if (role == Cluster.Role.LEADER) {
    meClient.offer(encodeOrder(correlationId, order));
    }
    }
  • 待转发订单的跟踪状态可在故障切换后存活。 由于 pendingForward 是 OMS 集群可复制状态的一部分(会被快照 + 回放),新 leader 确切地知道哪些订单已被接受但尚未被 ME 确认——并能将它们重发出去。

  • 撮合引擎的 onSessionMessage() 必须在将订单应用到订单簿之前按 correlationId 去重。

方面详情
协议Raft 共识(Aeron 集群内部)
重试责任方由 Aeron 集群框架自动处理
确认机制Raft 日志复制——leader 复制给 follower,多数派 ack 后即提交
为何无需手动重试一旦消息提交到 Raft 日志,框架就保证它会被复制并在所有节点上回放。这就是恰好一次的边界
重试责任方能否依赖源头重发?原因
Customer → API GW客户否——客户就是源头不存在更上游的源头。客户是其意图的唯一可信源。
API GW → OMS ClusterAPI Gateway部分可以——若 GW 崩溃,客户会超时并重发到另一个 GW 实例,这实际上重试了整条流程但 GW 仍应首先自行重试。仅依赖客户重发会增加延迟(客户超时通常更长)。GW 的上下文还在内存中,能更快重试。
OMS Cluster → ME ClusterOMS leader可以——OMS Raft 日志就是安全网。 若 OMS leader 崩溃,新的 OMS leader 从 Raft 日志回放,发现待处理(未确认)的订单,并将它们重发给 ME。这一跳无需上游帮助。这正是 OMS 作为集群的关键优势:在途订单状态在 OMS Raft 日志中是可复制且持久的。与崩溃时会丢失内存状态的无状态 OMS 不同,集群化的 OMS 通过日志回放恢复其待转发队列。只有当订单从一开始就从未提交到 OMS Raft 日志时,才需要上游(GW)重发。
OMS 内部 (Raft)Aeron 框架不适用——自动处理Raft 共识协议负责复制。无需应用层重试。
ME 内部 (Raft)Aeron 框架不适用——自动处理Raft 共识协议负责复制。无需应用层重试。
  • 每个发送方都对自己所在层级的重试负责。 这是最安全的默认做法——每一层都对其出站这一跳的可靠性负责。
  • 将 OMS 设计为 Aeron 集群是关键的设计抉择。 它为订单生命周期提供了持久化、可复制的状态。无状态的 OMS 在崩溃时会丢失在途订单,必须依赖上游重发。集群化的 OMS 通过 Raft 日志回放恢复自身的待处理状态——对于 OMS→ME 这一跳,它是自愈的。
  • 集群到集群的通信需要谨慎处理。 只有 OMS leader 向 ME 发送。在 leader 选举时,新 leader 必须从 Raft 日志重建在途状态,并重新建立到 ME 的 Aeron 客户端会话。follower 绝不能独立向 ME 发送。
  • 上游重发是兜底机制,而非主要机制。 若 API Gateway 崩溃,客户最终会重发。但在 OMS→ME 边界之内,OMS 集群自行处理重试——无需上游帮助(除非订单从一开始就未到达 OMS)。
  • 幂等性必须端到端地传播。 clientOrderId 起源于客户,并流经每一层。每个集群都使用这个键(或像 correlationId 这样的派生键)去重。没有它,任何一层的重试都会导致订单被重复执行。
  • 存在两个恰好一次的边界。 OMS Raft 提交保证订单被持久地接受。ME Raft 提交保证订单被持久地撮合。在这两个边界之间(OMS→ME),契约是至少一次 + 幂等性。

集群能够自行扛过节点和网络故障。但它扛不过一个错误的 clusterMembers 字符串或一次 kill -9。这些是由你造成的故障,也正是运行手册存在的意义所在——预防它们。

情形数据丢失风险可用性影响恢复复杂度补救措施
集群配置错误完全(无法组成集群)低——修复配置并重启部署前校验 clusterMembers 字符串、端点和 member ID;修复配置;重启所有节点
持久化数据被误删可能至完全取决于范围中等至 Critical单节点:清空并从对等节点重建。所有节点:从外部备份恢复。实施文件系统级保护(快照、不可变备份)
不当关停(对所有节点 kill -9)可能完全中等——检查录制完整性重启所有节点;Aeron Archive 在启动时校验录制;若损坏,从备份恢复。生产环境请使用优雅关停(SIGTERM)
滚动升级失败静默分叉可能高——回滚可能需要旧快照停止已升级的节点;恢复升级前的快照;用旧版本重启;修复兼容性后再重试
节点间时钟偏移降级(误触发选举)低——同步时钟(NTP)在所有节点上配置 NTP/chrony;增大选举超时以容忍轻微偏移
  1. 配置校验——部署前校验集群配置(member ID、端点、端口冲突)。
  2. 优雅关停——始终使用 SIGTERM,绝不使用 SIGKILL。优雅关停让节点能完成在途操作并干净地关闭录制。
  3. 升级前快照——任何升级之前都务必生成一份快照。这给你一个干净的回滚点。
  4. 文件系统保护——对 archive 和集群目录使用不可变备份或文件系统快照。
  5. 时钟同步——NTP/chrony 是必选项。时钟偏移不会导致数据丢失,但可能触发不必要的选举。

这些故障有一个共同的特征:没有任何东西崩溃,但 leader 漏掉了一次心跳,于是集群陷入恐慌。资源压力会以误触发选举的形式表现出来,这也正是它如此容易被误诊的原因。

情形数据丢失风险可用性影响恢复复杂度补救措施
JVM OOM单节点下线低——调优后重启增大堆(-Xmx)后重启节点;剖析内存使用;修复内存泄漏;在服务中实现状态淘汰
文件描述符耗尽单节点下线低——增大 ulimit 后重启增大 ulimit -n;重启节点;检查应用代码中的 FD 泄漏
CPU 饥饿 / GC 停顿降级(误触发选举)中等——调优 GC / 线程切换到低延迟 GC(ZGC/Shenandoah);将 Aeron 线程绑定到专用 CPU 核心;降低服务中的分配速率
线程饥饿(SHARED 模式 + 慢处理器)降级(被认为已死)中等——优化处理器或改用 DEDICATED优化 onSessionMessage 处理器;将重活卸载出去;切换到 DEDICATED 线程模式;增大心跳超时

最常见的模式是一个反馈回路。资源压力引发一次选举,选举引发重连流量,而这些流量又带来更多压力。

注意这是如何与上文的客户端小节呼应起来的:行为良好的客户端重连与幂等逻辑能阻止这股重连洪峰把一次 GC 停顿演变成一场持续的级联。

资源监控预防
堆内存-XX:+HeapDumpOnOutOfMemoryError、JMX 指标合理设置堆大小,剖析分配速率,修复泄漏
文件描述符lsof 计数、/proc/pid/fd设置 ulimit -n 65536+,正确关闭资源
CPUperfasync-profiler、周期时间计数器绑定线程,隔离核心,使用合适的空闲策略
线程线程转储、周期时间计数器生产环境使用 DEDICATED 模式,避免在处理器中阻塞

把这些故障模式映射回你的延迟目标,优先级便自然浮现:

  • p50 取决于 onSessionMessage() 一个缓慢或频繁分配的处理器不只是抬高中位数——在 SHARED 模式下它还会让 conductor 饥饿并误触发选举。保持处理器精简,并在生产环境运行 DEDICATED 模式。
  • p99 会被 GC 停顿和误选举毁掉。 一次长时间的 stop-the-world 停顿既会让 p99 飙升,可能让 leader 掉线。低延迟 GC(ZGC/Shenandoah)、绑定的 Aeron 线程和隔离的核心共同守护着尾延迟。
  • 级联期间吞吐量崩溃。 由重连洪峰喂养的选举风暴,会在其持续期间让整个集群下线。具备 correlation-ID 重试和合理退避的幂等客户端,正是让一次瞬时停顿不致变成持续宕机的关键。

最廉价的可靠性收益几乎总是来自边界处的纪律——幂等重试、优雅关停、合理设置限额——而不是事故发生时的临场英雄主义。