Python aiohttp.ClientSession 的生命周期管理

3次阅读

clientsession 必须显式关闭,因其持有连接池、dns缓存等异步资源,gc不会调用close导致文件描述符泄漏;须用async with或显式await session.close(),且不可跨Event loop复用。

Python aiohttp.ClientSession 的生命周期管理

ClientSession 必须显式关闭,不能靠 GC 回收

aiohttp 的 ClientSession 不是普通对象,它内部持有连接池、DNS 缓存、ssl 上下文等异步资源。python 的垃圾回收(GC)不会主动调用其 __aexit__close(),导致 TCP 连接不释放、文件描述符泄漏、后续请求卡死或报 RuntimeError: Session is closed

常见错误现象:脚本跑几次就报 OSError: [errno 24] Too many open files;或者在 asyncio.run() 结束后还看到未完成的连接日志。

  • 必须用 async with ClientSession() as session: 包裹使用范围
  • 若需跨多个协程复用 session(如全局 session),务必在程序退出前显式 await session.close()
  • 不要在函数里返回未关闭的 session 给调用方——责任边界要清晰

复用 session 是必须的,但不能跨 event loop

一个 ClientSession 实例绑定到创建它的 asyncio event loop。如果在不同 loop(比如新线程里新建 loop、或 pytest 中反复启停 loop)中复用旧 session,会直接抛 RuntimeError: Session is closed 或静默失败。

使用场景:Web 服务(如 fastapi)中常把 session 当作依赖注入;命令行工具中想避免每次请求都新建 session。

立即学习Python免费学习笔记(深入)”;

  • FastAPI 等框架推荐用 lifespan hook,在 startup 时创建 session,shutdown 时 close
  • 测试时别在 setUp 里建 session,改用 fixture + yield + await session.close()
  • 绝对不要把 session 存在模块级变量里然后跨 test case 复用

timeout 和 connector 参数影响连接行为远超预期

ClientSession 的 timeout 控制的是单次请求生命周期,而 connector(如 TCPConnector)控制的是底层连接池策略。两者配合不当会导致超时逻辑混乱、连接复用失效、甚至 DNS 查询被缓存数分钟。

典型问题:设置了 timeout=5,但某个请求卡住 60 秒才报错;或并发请求量大时大量新建连接,触发服务器限流。

  • timeout 应设为 aiohttp.ClientTimeout(total=30, connect=5, sock_read=10),避免只设 total 导致 DNS 解析失败也计入超时
  • TCPConnector(limit=100, limit_per_host=30, ttl_dns_cache=300) —— 默认 limit=100 看似够用,但 limit_per_host=0(即不限)才是旧版默认值,升级后容易突然变慢
  • DNS 缓存默认 10 分钟,若后端做滚动发布,建议显式设 ttl_dns_cache=60

session 关闭后仍可能有 pending request 报错

关闭 session 并不等于立刻终止所有进行中的请求。如果在 await session.close() 前已有 await session.get(...) 发出但尚未收到响应,这些 task 仍会继续运行,并在读取响应体时抛 ClientConnectionErrorCancelledError

这不是 bug,而是 asyncio 的正常调度行为:close 只是拒绝新请求、清理空闲连接,不强制 cancel 正在传输的 request。

  • 关键点:关闭 session 前,确保所有已发请求已完成或已被 cancel
  • 安全做法是在 shutdown 逻辑里用 asyncio.shield() 包裹关键请求,或用 asyncio.wait_for(task, timeout=...) 控制等待时间
  • 别依赖 “关 session 就万事大吉”——网络 IO 的不确定性始终存在

session 生命周期最麻烦的地方不在怎么开,而在怎么确认它真关了:连接池清空、DNS 缓存失效、所有 pending task 归零。这些状态没法靠 print 看出来,得靠 lsof -i | grep python 或 asyncio debug 模式验证。

text=ZqhQzanResources