代理请求失败后,首先要判断的是:这个已获授权的任务,还有没有预算再试一次?只有操作可以安全重放、故障可能是暂时的,而且总次数、剩余时间与共享重试配额都允许,才进入下一次尝试。等待结束后仍需重新检查,不能直接发送。

本文面向持续运行公开数据采集、地域检查等授权任务的数据工程师和技术负责人,提供一套保守的决策方法与离线 Python 工具。所有数字都是演算假设,不是 IPHTML 实测数据、生产推荐参数或产品限额。

一、先分类,别把所有失败都交给重试

状态码本身不能证明问题出在客户端、代理还是目标网站。先记录响应来源和任务上下文,再决定是否调整代理配置。本文工具采用以下有限范围。

观察到的结果本文采用的决策下一步
502、503、504 或传输超时可以进入预算判断,不代表自动重试确认访问授权、重放安全性,以及故障可能暂时恢复,再检查三项预算。
429停止当前任务的普通重试路径在受影响的目标范围内协调暂停,遵守 Retry-After,复核来源限额后再考虑调度。
403 或挑战页面停止核对访问许可与支持的接入方式,不通过更换出口绕过限制。
407停止先解决代理认证问题。
返回 200,但内容缺失或不合格停止传输层重试检查目标页面、解析器与验收规则,再拿到一次 200 也可能仍是错误内容。
TLS 校验失败或无法识别的结果停止并诊断检查信任链和配置,不以关闭证书校验换取“成功”。

RFC 6585 将 429 定义为限流,并允许响应携带 Retry-After,但没有规定唯一的计数范围。本文把目标范围共享暂停作为保守的协调方式;有服务方文档时应依照其范围处理。没有可用等待时间或明确恢复规则时,应由负责人解决暂停原因,不能让任务反复回队列碰运气。

工具只接受 GET、HEAD,并要求调用方明确确认允许重试和允许访问。这比 HTTP 定义的全部幂等方法更窄。RFC 9110 的幂等语义说明了重放判断的依据。不要把这段规则用于支付、上传或创建资源:请求超时不代表服务端没有完成操作。

二、首次请求前,写清三道预算

  1. 单任务总尝试次数。本文 max_attempts=3 表示首次一次,最多再重试两次。任务转移到另一个 worker、重新入队或进程重启时,任务标识和计数应继续保留。
  2. 贯穿整个任务的截止时间。从最初开始计时,涵盖排队、连接、读取、退避等待和内容校验。下一次执行必须留出完整预算与正的余量。单次 socket 读取超时不等于这个期限;测量边界可参考代理延迟拆分方法。
  3. 共享重试配额。明确目标范围与统计窗口,由一个协调机制原子扣减。每个 worker 各自有 20 次,不等于整个任务群只有 20 次。来源速率限制与并发限制仍需另行遵守。

只让一层负责重试。增加循环之前,先检查 HTTP 库、SDK、队列和应用是否已经会自动重试。AWS 的重试指导说明了多层叠加带来的负载风险。下面的离线工具不会替你检测或关闭这些机制。

三、计算等待时间,不要把 Retry-After 截短

Retry-After 可以是非负整数秒,也可以是 HTTP 日期。秒数从收到响应时计算;日期则与带时区的响应接收时间比较,已经过去的日期不增加等待时间。遇到格式无法解析的值,本文工具直接停止,不猜测。

剩余时间 = 任务期限 - 已用时间
退避上限 = min(退避封顶值, 基础等待 * 2 ** (已尝试次数 - 1))
抖动等待 = 0 到 1 之间的随机值 * 退避上限
本次等待 = max(抖动等待, 服务端要求的等待)

只有同时满足以下条件,才返回 WAIT_THEN_RECHECK:
  已尝试次数 < 总尝试次数上限
  共享重试配额 > 0
  本次等待 + 下一次完整执行预算 < 剩余时间

这里的全区间随机退避是本文选择的策略。封顶值只约束自己增加的退避,不能压缩服务端给出的等待时间。实际调度器应保存一个明确的 not_before 时间点,不能每次轮询都重新从头计算同一条相对秒数。任务耗时使用单调时钟,HTTP 日期转换单独处理;时钟误差不明时保守暂停。

假设一个获准访问的 GET 在首次尝试后返回 503:任务期限 20 秒,已经用掉 4 秒,下一次完整执行需要预留 5 秒;基础等待 1 秒,退避封顶 4 秒,随机值 0.5,Retry-After 为 6 秒。总尝试上限为 3,且仍有共享配额。抖动等待为 0.5 秒,实际等待取 6 秒;剩余 16 秒,等待加执行需要 11 秒,还有 5 秒余量,因此返回 WAIT_THEN_RECHECK。

只改变其中一项计算结果
已经用掉 12 秒剩余 8 秒,需要 6 + 5 = 11 秒停止:时间不够
已经用掉 9 秒剩余 11 秒,正好需要 11 秒,没有余量停止:执行严格期限规则
Retry-After 为 12 秒仍需等待 12 秒,不能截成 4 秒;12 + 5 超过剩余 16 秒停止:不能压缩服务端等待
已尝试 3 次,或共享配额已耗尽其他预算已经用完即使还有时间,也停止
状态码改成 429交给共享暂停机制处理停止当前重试路径

四、用离线工具复算

下载重试预算 Python 工具。它只使用标准库,不发送网络请求,也不实际等待。先执行合成用例和演示:

python3 proxy-retry-budget.py --self-test
python3 proxy-retry-budget.py --demo

内置演示使用另一组简单数据:剩余 19 秒,服务端日期要求再等 5 秒,下一次执行预留 3 秒,输出为 WAIT_THEN_RECHECK、等待 5 秒、余量 11 秒。要复算上文案例,把下面内容保存为 check_retry.py,与下载文件放在同一个文件夹:

import runpy

planner = runpy.run_path("proxy-retry-budget.py")["plan_next_attempt"]
inputs = dict(
    method="GET", outcome=503, attempts_used=1, max_attempts=3,
    elapsed_seconds=4, deadline_seconds=20,
    per_attempt_timeout_seconds=5, remaining_retry_tokens=20,
    retry_allowed=True, access_allowed=True,
    jitter_draw=0.5, base_backoff_seconds=1,
    backoff_cap_seconds=4, retry_after="6",
)
print(planner(**inputs))
inputs["elapsed_seconds"] = 12
print(planner(**inputs))

执行 python3 check_retry.py,第一个结果等待 6 秒、余量 5 秒;第二个结果因期限不足而停止。这只是根据输入做决策,不证明真实请求能在 5 秒内完成。工具不预占配额、不执行请求、不控制并发,也不取消正在运行的任务。输出里的预计剩余配额只是推演值,必须等协调器实际预留后才算数。

五、把单任务上限变成整批任务的上限

假设一批有 100 个任务,每个任务首次执行一次,最多总共尝试三次。如果只有单任务上限,最多可能执行 300 次顶层尝试。若给整批任务共享 20 个重试配额,且本批执行期间不补充,那么上限变为 100 + 20 = 120 次。

这依赖统一的原子计数、稳定任务标识以及没有隐藏重试层。它不等于字节或金额上限:响应大小、重定向和浏览器子请求都可能增加工作量。必须定义清楚“计一次”的单位,把额外请求单独纳入统计,有需要时再增加流量与支出限制。用有效结果成本模型复核实际花费,不要默认每次尝试成本一样。

429 延后队列同样需要最长任务年龄和明确负责人。是否把一次延后计入重试次数,可以按系统设计决定;但重置截止时间、悄悄生成新任务,会使预算失效。任务过期就记录停止原因结束,不能无限换身份重新排队。

六、扩量之前,检查真实执行链路

  • 等待结束时,再核对授权、共享暂停状态、速率与并发限制、剩余期限和次数;真正派发前原子预留一个重试配额。
  • 执行器必须能覆盖完整的单次执行预算,包括校验与清理。AWS 对超时边界的说明提醒我们,连接或 socket 参数未必覆盖全部工作。
  • 保存脱敏任务编号、尝试序号、结果类型、响应接收时间、最早再执行时间、截止时间和最终停止原因,不记录代理密码、认证头或页面中的个人信息。
  • 在少量获授权样本中,比较重试找回的有效任务、额外请求、超期数量与成本。前后沿用同一份可靠性计分卡定义,不能通过放宽成功规则美化结果。

如果失败集中在授权、解析或来源不支持的访问方式,官方 API、授权数据源或流程修正可能比增加代理容量更合适。如果任务要求出口保持一致,恢复设计还应遵守会话保持条件。

下一步:先整理失败任务证据,再对照轮换代理产品核查是否匹配,并通过现有联系入口确认当前约束和会话行为。提供脱敏时间段、受影响地区、尝试次数与停止原因即可。只有授权工作负载通过自己的验收标准,才考虑购买或扩容;这个离线工具不构成 IPHTML 速度、容量或支持响应时间的保证。