用aiohttp并发请求需三重防护:clienttimeout控单请求生命周期,tcpconnector管连接复用,asyncio.semaphore限并发数,缺一不可;裸用asyncio.gather易致端口耗尽、服务拒绝或任务阻塞。

直接说结论:用 aiohttp.ClientSession + asyncio.gather 并发发请求,比同步快 50 倍以上;但不加限制会触发连接数爆炸、目标服务器拒绝或本地端口耗尽——关键不是“怎么并发”,而是“怎么安全地并发”。
为什么不能裸用 asyncio.gather 发 1000 个请求?
看似简单的一行 await asyncio.gather(*[fetch(session, u) for u in urls]),实际会瞬间发起全部请求。问题立刻暴露:
- 操作系统默认单机最多约 65535 个可用端口,1000 个并发 TCP 连接可能撞上限(尤其短连接+高频率)
- 目标服务端通常有连接速率限制(如 nginx 的
limit_req),没节制的请求会被 429 或直接断连 -
aiohttp默认TCPConnector(limit=100),但这是连接池上限,不是并发请求数上限——它允许你同时发起远超 100 个请求,只是复用连接而已 - 没设超时,某个慢响应会拖垮整个批次(比如 DNS 卡住、TLS 握手失败)
必须配的三项:ClientTimeout、TCPConnector、asyncio.Semaphore
这三者分工明确,缺一不可:
-
ClientTimeout控制单个请求生命周期:aiohttp.ClientTimeout(total=10, connect=3, sock_read=7)—— 总超时 10 秒,建连最多 3 秒,读响应最多 7 秒 -
TCPConnector管理底层连接复用:aiohttp.TCPConnector(limit=300, limit_per_host=30, ttl_dns_cache=300)—— 全局最多 300 连接,单域名限 30 连,DNS 缓存 5 分钟 -
asyncio.Semaphore(50)是真正的并发闸门:所有session.get()必须先async with sem:才能执行,硬控同时活跃请求数 ≤ 50
示例片段:
sem = asyncio.Semaphore(50)
timeout = aiohttp.ClientTimeout(total=10)
connector = aiohttp.TCPConnector(limit=300, limit_per_host=30)
async with aiohttp.ClientSession(
timeout=timeout,
connector=connector
) as session:
tasks = [fetch_with_sem(session, url, sem) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
fetch_with_sem 里必须处理哪些异常?
异步请求失败原因比同步多得多,常见且必须捕获的有:
-
aiohttp.ClientConnectorError:DNS 解析失败、目标地址不可达、SSL 证书错误(尤其自签名或过期) -
aiohttp.ServerDisconnectedError:服务端主动断连(常见于反爬或负载过高) -
asyncio.TimeoutError:超时已触发,但事件循环仍在等响应(注意:它不是ClientTimeout抛的,而是底层 transport 层) -
aiohttp.ClientResponseError:HTTP 状态码非 2xx/3xx,比如 403、429、502 ——response.raise_for_status()会抛这个 -
UnicodeDecodeError:响应内容编码异常(尤其没声明 charset 的 HTML),建议用response.content.read()+chardet探测
别用裸 except Exception,否则会吞掉 KeyboardInterrupt(Ctrl+C 失效)和 CancelledError(任务被取消时的正常信号)。
容易被忽略的细节:DNS 缓存、SSL 验证、User-Agent
这些不起眼的配置,常导致线上行为和本地测试不一致:
- DNS 缓存默认关闭,高并发下反复解析同一域名会放大延迟。开
ttl_dns_cache=300后,首次解析后 5 分钟内复用结果 - 生产环境若访问内部 HTTPS 服务(如自签证书),得关 SSL 验证:
connector = aiohttp.TCPConnector(ssl=False);但公网请求绝不能关 - 不带
User-Agent的请求大概率被 Nginx 或 Cloudflare 拦截为机器人。固定 header 不够,建议轮换几个合法 UA 字符串 - 如果目标服务依赖 cookies 或 session 维持状态,
ClientSession必须复用(不能每个请求 new 一个),否则 cookie 不生效
最后提醒一句:aiohttp 的性能优势只在 I/O 密集场景成立。如果你的 response.text() 后还要做大量 CPU 计算(比如解析巨量 JSON、图像处理),记得用 loop.run_in_executor 脱离事件循环,否则会阻塞整个协程调度。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











