一次超时之后,客户端重试;网关也重试;上游 SDK 再重试。表面上只处理一个问题,后台可能已经出现多份相同请求。生成任务又往往耗时长,旧请求即使失去连接,也可能继续占着推理资源。
因此重试策略不能只写“最多三次”。先明确谁负责重试、哪些失败允许重试、全链路还剩多少时间,以及重复请求如何关联。缓存复用和路由能降低部分工作,但不会自动提供幂等语义。Dynamo 的路由与调度范围也应与应用层的请求去重区分开。
把总时间预算带过每一次尝试
下面是纯离线决策器:输入预先给定的结果和耗时,不发网络请求、不 sleep。request_id标识同一个业务请求,attempt_id区分它的各次尝试。服务端要实现去重,还需要自己的持久或有界记录,不能只靠传一个字符串。
from dataclasses import dataclass @dataclass(frozen=True) class Result: status: int elapsed_ms: int RETRYABLE = {429, 503} def replay(request_id, outcomes, budget_ms, backoff_ms=10, max_attempts=3): if budget_ms <= 0 or backoff_ms < 0 or max_attempts < 1: raise ValueError("invalid policy") used, trace = 0, [] for i, result in enumerate(outcomes[:max_attempts], 1): if result.elapsed_ms < 0 or used + result.elapsed_ms > budget_ms: return "deadline", trace used += result.elapsed_ms trace.append((request_id, i, result.status, used)) if 200 <= result.status < 300: return "success", trace if result.status not in RETRYABLE: return "stop", trace if i == max_attempts or used + backoff_ms >= budget_ms: return "exhausted", trace used += backoff_ms return "no_more_results", trace state, trace = replay("r1", [Result(503, 20), Result(200, 30)], 100) assert state == "success" and trace[-1][-1] == 60 assert replay("r2", [Result(400, 5), Result(200, 5)], 100)[0] == "stop" assert replay("r3", [Result(503, 20)], 30)[0] == "exhausted" print(state, trace)示例把 429 和 503 作为可重试状态,仅说明策略形式;真实接口可能要求读取Retry-After,某些请求即使收到这两种状态也不适合立即重放。流式输出已经提交一部分时,更不能把新响应直接接到旧文本后面。
预算还需要在群体层面受控
每条请求最多三次,在所有请求同时失败时仍可能造成三倍尝试。网关通常还需要总重试额度、并发上限与随机退避。随机抖动应来自独立分布,避免所有客户端在同一个固定间隔重新冲向后端。
超时不是“上游确定没执行”。晚到响应与取消消息需要用 attempt 身份核对,不能误释放新一轮的资源预留。若业务有外部副作用,幂等键的保留时间、结果缓存和冲突行为必须由接口契约定义。
这里没有实现 HTTP 客户端或生产级重试库,也未验证服务端取消、去重和网络故障。它能检查有限预算下的决策次序,不能证明重试带来了更高成功率。上线前应同时统计原始请求数、尝试数、有效完成数和被取消后仍消耗的工作。