真正稳定的并发控制在于连接生命周期、响应体处理和错误反馈闭环三处细节。固定Semaphore只控发起数不控活跃连接数,需用aiohttp.TCPConnector限制连接池(如limit=15、limit_per_host=4),并动态调整并发窗口,配合及时释放资源与合理日志。

并发数设太高,asyncio爬虫不是卡死就是内存爆掉,不是代码写错了,是没做动态节流。
为什么asyncio.Semaphore不能直接硬限并发数
很多人用asyncio.Semaphore锁住协程入口,比如设成10就以为稳了。但问题在于:目标网站响应时间波动大,有些请求耗时2秒,有些卡在DNS或TLS握手要8秒,固定值会导致大量协程在信号量外排队,内存持续上涨,event loop被拖慢,最终asyncio.TimeoutError或MemoryError爆发。
- 固定
Semaphore只控“同时发起数”,不控“同时活跃连接数” - HTTP/1.1连接复用下,一个TCP连接可能挂多个等待响应的请求
- 没释放
response.content或没调response.close(),连接池资源泄漏更隐蔽
用aiohttp.TCPConnector限制底层连接池
真正起作用的是连接池配置,不是上层协程数。默认aiohttp不限制连接总数,所有协程都往里塞,底层socket撑不住。
-
limit=20:整个session最多20个并发TCP连接(推荐从10起步) -
limit_per_host=5:对同一域名最多5个连接(防止单站打穿) -
keepalive_timeout=30:空闲连接保持30秒,避免频繁建连开销 - 必须配合
await response.read()或await response.text(),否则连接不释放
connector = aiohttp.TCPConnector(
limit=15,
limit_per_host=4,
keepalive_timeout=25,
force_close=False
)
async with aiohttp.ClientSession(connector=connector) as session:
# 后续请求自动受控
根据响应延迟动态缩放并发窗口
静态限速扛不住网络抖动。需要实时观测:平均RTT、失败率、连接排队时长,再调整Semaphore值。
立即学习“Python免费学习笔记(深入)”;
- 每100个请求统计一次:
avg_latency > 2.5s或503错误率 > 8%→ 并发数减半 - 连续3轮
avg_latency < 0.8s且无5xx → 并发数+2(上限不超过初始值×1.5) - 用
asyncio.Queue做缓冲队列,把URL推入队列,消费者协程按当前窗口大小拉取 - 避免用
time.sleep()在协程里硬等,改用await asyncio.sleep()并设超时
崩溃前最常被忽略的内存点
asyncio爬虫崩得悄无声息,往往不是CPU跑满,而是对象堆积在内存里不回收。
-
BeautifulSoup解析后不删soup变量,引用计数卡住,尤其用html.parser时更严重;换lxml可缓解 - 没用
async with response,导致response._connection一直挂着,连接池无法复用 - 日志打太密,比如每个请求都
logger.info(f"done {url}"),异步IO日志模块本身会成为瓶颈 - 全局变量存了大量
bytes或str缓存,没做LRU或定时清理
真正稳定的并发控制,不在协程数量上做文章,而在连接生命周期、响应体处理、错误反馈闭环这三处卡死细节。漏掉任意一环,动态调整都是假动作。


















