超时、重试与限流
约 1357 字大约 5 分钟
2026-05-10
网络是不可靠的。可靠的网络程序必须处理超时、重试和限流。
- 为什么必须设置超时
- 哪些错误适合重试
- 指数退避
- jitter
- 1timeout 永远要设:连接 + 读取分别控制(如 (3, 10)),不传超时会让进程被一个坏接口卡死。
- 2只重试临时性错误:网络抖动、连接超时、读取超时、502/503/504、带 Retry-After 的 429。
- 34xx(参数/认证/权限/404)重试 100 次也没用 —— 改请求本身或修业务逻辑。
- 4重试间隔用指数退避 + jitter:避免大量客户端同步重试造成雪崩。
- 5POST 重试前确认幂等:用 Idempotency-Key 头,或服务端有去重逻辑,否则可能下两单。
网络是不可靠的。可靠的网络程序必须处理超时、重试和限流。
为什么必须设置超时
如果不设置超时,请求可能无限等待。
requests.get(url) # 不推荐
requests.get(url, timeout=5) # 推荐可以区分连接超时和读取超时:
requests.get(url, timeout=(3, 10))哪些错误适合重试
适合重试的情况:
- 连接临时失败
- DNS 临时失败
- 读取超时
- 502、503、504
- 429 且服务端允许稍后重试
不适合盲目重试的情况:
- 400 参数错误
- 401 未认证
- 403 无权限
- 404 资源不存在
- 非幂等 POST 请求
指数退避
重试间隔不应该固定太短。指数退避会逐渐增加等待时间。
import time
for attempt in range(3):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
break
except requests.RequestException:
if attempt == 2:
raise
time.sleep(2 ** attempt)jitter
如果大量客户端同时重试,可能造成雪崩。jitter 是在等待时间上增加随机抖动。
import random
import time
delay = 2 ** attempt + random.random()
time.sleep(delay)Retry-After
遇到 429 或 503 时,服务端可能返回:
Retry-After: 30表示客户端应该等待 30 秒后再重试。
幂等性与重试风险
GET 请求通常可以重试。创建订单、扣款、发送短信这类 POST 请求不能随便重试,否则可能造成重复操作。
解决方式:
- 使用幂等键 Idempotency-Key
- 服务端去重
- 客户端只在明确安全时重试
限流
限流是控制请求频率,避免压垮自己或对方服务。
简单限流:
import time
for url in urls:
fetch(url)
time.sleep(0.5)复杂场景可以使用令牌桶、队列或异步 Semaphore。
timeout 不是“让请求更快”
timeout 的作用不是加速服务器,而是给你的程序一个“最多等多久”的边界。没有 timeout 时,如果网络卡住,程序可能一直挂在那里。
requests.get(url) # 不推荐:可能等很久
requests.get(url, timeout=5) # 推荐:最多等 5 秒左右更细一点可以写成:
requests.get(url, timeout=(3, 10))含义是:连接最多等 3 秒,连接成功后读取响应最多等 10 秒。
重试不是所有错误都适合
适合重试的通常是“临时问题”:
- 网络抖动;
- 连接超时;
- 读取超时;
- 服务器临时返回 502、503、504;
- 接口返回 429 并告诉你稍后再试。
不适合盲目重试的通常是“请求本身有问题”:
- 400 参数错误;
- 401 未登录;
- 403 没权限;
- 404 地址不存在;
- JSON 格式写错。
这些错误重试 100 次也大概率没用,应该改请求本身。
一个简单的手写重试例子
不用一开始就引入复杂库,可以先理解基本思路:
import time
import requests
def get_with_retry(url, retries=3):
for attempt in range(1, retries + 1):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response
except (requests.Timeout, requests.ConnectionError) as exc:
if attempt == retries:
raise
wait = 2 ** (attempt - 1)
print(f'第 {attempt} 次失败:{exc},{wait} 秒后重试')
time.sleep(wait)
response = get_with_retry('https://httpbin.org/get')
print(response.status_code)这里的等待时间是 1 秒、2 秒、4 秒,属于很简化的指数退避。
限流:别把对方服务打爆
限流可以理解为“控制请求速度”。即使你的电脑能一秒发很多请求,也不代表应该这么做。
简单脚本里可以先用 sleep:
import time
import requests
for page in range(1, 6):
response = requests.get(
'https://example.com/items',
params={'page': page},
timeout=5,
)
print(response.status_code)
time.sleep(1)如果接口文档写了“每分钟最多 60 次”,就要按文档限制来。遇到 429 Too Many Requests,通常表示你请求太快了。
给初学者的建议
先把规则定简单一点:
- 所有网络请求都设置 timeout;
- 只对超时、连接失败、502/503/504 做有限重试;
- POST 创建订单、扣款、发消息这类操作不要随便重试;
- 爬取页面或批量请求时加 sleep 或并发限制;
- 把最后失败的 URL、状态码和错误信息打印出来,方便排查。
总结
网络请求必须设置超时。重试要有最大次数、退避策略和幂等性判断。限流是对远程服务和自己程序的保护。
- timeout 是给程序设置等待上限,不是让服务器更快。
- 重试只适合临时网络问题或 502/503/504 等短暂服务异常,不要盲目重试参数和权限错误。
- 批量请求时要限流,既保护自己程序,也尊重对方服务。
版权所有
版权归属:Shuo Liu
