Python asyncio 并发控制实战:信号量、连接池与超时治理

举报
yd_285576981 发表于 2026/10/27 00:04:41 2026/10/27
【摘要】 一、并发数不是越大越好把上万个 URL 丢进 asyncio.gather,最直观的写法是给每个 URL 建一个 Task。但这样做会同时打开成千上万个连接:文件描述符耗尽、对端限流、GC 压力上升,最终表现为大量超时与重试风暴。真正可控的做法是把「并发」拆成三层:任务层:asyncio.Semaphore 限制同时进入临界区的协程数连接层:TCPConnector.limit 限制同时存...

一、并发数不是越大越好

把上万个 URL 丢进 asyncio.gather,最直观的写法是给每个 URL 建一个 Task。但这样做会同时打开成千上万个连接:文件描述符耗尽、对端限流、GC 压力上升,最终表现为大量超时与重试风暴。

真正可控的做法是把「并发」拆成三层:

  • 任务层:asyncio.Semaphore 限制同时进入临界区的协程数
  • 连接层:TCPConnector.limit 限制同时存在的 TCP 连接数
  • 协议层:超时、重试与退避策略

三者不匹配,就会出现「信号量是 50、连接池是 100」这种自相矛盾的配置。

二、三个高频坑

1. 信号量与连接池大小不匹配

Semaphore 控制的是「同时进入临界区的协程数」,TCPConnector.limit 控制的是「同时存在的 TCP 连接数」。如果 limit 小于信号量,多出来的协程会卡在获取连接上,表现为「看着并发 100,实际吞吐只有 20」。

2. 只设 total 超时

ClientTimeout(total=10) 覆盖整个请求生命周期,包含排队等待连接的时间。拥塞时协程可能还没发出请求就被判定超时,重试又叠加新的排队,形成雪崩。生产上更稳的做法是分别声明 connect、sock_read 与 total。

3. gather 直接吞异常

asyncio.gather 在默认模式下,任一子任务抛异常会立即向上传播,但其余任务仍在后台运行,容易造成「上层已经失败,后台还在打请求」。要么显式 return_exceptions=True 收口,要么使用 3.11 起的 TaskGroup 做结构化并发。

三、可复用的骨架代码

import asyncio
import random
import aiohttp


async def fetch(session, url, sem, retries=3):
    async with sem:
        for attempt in range(retries):
            try:
                timeout = aiohttp.ClientTimeout(total=10, connect=3, sock_read=6)
                async with session.get(url, timeout=timeout) as resp:
                    resp.raise_for_status()
                    return await resp.text()
            except (aiohttp.ClientError, asyncio.TimeoutError):
                if attempt == retries - 1:
                    raise
                # 指数退避 + 抖动,避免重试同步化
                await asyncio.sleep(0.2 * (2 ** attempt) + random.random() * 0.1)


async def run(urls, concurrency=50):
    sem = asyncio.Semaphore(concurrency)
    connector = aiohttp.TCPConnector(
        limit=concurrency,      # 与信号量对齐
        limit_per_host=10,      # 单域名保护
        ttl_dns_cache=300,
    )
    async with aiohttp.ClientSession(connector=connector) as session:
        tasks = [asyncio.create_task(fetch(session, u, sem)) for u in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)

四、上线前要看的三个指标

  • 在途请求数(in-flight):应稳定在信号量附近,持续打满说明后端已是瓶颈
  • 连接复用率:limit_per_host 过小会频繁建连,TLS 握手开销会吃掉并发收益
  • 超时分布:区分 connect 超时与 read 超时,前者多为网络或限流,后者多为后端慢查询

五、小结

asyncio 的并发能力来自事件循环,但「能并发」不等于「该并发」。把信号量、连接池、超时三者对齐,再配合带抖动的重试与结构化并发,才能在流量突增时保持可控。压测时建议从并发 10 开始逐步加档,观察在途请求数与 P99 延迟的拐点,而不是直接冲到一个看起来很爽的数字。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。