Python requests 重试与超时:给接口调用加三层保护
调用第三方接口时,最常见的故障有三类:网络抖动、服务端短暂过载、DNS 解析偶发失败。没有保护的代码往往在第一次异常时就直接崩掉,或者更糟——无限期挂住,拖垮整个服务。本文给 requests 调用加三层保护:超时、重试、熔断与总时限,三层分工不同,互相配合。
第一层:超时——先给每一次等待设上限
requests 默认没有超时。一次调用可能在连接阶段就挂住,也可能在等响应体时一直等下去。正确做法是把连接超时和读取超时分开设置:
import requests
resp = requests.get(url, timeout=(3, 10))
timeout=(3, 10) 表示建立连接最多等 3 秒,读取响应最多等 10 秒。只传一个数字时,连接和读取共用同一个上限,容易把响应慢但正常的接口误判为超时。超时值不要拍脑袋定:按接口的 P99 耗时来设,内部服务可以短一些,跨公网的第三方接口要留足余量。
第二层:重试——只对值得重试的失败重试
不是所有失败都值得重试。连接异常、502、503、504 这类“可能只是暂时的”失败可以重试;400、401、404、422 这类请求本身有问题的失败,重试多少次结果都一样。用 urllib3 的 Retry 配合 HTTPAdapter 可以把规则写清楚:
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=3,
backoff_factor=1,
status_forcelist=[502, 503, 504],
allowed_methods=["GET", "HEAD"],
)
adapter = HTTPAdapter(max_retries=retry)
session = requests.Session()
session.mount("https://", adapter)
session.mount("http://", adapter)
退避因子设为 1 时,重试间隔大约是 0、2、4 秒,指数退避既给了服务端恢复时间,又不会把调用方拖垮。重试次数不要设得太高:下游已经过载时,大量客户端同时重试就是二次冲击。
POST 等非幂等请求默认不要重试,因为不知道第一次请求是否已经生效。如果确实需要重试写操作,先确认接口支持幂等键,再在请求头里带上 Idempotency-Key,让服务端能识别重复提交。
第三层:熔断与总时限——别让一次故障拖垮整个服务
重试只能应对短暂故障。如果下游持续不可用,继续重试只会耗尽连接池和线程。这时需要两道防线:一是给整个操作设总时限,二是简易熔断。
import time
import requests
deadline = time.monotonic() + 30 # 整个操作最多 30 秒
while time.monotonic() < deadline:
try:
resp = session.get(url, timeout=(3, 10))
resp.raise_for_status()
break
except requests.RequestException:
if time.monotonic() >= deadline:
raise
time.sleep(1)
总时限保证无论重试多少次,调用都不会超过 30 秒。熔断则是在此基础上再进一步:记录连续失败次数,达到阈值后在一段时间内直接拒绝请求,而不是每次都去撞墙;等冷却期过了再放少量请求探路,成功了恢复,失败了继续熔断。
最后别忘了可观测性:把超时、重试次数、最终失败率记到日志和监控里。没有这些数字,超时和重试参数永远只能靠猜,也没法判断某次故障到底是网络问题还是下游真的挂了。
三层如何配合
三层不是互相替代,而是分工:超时保证单次调用不挂死;重试消化短暂抖动;熔断和总时限守住系统整体。落地时建议:超时按接口 P99 设定、重试只覆盖幂等请求和可恢复状态码、熔断阈值先宽后严、所有参数做成配置项而不是写死在代码里。接口调用的健壮性不是某一个参数,而是一组互相配合的边界。把这三层加好,绝大多数偶发故障都会在用户感知到之前被消化掉。
- 点赞
- 收藏
- 关注作者
评论(0)