调度、超时、重试与背压

本节目标

为时间触发和外部调用定义清晰的时间语义,并用超时、有限重试、重试预算、限流和背压避免重复执行与故障放大。

时间语义必须完整

“每天 9 点运行”至少缺少:

  • 时区,例如 Asia/Shanghai;
  • 夏令时出现重复或缺失本地时间时怎么办;
  • 系统停机错过三次后逐次补跑、合并一次还是跳过;
  • 前一次尚未完成时允许重叠、跳过还是串行等待;
  • 允许延迟多久,超过什么 deadline 就没有业务价值。

RFC 3339 表示的是带 UTC 关系的时间戳。它适合记录“某个瞬间”,但不能单独表达“每个地区工作日 09:00”这样的日历规则;调度还需保存 IANA 时区名和业务日历。

定时触发去重键可用结构化 (schedule_id, logical_fire_time)。不要用 worker 实际醒来的当前时间,也不要用未转义连接符拼成字符串,否则停机补跑、延迟触发或字段字符组合会被误认为新任务。还应区分该逻辑周期的 operation_id 与每次领取/投递的 attempt_id:补跑、broker redelivery 和 worker 接管都可能增加 attempt,但不能重新定义同一个业务周期。

五种常见超时

  1. 队列等待超时:任务还未被 worker 领取。
  2. 单次 attempt 超时:一次网络/工具调用最多等待多久。
  3. 步骤总超时:包含所有 attempt 与退避。
  4. 外部事件/审批等待期限:到期转拒绝、升级或人工处理。
  5. 工作流总 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 的报告任务:

  1. 写出 logical fire time、停机三天后的补跑策略和重叠策略。
  2. 为抓取 API、LLM 摘要和发邮件分别设 attempt/step/workflow 超时。
  3. 把错误分成暂时、永久、业务拒绝和结果未知。
  4. 计算所有层的最坏调用次数,并将其收敛到可解释上限。
  5. 给出队列年龄、重试预算和供应商配额的告警条件。

自测

  1. attempt 超时为什么不能证明下游未执行?
  2. jitter 解决什么问题,为什么仍需要总 deadline?
  3. 队列长度稳定是否足以证明没有积压风险?
  4. 重试预算与单请求最大 attempt 有什么区别?

下一步

继续 持久状态、恢复与幂等。

参考资料