aiohttp.ClientSession 必须复用,因新建会重复初始化连接池、SSL 上下文并绑定事件循环,导致开销大、RuntimeError、连接泄漏及文件描述符耗尽;应全局单例创建,用 async with 包裹单次请求。

为什么 aiohttp.ClientSession 必须复用,不能每次请求都新建
因为新建 ClientSession 会重新初始化连接池、SSL 上下文和事件循环绑定,不仅开销大,还会导致 RuntimeError: Session is closed 或连接泄漏。尤其在高并发场景下,频繁创建销毁 session 容易触发文件描述符耗尽(OSError: [Errno 24] Too many open files)。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 全局或作用域内只创建一次
ClientSession,用async with仅包裹单次请求逻辑,不是整个 session 生命周期 - 若需定制超时或 headers,统一在 session 初始化时传入
timeout和headers参数,而非每个session.get()都重复指定 - 在 Web 应用(如 FastAPI)中,通过 lifespan hook 或依赖注入管理 session 生命周期,避免在路由函数里现场 new
aiohttp 中如何正确处理 JSON 响应和编码问题
直接调用 response.json() 看似方便,但默认不校验 Content-Type,且对非 UTF-8 编码(如 GBK、ISO-8859-1)会抛 UnicodeDecodeError。更隐蔽的问题是:某些 API 返回 text/plain 但内容实为 JSON 字符串,此时 .json() 会静默失败或解析出错。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 优先用
await response.text(encoding="utf-8")拿原始字符串,再用json.loads()显式解析,便于捕获和调试格式错误 - 若响应头含
charset=gbk,必须显式传 encoding:await response.text(encoding="gbk"),不能依赖自动探测 - 对不确定 content-type 的接口,先打印
response.content_type和response.charset调试,再决定解析路径
并发请求控制:用 asyncio.Semaphore 还是 aiohttp.TCPConnector 的 limit
两者都管并发,但作用层级不同:TCPConnector(limit=10) 控制的是底层 TCP 连接数上限,影响复用和端口占用;Semaphore 控制的是协程并发数,影响任务调度节奏。混用时若配置冲突(比如 semaphore=5 但 connector.limit=100),实际并发仍受更严的那个限制。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 优先用
TCPConnector(limit=20, limit_per_host=5)—— 它能真正防止对单个 host 建连风暴,比纯协程限流更贴近网络层实际压力 - 需要业务级节流(如每秒最多调 3 次某 API)时,才叠加
Semaphore,且注意把它定义在 session 外部,避免每次请求都 new 一个新实例 - 不要设
limit=0(无限制),Linux 默认单进程最大 socket 数约 1024,超出会报OSError: [Errno 24]
异常捕获必须覆盖的三个关键点
aiohttp 的异常分散在多个层级:DNS 解析失败(aiohttp.ClientConnectorError)、连接超时(asyncio.TimeoutError)、HTTP 状态码非 2xx(response.raise_for_status() 抛 aiohttp.ClientResponseError)。漏掉任意一类,都会让程序意外中断。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 外层用
try/except (aiohttp.ClientError, asyncio.TimeoutError) as e:捕获网络层异常 - 内层对每个
response显式调用response.raise_for_status(),否则 404、502 等状态码不会抛异常,容易误判为成功 - 记录异常时务必包含
str(e)和response.url if "response" in locals() else None,否则排查时不知道失败发生在哪个 URL










