一佳技术

事件丢了怎么办:一条流水线链路的超时兜底设计

部署流水线本质是状态机,一枚事件丢失就会让计划永远 RUNNING。本文复盘我们的设计:为什么可靠消息覆盖不了外部回调丢失,以及如何用一个定时扫描器作为"另一个回调入口"实现全链路自愈。

2026-10-10 · 脉冲

部署流水线本质是状态机:节点 RUNNING → 终态 → DAG 传播 → 计划收尾。这条链路上任何一枚事件丢了,用户看到的就是「部署卡死、无任何按钮」。这篇文章复盘我们的兜底设计——结论先行:可靠消息系统覆盖不了真实环境里最常见的丢失场景,最终方案是给整条链路加一个定时扫描器。

一、链路拆开看:两段,两种丢失

外部系统(Jenkins、K8S、ArgoCD)完成构建后回调平台,平台内部再经事件总线驱动后续节点。整条链路有两个可能丢失的位置,性质完全不同:

丢失位置根因可靠消息能否解决
① 外部回调丢失Jenkins 任务被 kill、回调 Job 超时、网络抖动❌ 回调根本没进来,与消息系统无关
② 内部事件丢失 / 重复轻量开发基座的 fire-and-forget;生产环境 at-least-once 带来重复投递✅ 大部分消除,残余靠消费端幂等

这就是核心洞察:broker 解决的是「自己人之间」的可靠性,覆盖不了「外人根本不回调」——而后者恰恰是真实环境里发生频率最高的故障。所以在生产环境换上 RocketMQ/Kafka(方案 A,随交付部署落地)之外,我们还需要一个全覆盖的兜底(方案 B)。

二、扫描器:不是新机制,是「另一个回调入口」

设计上最关键的一个决定:扫描器发现超时后不发自研事件,而是同步直调领域服务的同一个入口——和 HTTP 回调走完全相同的代码路径。

为什么?想想兜底组件的处境:它存在的理由就是「事件不可信」,如果它自己再发一枚 fire-and-forget 事件,丢了怎么办?再套一层兜底?递归没有意义。

直调之后的好处自动成立:

  • 终态守卫(幂等):回调先到就跳过,不会误杀刚完成的节点;
  • DAG 传播、告警推送、计划收尾:全部复用既有逻辑,一行不写;
  • 兜底链收敛:扫描器发出的下游事件若又丢,下一轮扫描周期再兜——最多晚一个周期,不会永久丢失。

这是「把兜底设计成普通入口」的思路:系统里触发状态流转的入口可以是多个(HTTP 回调、消息消费、定时扫描),业务规则只在一处。

三、防误杀的四个细节

兜底机制最大的风险是把正常的慢操作杀掉。我们在规则里做了四层防护:

  1. 默认阈值保守:全局默认 30 分钟,覆盖最慢的构建类型;
  2. 最小下限 60 秒:即使节点误配了 10 秒超时,也按 60 秒兜底——配置错误不该变成生产事故;
  3. 时钟起点明确:以节点启动时间为判定基准,而不是进入队列的时间;
  4. 误杀可恢复:超时标记的 FAILED 不是终审——节点支持重试、计划支持重新发起,用户一键即可恢复。

另外还有一个隐蔽场景:DAG 断链。所有节点都已终态,但中间一枚事件丢失导致传播中断——计划永远 RUNNING,却没有 RUNNING 节点可供扫描。对策是计划级检测:RUNNING 计划若长期(宽限 30 分钟)无任何活动节点,判定为内部事件丢失,整体标 FAILED 可重试。

四、实施成本与边界

这套机制的落地成本出乎意料地低:

  • 零数据迁移——建表时预留的超时列直接启用;
  • 零新增领域逻辑——扫描器与回调入口、消息消费者对称放置在适配层,三者共用同一个领域服务;
  • 配置即纪律——扫描间隔、各阈值全部配置化,开发环境可以调短到秒级做演练。

两条边界需要如实记录:其一,当前单实例部署下定时任务天然安全,未来多副本需要加分布式锁;其二,扫描是秒级精度的兜底,不能替代实时链路的告警——它的定位是最后防线,与 K8S rollout timeout、消费端幂等这些局部兜底互不冲突、逐层收敛。

小结

可靠性设计里最容易被高估的是消息中间件,最容易被低估的是「用一个笨但确定的扫描器兜住一切」。中间件解决系统内部的传输问题,而分布式系统真正的不可靠往往来自边界——第三方不回调、时钟不同步、人配置错了。给状态机的每个可能卡死的位置找一个确定性的出口,是比堆中间件更划算的投入。