aiohttp 连接池:那个 limit 参数到底在限什么

aiohttp 连接池:那个 limit 参数到底在限什么

aiohttp 文档里有一行参数:TCPConnector(limit=100)。很多人把它当成"并发上限"用,觉得设了 100 就是"最多 100 个请求并发"。结果跑起来发现,要么被对面服务拉黑,要么 CPU 暴涨,要么直接 OOM。

问题在于,limit 限的是连接数,不是协程数。这两者不是一回事。

连接池到底是什么

先说连接池是什么。HTTP 请求底层是 TCP 连接。每次新建连接都要走三次握手,慢启动还会吃一段性能。如果业务上要发很多请求,连接池就出现了------它把建好的连接"留着",下次用的时候直接拿,省掉握手和慢启动的开销。

aiohttp 的 TCPConnector 就是这个东西的体现。limit=100 意思是这个连接池最多持有 100 个连接。当某个协程发请求时,aiohttp 会从池里拿一个空闲连接给它用;请求结束后,连接不会被销毁,而是放回池里,等下一个协程用。

听起来很完美,对吧。但这里有个隐含前提------"池里有空闲连接可用"。

当协程数超过连接数会怎样

假设你起了 500 个协程,但连接池上限是 100,会发生什么?

前 100 个协程拿到连接,开始跑。后 400 个协程呢?它们不会消失,而是去等待一个叫 acquire 的 future,等池里有连接释放出来自己就能拿。

听起来也合理。但实际上,这 400 个协程并不是"在 IO 上阻塞"------它们仍然在事件循环里挂着,每个都是一个 task 对象,持有自己的状态机和 future。事件循环在每个 tick 都要去检查这 400 个 future 的状态,这本身就是开销。

更糟的是这 400 个协程都是"瞬时塞进去的"。比如你写 asyncio.gather(*[fetch(url) for url in urls]),500 个 task 瞬间被 create,然后一起跑向连接池。结果是:

  • 100 个拿到了连接
  • 400 个在 acquire 上挂着
  • 事件循环调度成本瞬间爆表
  • 100 个跑完后释放连接,400 个一起涌上来,瞬时并发 400

这个"瞬时并发 400"才是真正打爆对面服务的原因。

limit 和 limit_per_host 的区别

aiohttp 的 TCPConnector 有两个看起来很像的参数:

  • limit:总连接数上限,跨所有 host
  • limit_per_host:每个 host 的连接数上限

默认 limit=100,limit_per_host=0(也就是不限制 per host)。

这两个参数什么时候会不一样?比如你的业务里有 5 个不同的 API 服务,你调 a.com 调得最多。如果你设 limit=100, limit_per_host=20,那 a.com 最多占 20 个连接,剩下 80 个连接给其他 host 用。这样可以防止某一个 host 吃光所有连接。

我之前做的一个项目就是这个问题------我只调一个服务,limit=100 完全够用。后来业务扩展到 3 个服务,没注意 limit_per_host,结果 a 服务把 100 个连接吃光了,b 和 c 拿不到连接直接超时。

合理做法:

  • 单 host 业务:limit 设成你需要的并发数
  • 多 host 业务:设 limit_per_host 防止某个 host 吃满

连接池满的时候,到底发生了什么

很多教程讲到这块都是一笔带过:"连接池满了会等"。但实际表现是什么样的?

当池里 100 个连接都被占用,新来的协程调 session.get(url),aiohttp 会在 acquire 这个 future 上 await。这个 await 是非阻塞的------它不会卡住事件循环,只是把这个协程挂起,等连接释放时被唤醒。

问题在于,事件循环里"被挂起的协程"也是有成本的。每个协程都是一个 task 对象,Python 用生成器实现 task,每个 task 持有栈帧、局部变量、future 链。这些全加起来,5000 个挂着的协程轻松吃掉 1-2 GB 内存。

我之前那次翻车就是这种情况:起了 5000 个协程去抢 100 个连接,结果 4900 个挂在 acquire 上,内存直接涨了 800 MB。

正确的限流方式

如果你要保证"最多 100 个请求并发",单纯设 limit=100 是不够的------它只能保证"最多 100 个连接被同时使用",但不能保证"最多 100 个协程在活跃"。

正确的做法是双层限流:

python 复制代码
connector = aiohttp.TCPConnector(limit=100)  # 连接池上限
sem = asyncio.Semaphore(100)                  # 协程并发上限

async def fetch(url, session):
    async with sem:                            # 协程先过 sem
        async with session.get(url) as resp:   # 再从连接池拿连接
            return await resp.text()

这样写:

  • sem 保证活跃协程数不超过 100
  • 连接池保证实际建立的连接数不超过 100
  • 超出 sem 的协程在 sem 上挂着,不进事件循环抢调度

但即便这样也有问题------gather 一瞬间把 5000 个 task 全 create,5000 个协程还是在内存里挂着。

更稳的做法是用 Queue 配合固定 worker:

python 复制代码
queue = asyncio.Queue(maxsize=200)

async def worker(session):
    while True:
        url = await queue.get()
        if url is None:
            queue.task_done()
            return
        try:
            async with session.get(url) as resp:
                await resp.text()
        finally:
            queue.task_done()

async def main(urls):
    async with aiohttp.ClientSession(connector=connector) as session:
        workers = [asyncio.create_task(worker(session)) for _ in range(100)]
        for url in urls:
            await queue.put(url)
        for _ in range(100):
            await queue.put(None)
        await asyncio.gather(*workers)

这套写法里,内存里只有 100 个 worker 协程 + Queue 里的待处理任务。Queue 有 maxsize,生产者满了会 await,内存不会爆。

几个我常用的排查命令

最后说几个排查连接池问题常用的命令:

python 复制代码
# 看当前活跃连接数
connector._acquired_per_host
# 看到底有多少协程挂着
len(asyncio.all_tasks())
# 看连接池具体状态
connector._acquired_per_host

第一个是 aiohttp 内部属性,第二个是 Python 3.8+ 的标准 API。出事的时候先看这两个,能快速判断是"连接不够"还是"协程太多"。

小结

把今天说的串一下:

  • aiohttp 的 limit 限的是连接数,不是协程数
  • 协程数 > 连接数时,超出部分会挂在 acquire 上,不会自动退出事件循环
  • 瞬时并发打爆对面服务是常见问题,gauge 数量不代表实际并发
  • 多 host 业务要设 limit_per_host
  • 想真正限流,配合 Semaphore + Queue 比单纯设 limit 更稳

搞清楚这些,aiohttp 才用得明白。不然就只是"看着像在用异步",实际上踩了一堆看不见的坑。

相关推荐
花间相见1 小时前
【计算基础|MySQL06】MySQL 架构与一条 SQL 的旅程
数据库·后端
Thneonl1 小时前
Spark 比 MR 快在哪?管住最贵的 shuffle
后端·架构
申君健24864182772022 小时前
AI SDK 概念全景总结(v7) 二
后端
申君健24864182772022 小时前
AI SDK 概念全景总结(v7)
后端
imDwAaY2 小时前
SQL 查询突然变慢了?一篇讲清慢 SQL 排查思路
后端·mysql
小羊没烦恼!2 小时前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
the局外人2 小时前
学会 SQLAlchemy 搭建 Python 与 MySQL 之间的数据访问桥梁 🚀
后端·python·sql
金銀銅鐵3 小时前
[Java] 用 GUI 展示 ArrayList 扩容的过程
java·后端·ai编程