asyncio.semaphore仅控制并发数量,无法防止瞬时请求脉冲;需叠加请求级随机延迟、ip/会话隔离及连接层优化,才能真正规避服务端429限流。

直接用 asyncio.Semaphore 控制并发数,再配合 time.sleep() 或退火式延迟,是最简单有效的第一道防线。但仅靠这个远远不够——瞬时高并发冲击真正危险的地方在于:它可能绕过本地限流,直接打穿服务端令牌桶或 Redis 计数器的窗口边界。
为什么固定并发数挡不住瞬时冲击
很多爬虫写 asyncio.Semaphore(10) 就以为稳了,结果上线后还是被 429 打爆。问题出在:Semaphore 只管“同时发起多少请求”,不管“这些请求是否集中在 100ms 内到达”。比如 10 个协程几乎同时 await session.get(),服务端看到的就是 10 个请求在毫秒级内抵达,哪怕你每秒只发 10 次,也等同于突发流量。
- 服务端限流(如令牌桶)通常按纳秒/毫秒粒度补令牌,但客户端并发请求没有自然间隔
- 网络调度、DNS 缓存、TCP 连接复用会让多个请求实际发出时间高度趋同
- 如果所有请求共用同一 IP + 同一 User-Agent,服务端风控会直接合并统计,放大冲击效果
必须加上的第二层:请求级错峰与 jitter
在每个请求前插入带随机扰动的等待,才能把“10 个请求瞬间砸过去”变成“10 个请求在 500ms 窗口内均匀散开”。这不是为了降低总量,而是把脉冲变成平滑波形。
- 不要用固定
time.sleep(0.1)—— 所有请求仍会同步苏醒,形成新脉冲 - 改用
await asyncio.sleep(random.uniform(0.05, 0.15)),让每个请求醒来时间不同 - 更稳妥的做法是结合上一次响应耗时:如果上次请求耗时
latency,下一次至少等max(0.1, latency * 1.5) - 对关键接口(如登录、搜索),额外增加
random.randint(100, 300) / 1000秒抖动,防模式识别
绕不过去的第三层:IP 和会话隔离
即使你本地做了完美错峰,所有请求走同一个出口 IP,服务端依然能一眼识别为机器行为。瞬时冲击的本质是“单点资源集中释放”,解法就是分散源头。
- 别只依赖一个代理 IP;用池化管理,按
70% 生命周期主动轮换(比如 20s 有效期的 IP,在 14s 后就标记为待替换) - 每个 IP 绑定独立
aiohttp.ClientSession,并设置不同headers['User-Agent']和cookies - 避免跨 IP 复用同一 token —— 会话级限流会把你所有 IP 归为同一实体
- Redis 限流 key 必须包含 IP + 接口路径 +(可选)token 前缀,否则计数失效
容易被忽略的致命细节
很多人调好并发和延迟就上线,结果跑两小时突然全量 429。问题往往出在连接复用和 DNS 缓存上:aiohttp 默认复用 TCP 连接,而服务端可能对单个连接做 per-connection 限流;本地 DNS 缓存又会让所有请求打到同一台后端实例。
- 显式关闭连接复用:
aiohttp.TCPConnector(limit=0, keepalive_timeout=0) - 禁用 DNS 缓存:
aiohttp.TCPConnector(use_dns_cache=False) - 检查服务端返回的
X-RateLimit-Remaining头,动态调整当前 IP 的并发上限,而不是硬编码 - 429 响应里如果有
Retry-After,必须严格遵守,且该 IP 后续 5 分钟内自动降权
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











