Skip to content

异步任务卡死、重复消费与状态恢复故障复盘

30 秒复盘结论: 这是一场故障演练:Worker 已完成外部副作用,但在更新任务状态和确认消息前崩溃;租约过期后消息再次投递,造成任务长期停在 PROCESSING 或重复执行。止损是暂停相关分区、隔离消息并按 task_id/effect_id 对账;长期修复采用幂等收件箱、事务发件箱、带版本租约、对账任务和死信队列。当前只有演练 Schema 与验证计划,没有真实消息、积压量或恢复时长。

目录

1. 摘要与小白解释

仓库管理员搬完一箱货,正准备在登记簿上写“完成”时突然停电。下一班管理员只看到登记簿仍写“处理中”,于是又搬了一次。这里,货物移动是外部副作用,登记簿是任务状态,交接令牌是消息确认,值班有效期是租约。

正确做法不是看到“处理中”就永远等待,也不是超时就盲目重做,而是查询货物是否已经到位,再恢复登记簿。类比忽略了多分区顺序、不可查询外部系统、事务边界和补偿失败。

项目内容
现象任务卡在处理中、消息重复投递、外部结果重复
直接原因副作用、状态写入和消息确认不在同一事务边界
根本原因缺少结果未知状态、幂等消费和周期对账
止损暂停消费、隔离消息、对账外部事实、人工恢复
修复Inbox/Outbox、租约状态机、Reconciler、DLQ

2. 故障教学图片

图:故障教学图片|异步任务结果未知与状态恢复闭环

替代文本: 任务 API、消息队列、Worker、外部副作用和状态库形成异步链路;副作用成功但确认丢失和租约过期导致卡死或重复,证据支持暂停消费、对账和幂等恢复,事实边界为故障演练。

教学图片待人工审图任务 API、消息队列、Worker、外部副作用和状态库形成异步链路;副作用成功但确认丢失和租约过期导致卡死或重复,证据支持暂停消费、对账和幂等恢复,事实边界为故障演练 暂不公开,正文与 Mermaid 图可正常阅读。

读图结论: 结果未知时先核对外部副作用,再恢复任务状态和消息消费。

图中红色路径同时展示“卡死”和“重复”两种表象,它们可能共享同一事务断点;绿色路径将幂等消费、可靠发布、租约和对账组合起来。

图片生成记录: model=gpt-image-2generated=2026-07-15prompt_version=v1查看 Prompt。PNG C2PA 记录 softwareAgent=gpt-imageversion=2.0

质检状态: Agent 已核对中文、箭头和事实边界;维护者审图待完成。

3. 事实、影响与指标

结论类型证据或验证动作
消息至少一次投递可能重复工程机制在消费后、Ack 前注入进程崩溃
PROCESSING 表示任务仍在运行不一定成立必须结合租约、心跳和外部事实
增加重试次数能恢复卡死不成立可能扩大重复副作用
本文是实际队列事故不成立没有真实 Broker、任务或日志证据

合成记录至少包含 task_idmessage_idattemptstatelease_ownerlease_versionlease_untileffect_idlast_heartbeat_aterror_classtrace_id

text
stuck_task_age = 当前时间 - 最近一次有效状态迁移时间
duplicate_effect_rate = 同一 task_id 对应多个有效 effect_id 的任务数 / 已产生副作用的任务数
reconciliation_lag = 外部事实形成时间 - 本地状态收敛时间

影响域包括生成、支付、通知、媒体处理和数据同步任务;未产生副作用的纯计算任务可以更积极地重试。具体告警阈值取决于任务 SLA 和最长合法执行时间。

相对时间线:T0 消费消息并获取租约;T0+1 外部副作用完成;T0+2 Worker 在状态写入前崩溃;T0+3 租约过期且消息再投递;T0+4 暂停分区并对账;T0+5 修复后重放隔离消息。

4. 技术栈与架构

4.1 技术点清单

技术点 ID技术点/环节类型采用方案链路职责版本/证据边界
TP-01消费幂等存储模式Inbox 唯一键同一消息只创建一次业务执行权演练 Schema
TP-02可靠发布存储/消息模式Transactional Outbox状态与待发布事件原子提交演练 Schema
TP-03执行所有权状态机版本化 Lease 与 Heartbeat判断 Worker 是否仍拥有任务待并发测试
TP-04状态恢复服务Reconciler 加 DLQ对账外部事实并隔离毒消息待故障注入

图:架构|异步任务、消息、租约与外部副作用边界

替代文本: 任务 API 在状态库创建任务和 Outbox,发布器发送到队列;Worker 通过 Inbox 和租约获得执行权后调用外部系统,Reconciler 查询状态库与外部事实,无法自动恢复的消息进入 DLQ。

图表加载中…

读图结论: 队列负责传递,不负责证明业务结果;最终状态必须由任务库和外部事实共同确认。

5. 调用与恢复流程

图:技术调用流程|副作用成功但确认丢失后的对账恢复

替代文本: Worker 通过 Inbox 和租约执行任务,外部副作用成功后 Worker 崩溃;消息再次投递时,新 Worker 不盲目执行,而是检测旧记录并由对账器查询外部结果,存在则收敛成功,不存在才重新执行。

图表加载中…

读图结论: 重复投递是正常传输语义,重复副作用才是必须由幂等与对账阻止的业务错误。

6. 止损、根因与选型

止损顺序:暂停受影响分区而不是清空队列;保存消息和任务快照;阻止旧租约继续写状态;按外部 effect_id 对账;将无法判断的任务隔离到 DLQ;确认安全后小批量重放。

  • 直接原因: Worker 在副作用成功后、状态写入和 Ack 前退出。
  • 根本原因: 系统把消息消费成功与业务副作用成功混为一谈,缺少对账状态。
  • 促成因素: 租约无版本、心跳只更新内存、状态更新不用 CAS、毒消息无限重试。
  • 非原因: Broker 重复投递不是缺陷;至少一次语义本就要求消费者幂等。
  • 防线失效: 测试没有覆盖进程在关键事务断点崩溃。
技术点 ID候选方案优点缺点/代价适用场景不适用场景选择结论与依据
TP-01仅内存去重重启失效、跨实例无效临时无副作用任务生产副作用不采用
TP-01持久化 Inbox可审计、跨实例增加写入至少一次消费极简离线脚本采用
TP-02先写库再直接发消息简单两步间可能丢消息可容忍人工补发核心状态事件不采用
TP-02Transactional Outbox本地事务一致发布器与清理成本数据库是真值无事务存储采用
TP-03固定超时抢占简单长任务会被误抢执行时长稳定AI/媒体长任务不采用
TP-03Lease + Heartbeat + CAS可判断所有权状态更复杂长时异步任务无共享状态采用
TP-04无限自动重试无人工介入毒消息和副作用放大高风险任务禁止
TP-04Reconciler + 有界重试 + DLQ可恢复且可隔离需要运维入口结果可查询或需人工判定完全不可追踪外部动作默认采用

7. 验证、防复发与面试表达

验证矩阵:消费前崩溃、外部调用前崩溃、调用成功后崩溃、状态提交后 Ack 丢失、心跳中断、旧 Worker 迟到写入、同消息并发消费、Outbox 重复发布、DLQ 重放。验收必须检查最终业务事实唯一、状态单调迁移、旧租约写入被拒绝和消息可审计。

防复发:任务状态机禁止任意跳转;所有副作用携带 task_id;Inbox/Outbox 表设唯一约束与归档策略;Reconciler 按状态年龄扫描;监控 stuck age、租约冲突、DLQ 增长和对账延迟;Runbook 固定“暂停—快照—对账—CAS 恢复—小批重放”。

难点卡: 最难的是区分“Worker 不在了”和“业务没有完成”,并允许旧 Worker、重投消息与对账器并发时仍只收敛到一个终态。

反模式:PROCESSING 当永久锁;手工把状态改成功却不核对外部事实;删除队列消除积压;用 Exactly Once 宣传语代替业务幂等。

7.1 30 秒面试回答

这是一次异步任务故障演练:Worker 已完成外部副作用,但在写状态和 Ack 前崩溃,消息重投后可能卡死或重复执行。我先暂停分区、隔离消息并按 task_id 对账,根因是消息确认、任务状态和外部事实之间缺少幂等与恢复协议。长期修复采用 Inbox、Outbox、版本租约、对账器和 DLQ,并通过关键断点崩溃与并发重放验证;当前没有真实生产数据。

7.2 60~90 秒与追问

排查时先看数据库状态和租约,再看 Broker 是否重投,最后核对外部 effect_id,不能只根据 UI 的“处理中”判断。至少一次投递允许消息重复,因此 Inbox 防止重复获得执行权;Outbox 解决本地状态和事件发布不一致;Lease 与 CAS 防止旧 Worker 迟到覆盖新结果。无法确认外部事实时进入人工接管,不能继续自动重试。

追问:为什么 Outbox 不能解决外部 API 原子性?租约多长合适?任务不可查询怎么办?DLQ 怎样安全重放?状态机如何防止旧 Worker 覆盖?

8. 总结

一句话记忆: 异步任务的消息状态、执行状态和业务事实必须分别取证再收敛。

  • 重复投递不等于重复副作用;
  • PROCESSING 必须有租约、版本和恢复路径;
  • Inbox、Outbox、Reconciler 与 DLQ 各自解决不同断点;
  • 恢复先对账外部事实,再 CAS 更新状态和重放;
  • 本文是故障演练,不是已发生事故。