在基于 asyncio 的高并发网络编程里,aiohttp 是最常用的 HTTP 客户端库之一。很多线上故障并不是代码逻辑错误,而是客户端没有正确约束请求生命周期与并发连接数,导致事件循环被慢响应阻塞或者系统端口耗尽。要解决这类问题,核心就是理解并配置好全局超时与连接池。

一、全局超时的设置方式
aiohttp 从 3.x 开始使用 ClientTimeout 对象来统一管理超时,而不是像 requests 那样分 session、分请求去传 timeout 元组。这个对象可以定义多个阶段的时限,包括总时长、连接建立、服务器响应读取等。把它传给 ClientSession,就能实现真正的全局超时。
如果不显式设置,aiohttp 默认总超时是 5 分钟,但连接阶段实际依赖系统 socket 行为,很容易出现“卡死”现象。因此建议在创建 Session 时明确传入 ClientTimeout,让所有通过该 Session 发出的请求都遵循同一套时间约束。
import aiohttp
from aiohttp import ClientTimeout
# 定义全局超时:总时长10秒,连接2秒,读取5秒,写入2秒
timeout = ClientTimeout(
total=10,
connect=2,
sock_read=5,
sock_connect=2
)
async def main():
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get('https://ipipp.com/api') as resp:
return await resp.text()
上面的代码把超时对象绑定到 Session,之后无论是 get、post 还是其他请求方法,都不需要再单独传 timeout 参数。如果某个特殊接口需要更短或更长的时间,也可以在具体请求中覆盖,例如 session.get(url, timeout=ClientTimeout(total=3)),但这种局部设置应当尽量少用,以免破坏全局一致性。
需要注意的是,total 是包含连接、读取、写入的完整耗时上限;如果设置了 total 同时又单独设了 connect 等,则以先触发的为准。生产环境中推荐只设 total 和 connect,简化排查逻辑。
二、连接池大小与 TCPConnector
aiohttp 的连接池由 TCPConnector 管理。很多人以为异步就是无限并发,其实底层 TCP 连接依然受限制。Connector 的 limit 参数控制整个 Session 的总并发连接数,limit_per_host 则限制对单一 host 的最大连接,避免把某个域名打挂。
默认情况下 limit 是 100,limit_per_host 是 0(表示不限制,实际继承 limit)。在爬虫或聚合网关场景,如果不限制 per_host,很容易触发对方服务的限流;而在内部微服务调用中,过小的 limit 又会让事件循环空转等待连接。因此必须按业务调整。
import aiohttp
from aiohttp import TCPConnector, ClientTimeout
connector = TCPConnector(
limit=200, # 总连接池大小
limit_per_host=50, # 单 host 最大连接
ttl_dns_cache=300, # DNS 缓存时间,减少解析开销
enable_cleanup_closed=True # 自动清理异常关闭连接
)
timeout = ClientTimeout(total=10, connect=2)
async def main():
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
async with session.get('https://ipipp.com/data') as resp:
print(resp.status)
在这段示例中,我们把连接池总大小设为 200,单 host 50。假如你同时请求 10 个域名,每个最多 50,就不会超过总 200 的上限。如果业务只对接一个 API 域,那么 limit_per_host 就约等于实际并发上限,此时可以把 limit 设得稍大以防其他域突发。
另外 TCPConnector 还支持 force_close 和 keepalive_timeout。若后端不支持长连接,可设 force_close=True 每次断开,但会损失性能;通常保持默认长连接并配合 enable_cleanup_closed 即可。
三、组合配置与最佳实践
将超时和连接池结合,是在 Session 层一次性完成的。下面给出一个较完整的生产级模板,适合大多数中高并发调用场景。
import asyncio
import aiohttp
from aiohttp import ClientTimeout, TCPConnector
def build_session():
timeout = ClientTimeout(total=8, connect=1.5, sock_read=4)
connector = TCPConnector(limit=300, limit_per_host=80, enable_cleanup_closed=True)
return aiohttp.ClientSession(connector=connector, timeout=timeout)
async def fetch(session, url):
try:
async with session.get(url) as resp:
return await resp.text()
except asyncio.TimeoutError:
return None
async def main():
urls = ['https://ipipp.com/a', 'https://ipipp.com/b']
async with build_session() as session:
tasks = [fetch(session, u) for u in urls]
results = await asyncio.gather(*tasks)
print(len(results))
if __name__ == '__main__':
asyncio.run(main())
这个模板把 Session 构建抽离成函数,方便在多处复用。超时压缩到 8 秒内,连接池给到 300,单 host 80,能够在普通 4 核机器上稳定支撑每秒数百次请求而不耗尽文件描述符。
实践中还要配合外部熔断与重试。aiohttp 自身不提供重试,可借助 tenacity 等库在 fetch 层装饰。但无论怎么封装,全局超时与连接池一定是地基,地基不稳,上层重试只会放大故障。
四、常见误区与排查
第一个误区是每次请求都新建 Session。Session 内部维护连接池,频繁创建销毁会让连接池失去意义,并且可能触及系统临时端口上限。应当一个事件循环内尽量复用同一个 Session。
第二个误区是把 limit_per_host 设得过大,认为这样更快。实际上若对端是数据库连接池较小的 API,过多并发只会让对端拒绝或变慢,反过来增加本端超时。可用压测逐步上调,观察 P99 延迟拐点。
# 错误示例:循环内建 Session,连接池无法复用
async def bad():
for url in ['https://ipipp.com/x', 'https://ipipp.com/y']:
async with aiohttp.ClientSession() as s: # 每次新建
async with s.get(url) as r:
print(await r.text())
上述写法在少量请求时看不出问题,一旦放到数千任务的 gather 中,会瞬间产生大量短连接,引发 Too many open files 错误。正确做法永远是把 Session 提到循环外,如前面模板所示。
最后,若日志中出现大量 asyncio.TimeoutError 且耗时接近你设的 total,先确认是网络慢还是连接拿不到。连接池满时请求会排队,排队时间也计入总超时,这时应调大 limit 或优化下游,而不是盲目加大超时。
aiohttpglobal_timeoutTCPConnector修改时间:2026-08-03 03:54:32