Uvicorn/Starlette/FastAPI async与非async线程及性能问题
FastAPI同步/异步接口性能异常问题解答
为什么单Uvicorn进程处理同步接口时会自动创建多线程?如何限制单进程仅用1个线程处理请求?
- Uvicorn的核心事件循环确实运行在单主线程上,但框架默认内置了线程池执行器,专门调度非
async def定义的同步视图函数。同步接口的逻辑不会直接跑在事件循环线程上阻塞循环,而是会被提交到这个内置线程池执行,这就是日志中出现多个工作线程处理请求的根本原因。 - 内置线程池的默认最大工位数随Python运行环境变化,若要强制单Uvicorn进程仅用1个线程处理所有请求,除启动时指定
--workers 1外,还需要在应用启动阶段替换事件循环的默认执行器,参考代码如下:
import asyncio from concurrent.futures import ThreadPoolExecutor from fastapi import FastAPI app = FastAPI() @app.on_event("startup") async def config_executor(): current_loop = asyncio.get_running_loop() # 替换默认线程池,最大工作线程设为1 current_loop.set_default_executor(ThreadPoolExecutor(max_workers=1))
注意:将同步接口的执行线程数限制为1后,所有同步请求会完全串行执行,遇到IO阻塞类场景时,接口整体吞吐量会大幅下降。
为什么异步接口的拉取性能反而弱于同步多线程场景?
这个结果和异步IO的设计预期不符,基本都是实现方式或测试设计的问题,核心原因通常有三类:
- 第一类是最常见的客户端写法错误:没有全局复用
httpx.AsyncClient实例,每次请求进来都新建、销毁异步客户端。新建异步客户端需要完成DNS解析、TCP握手、SSL证书校验、连接池初始化等一系列固定开销,5个请求重复执行这些操作的额外耗时,远大于同步接口的线程调度开销,直接拖慢整体响应速度。 - 第二类是异步代码存在隐式阻塞:如果异步接口中混入了同步阻塞调用(比如同步读写文件、用了同步请求库、没有正确加
await关键字触发异步调度),所有逻辑都会串行阻塞在唯一的事件循环线程上,本质是单线程串行执行,性能自然不如多线程并行处理的同步接口。 - 第三类是测试场景的干扰:你当前测试的是跨公网拉取第三方站点资源的场景,公网链路波动、目标站点的限流/限速策略、本地网络带宽争抢都会直接影响单次请求的耗时,5个并发的低压力测试下,这类外部因素带来的耗时偏差,很可能盖过框架本身的性能差异。
- 验证方法:在应用启动时初始化一个全局复用的
httpx.AsyncClient实例,所有异步接口共用这个实例发请求,排除建连开销后再做压测,异步接口的响应速度、吞吐量都会明显高于同步多线程版本,且并发量越高,异步的性能优势越显著。
内容的提问来源于stack exchange,提问作者raghavsikaria
相关产品推荐
相关产品推荐

