遇到429应使用asyncio.sleep()配合指数退避和retry-after头解析,结合域名级semaphore限流,并记录url、状态码、退避来源、重试次数等日志。

遇到429 Too Many Requests时别急着加time.sleep()
直接在协程里塞time.sleep()会阻塞整个事件循环,所有并发请求都卡住——这不是降频,是自废武功。真正该做的是让当前任务让出控制权,同时不影响其他任务运行。
正确做法是用asyncio.sleep()配合指数退避(exponential backoff),并把重试逻辑封装进可复用的装饰器或工具函数中:
-
asyncio.sleep()是协程函数,只挂起当前 task,不阻塞 event loop - 首次重试延迟建议从
1秒起步,每次失败翻倍(如 1s → 2s → 4s),避免雪崩式重试 - 必须设最大重试次数(比如 3 次),否则可能无限等待
- 注意:有些网站返回
429时会在Retry-After响应头里给出推荐等待秒数,优先读这个值
用aiohttp.ClientSession时如何自动提取Retry-After头?
很多反爬服务(如 GitHub API、Cloudflare)会在 429 响应里带 Retry-After: 5 或 Retry-After: Tue, 15 Oct 2024 12:34:56 GMT。手动解析容易漏格式,建议统一转成秒数再 sleep。
实操建议:
- 在
await session.get(...)后检查response.status == 429 - 读取
response.headers.get('Retry-After'),若为数字字符串(如"30"),直接转int;若为 HTTP 日期格式,用email.utils.parsedate_to_datetime()转时间戳后计算差值 - 不要依赖
Retry-After存在——它可能为空,此时回落到你的默认退避策略
如何避免多个协程同时撞上同一个限流窗口?
单机并发爬取时,多个 asyncio.create_task() 可能几乎同时发出请求,又几乎同时收到 429,然后一起退避、一起重试,形成“重试风暴”。这不是并发问题,是协调缺失。
解决思路不是降低并发数,而是引入轻量级令牌桶或信号量控制出口节奏:
- 用
asyncio.Semaphore(5)限制同一时间最多 5 个请求发出(注意:这是并发请求数,不是连接池大小) - 更精细的做法是按域名维度建独立
Semaphore,比如semaphores['example.com'] = asyncio.Semaphore(3),避免不同站点互相干扰 - 如果目标站明确要求每秒 N 次,可用
asyncio.Queue实现滑动窗口计数器,但多数场景下Semaphore+ 退避已足够
日志和监控不能只记“重试了”,得留证据
生产环境里,光打 logger.info("Retrying...") 没用。下次排查慢速或失败时,你根本不知道是哪个 URL、哪个 Host、第几次重试、等了多久。
关键字段必须记录:
- 原始请求 URL(
str(request.url)) - 响应状态码和
Retry-After值(即使为空) - 本次退避秒数(含来源:是头里读的,还是退避算法算的)
- 重试次数计数(
attempt=2/3这种格式) - 可选:当前
asyncio.current_task().get_name()辅助定位协程上下文
真正难的不是写重试逻辑,是让每次 429 都留下可追溯的行为痕迹——不然你以为在控频,其实只是把错误藏得更深了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











