当前位置: 首页 > news >正文

《AI大模型应用开发实战从入门到精通共60篇》041、异步编程:用asyncio提升LLM应用的并发性能

041 异步编程:用asyncio提升LLM应用的并发性能

从一次线上事故说起

凌晨两点,告警电话把我从床上拽起来。监控显示我们的LLM对话服务响应时间从200ms飙到了8秒,CPU负载却只有30%。查日志发现,每次用户请求都在等上游的OpenAI接口返回——一个请求等2秒,十个请求排队等20秒。同步阻塞,典型的“一个人干活,一群人围观”场景。

这个问题的本质是:LLM调用是I/O密集型操作,网络延迟占大头。同步代码里,线程在等待网络响应时啥也不干,白白浪费CPU时间片。当时我盯着火焰图里那条长长的recvfrom调用链,心想:必须上异步了。

asyncio到底在解决什么问题

很多人把asyncio和“并发”划等号,其实它解决的是等待时的资源复用。想象你在咖啡店排队点单:同步模式是你站在柜台前等咖啡做好才让下一个人点;异步模式是你点完单拿个号去旁边坐着,服务员继续接待下一个人,咖啡好了叫号。

Python的GIL让多线程在CPU密集型任务上表现糟糕,但对I/O密集型任务,asyncio用单线程+事件循环实现了比多线程更高效的并发。原因很简单:线程切换有开销,协程切换是用户态操作,轻量得多。

从同步到异步:一个LLM调用的改造实录

先看一个典型的同步LLM调用代码:

importrequestsimporttimedefcall_llm(prompt):# 这里踩过坑:requests默认超时时间很长,生产环境一定要设timeoutresp=requests.post("https://api.openai.com/v1/chat/completions",json={"model":"gpt-3.5-turbo","messages":[{"role":"user","content":prompt}]},headers={"Authorization":"Bearer YOUR_KEY"},timeout=30)returnresp.json()# 串行调用,10个请求要等20秒+prompts=["讲个笑话","写首诗","翻译成英文",...]results=[]forpinprompts:results.append(call_llm(p))

这段代码的问题:每个call_llm都在阻塞等待网络响应,10个请求依次执行,总耗时 = 单个请求耗时 × 请求数。

改成asyncio版本:

importasyncioimportaiohttp# 别用requests,它不支持异步asyncdefcall_llm_async(session,prompt):# 注意:aiohttp的session要复用,别每次请求都创建新的asyncwithsession.post("https://api.openai.com/v1/chat/completions",json={"model":"gpt-3.5-turbo","messages":[{"role":"user","content":prompt}]},headers={"Authorization":"Bearer YOUR_KEY"},timeout=aiohttp.ClientTimeout(total=30))asresp:returnawaitresp.json()asyncdefmain():# 这里踩过坑:connector参数控制连接池大小,默认20,并发高时要调大connector=aiohttp.TCPConnector(limit=50)asyncwithaiohttp.ClientSession(connector=connector)assession:prompts=["讲个笑话","写首诗","翻译成英文",...]# 创建10个协程任务,并发执行tasks=[call_llm_async(session,p)forpinprompts]results=awaitasyncio.gather(*tasks)returnresults# 运行事件循环start=time.time()results=asyncio.run(main())print(f"耗时:{time.time()-start:.2f}秒")

改造后,10个请求几乎同时发出,总耗时 ≈ 最慢的那个请求的耗时(通常2-3秒)。性能提升5-10倍。

那些年我踩过的asyncio坑

坑1:在异步函数里调用同步阻塞代码

asyncdefbad_example():# 别这样写!time.sleep会阻塞整个事件循环time.sleep(5)return"done"asyncdefgood_example():# 正确做法:用asyncio.sleep让出控制权awaitasyncio.sleep(5)return"done"

如果必须调用同步阻塞库(比如某些数据库驱动),用loop.run_in_executor把它扔到线程池里:

importconcurrent.futuresasyncdefcall_sync_db():loop=asyncio.get_event_loop()# 这里踩过坑:默认线程池大小是CPU核心数×5,高并发场景要自定义withconcurrent.futures.ThreadPoolExecutor(max_workers=20)aspool:result=awaitloop.run_in_executor(pool,sync_db_query)returnresult

坑2:忘记处理异常导致任务静默失败

asyncio.gather默认会抛出第一个异常,导致其他任务被取消。更稳妥的做法:

asyncdefsafe_gather():tasks=[call_llm_async(session,p)forpinprompts]# return_exceptions=True让异常作为结果返回,不会中断其他任务results=awaitasyncio.gather(*tasks,return_exceptions=True)fori,rinenumerate(results):ifisinstance(r,Exception):print(f"第{i}个请求失败:{r}")results[i]=None# 用默认值替代returnresults

坑3:事件循环嵌套

在Jupyter Notebook或某些框架里,事件循环可能已经运行,再调用asyncio.run()会报错。解决方案:

try:loop=asyncio.get_running_loop()exceptRuntimeError:# 没有运行中的事件循环,创建新的results=asyncio.run(main())else:# 已有事件循环,用create_taskresults=loop.run_until_complete(main())

实战:构建一个带限速的LLM并发调用器

LLM API通常有速率限制(比如每分钟60次),并发太高会被限流甚至封号。我们需要一个带令牌桶的异步限速器:

importasyncioimporttimeclassRateLimiter:def__init__(self,max_calls,period=60):self.max_calls=max_calls self.period=period self.tokens=max_calls self.last_refill=time.monotonic()self._lock=asyncio.Lock()# 这里踩过坑:必须用asyncio.Lock,不是threading.Lockasyncdefacquire(self):asyncwithself._lock:now=time.monotonic()elapsed=now-self.last_refill# 按时间比例补充令牌self.tokens=min(self.max_calls,self.tokens+elapsed*(self.max_calls/self.period))self.last_refill=nowifself.tokens<1:# 令牌不足,计算需要等待的时间wait_time=(1-self.tokens)*(self.period/self.max_calls)awaitasyncio.sleep(wait_time)self.tokens=0else:self.tokens-=1# 使用示例limiter=RateLimiter(max_calls=60,period=60)# 每分钟60次asyncdefrate_limited_call(session,prompt):awaitlimiter.acquire()# 获取令牌,可能等待returnawaitcall_llm_async(session,prompt)

这个限速器用asyncio.Lock保证令牌操作的原子性,用time.monotonic避免系统时间调整带来的问题。实际生产环境中,我还会加上指数退避重试逻辑。

性能调优:从10倍到50倍

基础异步改造能带来5-10倍提升,但想榨干性能,还需要几个技巧:

1. 连接复用

aiohttp.ClientSession默认会复用TCP连接,但要注意连接池大小。我一般根据API的并发限制来设置:

# 如果API允许100并发,连接池设120留余量connector=aiohttp.TCPConnector(limit=120,limit_per_host=120)

2. 请求合并

对于流式输出(SSE),用aiohttpcontent属性逐块读取,避免等待完整响应:

asyncdefstream_llm(session,prompt):asyncwithsession.post(url,json={"stream":True,...},headers=headers)asresp:asyncforchunkinresp.content:# 处理每个chunk,实现打字机效果yieldchunk.decode()

3. 使用uvloop加速

uvloop是用Cython重写的事件循环,性能比默认的asyncio事件循环快2倍:

importuvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())# 然后正常使用asyncio.run()

注意:uvloop在Windows上支持有限,生产环境建议在Linux上部署。

个人经验建议

  1. 别盲目全盘异步化。如果你的服务主要是CPU计算(比如图像处理、模型推理),异步带来的收益有限,反而增加代码复杂度。异步最适合I/O密集且并发高的场景。

  2. 异步代码的调试是个噩梦asyncio.gather里的异常堆栈经常指向事件循环内部,而不是你的业务代码。我的做法是:每个协程函数都加try/except,用logging.exception记录完整上下文,再重新抛出或返回错误码。

  3. 注意内存泄漏。异步代码里容易忘记释放资源,特别是aiohttp.ClientSession。始终用async with管理资源,或者显式调用session.close()

  4. 监控要跟上。异步服务的性能指标和同步服务不同,要关注事件循环的延迟(event loop lag)、协程队列长度、连接池使用率。我习惯在关键路径上埋点,记录每个协程的等待时间和执行时间。

  5. 从同步到异步的迁移策略:不要一次性重写所有代码。先找出I/O瓶颈最严重的模块(比如LLM调用、数据库查询),用异步重写,其他部分保持同步。用run_in_executor作为过渡桥梁。

最后说一句:asyncio不是银弹,但对付LLM这种高延迟、高并发的I/O场景,它确实是最趁手的工具。下次你的服务再因为同步阻塞而告警,试试把requests换成aiohttp,把time.sleep换成asyncio.sleep——你会回来感谢我的。

http://www.jsqmd.com/news/741532/

相关文章:

  • C语言PLCopen在线调试实战:5步定位ST代码运行时异常,98%工程师忽略的符号表同步陷阱
  • 为什么92%的C语言PLC项目在PLCopen Level A认证时失败?——基于37个真实产线案例的12项隐性合规红线清单
  • C++实现Windows防休眠工具:模拟鼠标移动与系统API调用详解
  • NHSE:动物森友会存档编辑框架的技术架构与生态价值
  • RTMP视频流的帧格式分析
  • 创业团队如何利用Taotoken管理多个项目的API Key与访问权限
  • 5个AI象棋实战技巧:从新手到高手的Vin象棋完全指南
  • 避开这些坑!OpenMV4颜色阈值调试保姆级指南(附Lab颜色空间工具)
  • 算法训练营第二十天|150.逆波兰表达式求值
  • 单目3D重建技术:从深度学习到工业应用
  • 2026成都八喜热水器维修标杆名录:前锋热水器官方维修、华帝壁挂炉24小时维修、华帝热水器官方维修、博世壁挂炉官方维修选择指南 - 优质品牌商家
  • 杀戮尖塔2mod二次元猎宝
  • 编程入门:if和switch分支结构
  • 云原生入门系列|第30集(终章):从零入门到实战落地,解锁云原生核心能力
  • Docker容器化部署OpenClaw AI智能体:安全隔离与自动化实践指南
  • CLM技术架构:构建企业级证书自动化管理平台
  • 百度网盘秒传脚本完整指南:永久文件分享的终极解决方案
  • 实测避坑:ESP32 ADC采样率虚标?手把手教你用DMA模式获取真实数据(附IDF V4.4.2修复方案)
  • CaaS商业模式解析:证书即服务如何创造商业价值
  • 基于STM32F1实现LADRC线性自抗扰控制(TD、ESO、LSEF编程),以直流电机调速控制为例,支持串口调试,上位机调试
  • Raspberry Pi 5 16GB版性能解析与优化指南
  • 沉淀仓核心配件(H 管)安装与作用
  • 企业级AI推理系统性能评估与优化实践
  • DDrawCompat解决方案:让Windows 11完美运行DirectX 1-7经典游戏
  • 三甲医院AI联合实验室内部流出:127行高鲁棒性MRI脑卒中分割代码,支持T1/T2/FLAIR多序列融合,误报率低于0.8%(附ROC曲线验证图)
  • anlogic pl中断驱动配置
  • LILYGO T-Pico-2350开发套件:双核MCU与无线SoC的完美融合
  • R3nzSkin英雄联盟换肤工具:从源码编译到安全使用的完整指南
  • 数据结构协议:跨语言数据一致性的核心解决方案
  • 量子误差缓解技术:DCA方法原理与应用实践