调度、超时、重试与背压
本节目标
为时间触发和外部调用定义清晰的时间语义,并用超时、有限重试、重试预算、限流和背压避免重复执行与故障放大。
时间语义必须完整
“每天 9 点运行”至少缺少:
- 时区,例如
Asia/Shanghai; - 夏令时出现重复或缺失本地时间时怎么办;
- 系统停机错过三次后逐次补跑、合并一次还是跳过;
- 前一次尚未完成时允许重叠、跳过还是串行等待;
- 允许延迟多久,超过什么 deadline 就没有业务价值。
RFC 3339 表示的是带 UTC 关系的时间戳。它适合记录“某个瞬间”,但不能单独表达“每个地区工作日 09:00”这样的日历规则;调度还需保存 IANA 时区名和业务日历。
定时触发去重键可用结构化 (schedule_id, logical_fire_time)。不要用 worker 实际醒来的当前时间,也不要用未转义连接符拼成字符串,否则停机补跑、延迟触发或字段字符组合会被误认为新任务。还应区分该逻辑周期的 operation_id 与每次领取/投递的 attempt_id:补跑、broker redelivery 和 worker 接管都可能增加 attempt,但不能重新定义同一个业务周期。
五种常见超时
- 队列等待超时:任务还未被 worker 领取。
- 单次 attempt 超时:一次网络/工具调用最多等待多久。
- 步骤总超时:包含所有 attempt 与退避。
- 外部事件/审批等待期限:到期转拒绝、升级或人工处理。
- 工作流总 deadline:超过后整个结果失去业务价值。
超时只说明调用方停止等待,不证明下游没有完成。若付款请求超时,先用幂等键查询结果,再决定是否重试;直接再次扣款会扩大“结果未知”窗口。
重试只处理可能恢复的错误
| 错误 | 通常策略 | 原因 |
|---|---|---|
| 短暂网络中断、服务忙 | 有限重试 | 环境可能恢复 |
| 429/503 且有服务指示 | 遵守 Retry-After 后重试 | 下游给出恢复节奏 |
| schema 不合法 | 不重试 | 同一输入不会自行修复 |
| 401/403 | 通常不重试 | 权限或凭据问题需处理 |
| TLS 证书/主机名验证失败、入站签名无效 | 不重试,转配置/安全排查 | 重试不会让身份或信任链自行恢复 |
| 库存不足、审批拒绝 | 业务终止或补偿 | 不是技术故障 |
| 请求超时但下游状态未知 | 查询/对账后决定 | 盲重试可能重复副作用 |
工程上常用指数退避:
再加入随机抖动,避免大量实例同步重试。Microsoft 的瞬态故障指南也建议后台操作使用指数退避与 jitter,并强调先设置每次调用超时、限制总次数/总时长、考虑 Retry-After 和全局重试预算。
示例:初始 2 秒、上限 30 秒,失败后的基准等待为 2、4、8、16、30 秒。实际 jitter 形式由系统选定并测试,不能只写“指数退避”而没有最大次数与总 deadline。
计算多层重试放大
HTTP SDK 重试 3 次、步骤最多 4 个 attempt、工作流整体再重放 2 次,最坏下游调用不是 4 次,而可能达到 次。不同层的“次数”还可能分别表示总 attempt 或额外 retry,必须查清产品定义。
推荐由最了解业务幂等和截止时间的层统一策略,下层保留短小网络恢复,避免每一层都自行无限重试。
重试决定还应先过一个安全栅栏:对认证、授权、签名、资源版本或策略不匹配一律 fail closed;对“外部状态未知”先按 operation_id 查询或对账;只有确定是可恢复的传输/容量错误,且同一幂等意图、总 deadline 和重试预算仍有效时,才安排下一 attempt。不要为了提高成功率,把证书错误、审批失效或输出 schema 错误归入暂时故障。
背压与重试预算
背压是下游处理不过来时,主动减慢或拒绝上游,而不是继续堆积。常用手段:
- 每租户、工作流、步骤和外部依赖的并发上限;
- 有界队列与最大等待时间;
- 令牌桶/速率限制和供应商配额;
- 低优先级降级、延后或丢弃策略;
- 死信/人工队列保存重试耗尽的任务;
- 全服务重试预算,防止大量请求各自少量重试形成风暴。
队列能平滑峰值,但不是无限缓冲。持续“到达率 > 处理率”时,积压一定增长;要监控队列年龄而不只看长度,并在容量不足时限流或扩容。
LLM 节点的调度资源
LLM 调用还受 token/请求速率、上下文长度、预算、并发和供应商地区限制。把这些作为工作流资源:
- 调度器分配令牌和成本预算;
- 节点报告结构化限流/超时错误;
- 模型切换属于版本化路由策略,不能静默改变任务质量;
- 回退到小模型前先确认输出合同和评测门槛仍满足。
常见错误与排查
- 重试风暴:检查是否有 jitter、总 deadline、全局预算和
Retry-After。 - 定时任务重复:检查 logical fire time 和重叠策略,不要依赖进程锁。
- 任务永远在队列:观察最老消息年龄、消费者吞吐和死信数量。
- 超时后重复扣款:对账下游幂等记录,区分未执行与结果未知。
- 把新的 queue delivery 当作新的业务周期:保留
logical_fire_time和operation_id,只增加attempt_id。 - 高优先级任务饿死其他任务:配置公平配额或最大等待提升机制。
练习
设计一个上海时区每天 09:00 的报告任务:
- 写出 logical fire time、停机三天后的补跑策略和重叠策略。
- 为抓取 API、LLM 摘要和发邮件分别设 attempt/step/workflow 超时。
- 把错误分成暂时、永久、业务拒绝和结果未知。
- 计算所有层的最坏调用次数,并将其收敛到可解释上限。
- 给出队列年龄、重试预算和供应商配额的告警条件。
自测
- attempt 超时为什么不能证明下游未执行?
- jitter 解决什么问题,为什么仍需要总 deadline?
- 队列长度稳定是否足以证明没有积压风险?
- 重试预算与单请求最大 attempt 有什么区别?
下一步
继续 持久状态、恢复与幂等。
参考资料
- RFC 3339(RFC 9557 对其有更新,访问于 2026-07-22)
- RFC 9110:HTTP Semantics
- Microsoft:Transient Fault Handling(访问于 2026-07-22)
- Microsoft:Retry Storm Antipattern(访问于 2026-07-22)
- Microsoft:Queue-Based Load Leveling(访问于 2026-07-22)