用 Python requests 写带重试与超时的 HTTP 客户端实战
在日常开发中,调用第三方 HTTP 接口是最常见的任务之一。很多新手直接写 requests.get(url) 就上线,结果一遇到网络抖动、服务端偶发 500,程序就直接崩溃或无限挂起。本文结合一个真实的服务间调用场景,讲清楚超时与重试这两个最容易被忽视、却最关键的细节。
一、先说超时:别让一次请求拖垮整个服务
requests 默认没有超时时间。一次 requests.get(url) 如果遇到服务端假死、网络黑洞,调用方会一直等下去,线程池被占满后整个服务雪崩。正确做法是每次请求都显式传 timeout,而且要区分连接超时和读取超时:
# 连接超时 3 秒,读取超时 10 秒
resp = requests.get(url, timeout=(3, 10))
连接超时管的是 TCP 建连和 TLS 握手阶段,读取超时管的是发出请求后等待首字节的时间。把两者分开写,比只写一个数字更能反映真实情况:内网服务建连通常很快,读取超时可以放宽;跨公网调用则相反。
二、重试:不是简单地套个 for 循环
网络抖动、网关 502/503、连接重置,这类瞬时故障重试一次往往就能成功。但重试不能无脑循环,要遵守三条原则:只重试可重试的错误、限制次数、带退避间隔。requests 配合 urllib3 的 Retry 是标准做法:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=3,
backoff_factor=0.5,
status_forcelist=[502, 503, 504],
allowed_methods=["GET", "HEAD", "OPTIONS"],
)
adapter = HTTPAdapter(max_retries=retry)
session = requests.Session()
session.mount("https://", adapter)
session.mount("http://", adapter)
resp = session.get("https://api.example.com/data", timeout=(3, 10))
resp.raise_for_status()
backoff_factor=0.5 意味着重试间隔按 0.5 秒、1 秒、2 秒指数退避,避免在服务端已经过载时继续雪上加霜。status_forcelist 只对 502、503、504 这类明确的服务端瞬时故障重试,404、400 这种客户端错误重试没有意义。
三、只对幂等请求自动重试
这是最容易踩的坑。GET、HEAD、PUT、DELETE 是幂等的,重试安全;POST 下单、扣款这类非幂等操作一旦重试,可能造成重复下单。上面的 allowed_methods 只放了 GET、HEAD、OPTIONS,就是这个原因。如果业务必须对 POST 做重试,一定要让服务端支持幂等键:
import uuid
headers = {"Idempotency-Key": str(uuid.uuid4())}
session.post(url, json=payload, headers=headers, timeout=(3, 10))
服务端用幂等键去重,即使请求真的发了两次,也只会执行一次。
四、生产环境还要补齐的三件事
- 日志:每次重试都记录日志,包括第几次重试、失败原因、耗时,方便事后定位是网络问题还是服务端问题。
- 熔断:连续失败达到阈值就短暂熔断,直接快速失败,别再拿请求去压已经出问题的下游。
- 会话复用:用
Session复用 TCP 连接,减少每次建连的开销;同时设置连接池大小,避免并发一高就排队。
结语
超时和重试看起来只是两个参数,背后却是可用性工程的基本功:超时保护自己,重试包容偶发故障,幂等保证重试安全。把这三点写进团队的 HTTP 客户端封装里,下次再遇到网络抖动,至少不会是半夜被叫醒的那种故障。
- 点赞
- 收藏
- 关注作者
评论(0)