WRITING / 2021.01.01

工程日志:异步媒体任务堆积后的背压复盘

一次高峰期异步任务堆积的脱敏复盘:为什么队列阈值没有阻止雪崩,以及如何重新设计背压、状态回调与恢复路径。

在一次流量高峰中,异步媒体处理链路出现持续堆积。表面现象是队列越来越长、机器却没有完全忙起来;进一步排查后发现,调度查询、任务回调、消费限速和资源缩减相互影响,把局部拥塞放大成了整条链路的恢复困难。

这篇日志不保留具体业务、容量和内部实现,只讨论复盘中可以复用的系统设计问题。

链路为什么会失去吞吐

简化后的系统包含四个环节:

消息入口 → 队列服务 → 调度器 → 执行节点
              ↑          ↓
              └── 状态回调 ──┘

高峰到来后,入口继续产生任务,队列服务根据阈值暂停读取;调度器为了计算队列状态执行了越来越重的查询;执行节点完成任务后又需要回调调度器更新状态。

当调度器被慢查询拖慢时,会同时出现两个后果:

  1. 新任务派发变慢,执行节点逐渐空闲;
  2. 已完成任务回调失败,任务仍被认为处于执行中。

于是系统看上去“队列很长、机器很多”,真正可用吞吐却持续下降。

背压阈值为什么没有控制住队列

原有背压只在某一层达到阈值后停止从消息系统读取。它限制的是“继续读取”,而不是所有来源的任务增长。

以下路径仍可能让待处理集合继续扩大:

  • 已经预取到本地、但尚未提交的任务;
  • 失败回调触发的重试和重新派发;
  • 绕过主入口的补偿或人工操作;
  • 多层队列之间状态更新存在延迟;
  • 执行完成但状态没有成功收敛的幽灵任务。

因此,不能把一个队列长度阈值等同于整个系统的容量边界。背压必须沿着任务生命周期逐级传播。

状态回调也是主链路

异步系统常把回调当成收尾动作,但任务能否释放资源、进入完成态、触发下游步骤,都依赖回调成功。

更稳妥的设计需要:

  • 回调幂等,同一完成事件可以安全重放;
  • 状态写入失败后进入独立重试通道;
  • 执行节点释放 slot 不依赖单次远程调用成功;
  • 调度器可以根据执行租约识别失联或超时任务;
  • 对“业务执行成功、状态提交失败”单独统计。

如果回调失败只能等待一个很长的统一超时,系统会在恢复瞬间产生批量重派,形成第二次流量峰值。

控制面查询不能随队列线性变重

故障期间,调度器需要频繁读取队列规模和任务状态。如果查询成本随队列长度增长,越是拥塞,控制面自身越慢。

队列规模应该使用增量计数、分桶统计或近似指标,而不是反复扫描大集合。精确查询可以保留给低频审计,不能放在高频调度循环中。

可以把控制面接口分成两类:

类型 要求
调度热路径 有界复杂度、可降级、避免大结果集
诊断与审计 允许更高成本,但限频并隔离资源

限速需要跟随真实处理能力

固定消费速率在平稳阶段简单有效,高峰和恢复阶段却可能产生两个极端:

  • 限制过低:执行节点空闲,积压消化速度跟不上;
  • 放开过快:大量任务瞬间进入调度器,使控制面雪崩。

更合适的输入包括:执行节点可用槽位、近期任务完成率、状态回调延迟、调度器错误率和下游资源水位。消费速率应缓慢上升、快速下降,并设置单次调整幅度。

恢复顺序

故障恢复不是把所有开关一次性打开。一个更安全的顺序是:

  1. 停止新的非必要任务进入;
  2. 修复或隔离控制面的高成本查询;
  3. 恢复状态回调与资源释放;
  4. 核对幽灵任务、重复任务和超时租约;
  5. 小步提高消费速率,观察完成率而非只看读取量;
  6. 最后恢复低优先级任务和补偿任务。

每一步都应定义继续和回退条件。只观察队列长度下降,可能掩盖失败率和重复执行正在上升。

复盘后的设计清单

  • 背压是否覆盖所有任务入口,而不只是消息消费者;
  • 控制面查询在最大积压下是否仍为有界成本;
  • 完成回调能否幂等重放,并与资源释放解耦;
  • 自动重派是否带抖动、速率上限和批次控制;
  • 优先级所需的元数据缺失时,是否采用安全默认值;
  • 缩容前能否确认执行中任务已经迁移或结束;
  • 恢复流程是否经过演练,而不是只存在于故障现场。

异步系统的稳定性不只取决于正常吞吐。真正困难的是拥塞发生后,系统是否还能保持控制面可用,并以可预测的速度回到稳态。