在一次流量高峰中,异步媒体处理链路出现持续堆积。表面现象是队列越来越长、机器却没有完全忙起来;进一步排查后发现,调度查询、任务回调、消费限速和资源缩减相互影响,把局部拥塞放大成了整条链路的恢复困难。
这篇日志不保留具体业务、容量和内部实现,只讨论复盘中可以复用的系统设计问题。
链路为什么会失去吞吐
简化后的系统包含四个环节:
消息入口 → 队列服务 → 调度器 → 执行节点
↑ ↓
└── 状态回调 ──┘
高峰到来后,入口继续产生任务,队列服务根据阈值暂停读取;调度器为了计算队列状态执行了越来越重的查询;执行节点完成任务后又需要回调调度器更新状态。
当调度器被慢查询拖慢时,会同时出现两个后果:
- 新任务派发变慢,执行节点逐渐空闲;
- 已完成任务回调失败,任务仍被认为处于执行中。
于是系统看上去“队列很长、机器很多”,真正可用吞吐却持续下降。
背压阈值为什么没有控制住队列
原有背压只在某一层达到阈值后停止从消息系统读取。它限制的是“继续读取”,而不是所有来源的任务增长。
以下路径仍可能让待处理集合继续扩大:
- 已经预取到本地、但尚未提交的任务;
- 失败回调触发的重试和重新派发;
- 绕过主入口的补偿或人工操作;
- 多层队列之间状态更新存在延迟;
- 执行完成但状态没有成功收敛的幽灵任务。
因此,不能把一个队列长度阈值等同于整个系统的容量边界。背压必须沿着任务生命周期逐级传播。
状态回调也是主链路
异步系统常把回调当成收尾动作,但任务能否释放资源、进入完成态、触发下游步骤,都依赖回调成功。
更稳妥的设计需要:
- 回调幂等,同一完成事件可以安全重放;
- 状态写入失败后进入独立重试通道;
- 执行节点释放 slot 不依赖单次远程调用成功;
- 调度器可以根据执行租约识别失联或超时任务;
- 对“业务执行成功、状态提交失败”单独统计。
如果回调失败只能等待一个很长的统一超时,系统会在恢复瞬间产生批量重派,形成第二次流量峰值。
控制面查询不能随队列线性变重
故障期间,调度器需要频繁读取队列规模和任务状态。如果查询成本随队列长度增长,越是拥塞,控制面自身越慢。
队列规模应该使用增量计数、分桶统计或近似指标,而不是反复扫描大集合。精确查询可以保留给低频审计,不能放在高频调度循环中。
可以把控制面接口分成两类:
| 类型 | 要求 |
|---|---|
| 调度热路径 | 有界复杂度、可降级、避免大结果集 |
| 诊断与审计 | 允许更高成本,但限频并隔离资源 |
限速需要跟随真实处理能力
固定消费速率在平稳阶段简单有效,高峰和恢复阶段却可能产生两个极端:
- 限制过低:执行节点空闲,积压消化速度跟不上;
- 放开过快:大量任务瞬间进入调度器,使控制面雪崩。
更合适的输入包括:执行节点可用槽位、近期任务完成率、状态回调延迟、调度器错误率和下游资源水位。消费速率应缓慢上升、快速下降,并设置单次调整幅度。
恢复顺序
故障恢复不是把所有开关一次性打开。一个更安全的顺序是:
- 停止新的非必要任务进入;
- 修复或隔离控制面的高成本查询;
- 恢复状态回调与资源释放;
- 核对幽灵任务、重复任务和超时租约;
- 小步提高消费速率,观察完成率而非只看读取量;
- 最后恢复低优先级任务和补偿任务。
每一步都应定义继续和回退条件。只观察队列长度下降,可能掩盖失败率和重复执行正在上升。
复盘后的设计清单
- 背压是否覆盖所有任务入口,而不只是消息消费者;
- 控制面查询在最大积压下是否仍为有界成本;
- 完成回调能否幂等重放,并与资源释放解耦;
- 自动重派是否带抖动、速率上限和批次控制;
- 优先级所需的元数据缺失时,是否采用安全默认值;
- 缩容前能否确认执行中任务已经迁移或结束;
- 恢复流程是否经过演练,而不是只存在于故障现场。
异步系统的稳定性不只取决于正常吞吐。真正困难的是拥塞发生后,系统是否还能保持控制面可用,并以可预测的速度回到稳态。