如果你最近在用 FastAPI,或者任何一个基于 asyncio 的 Python Web 框架,那你大概率在终端里敲过这样一行命令:uvicorn main:app --reload。很多朋友把 Uvicorn 当成框架自带的小工具,用它启动服务、调试接口,然后就不再深究了。但 Uvicorn 并不是框架的附属品,它是 Python 生态里专门用来运行异步 Web 应用的 ASGI 服务器,是整个异步 Web 技术栈中最底层、也最关键的一道关口:它负责接收浏览器或客户端的网络请求,把 HTTP 消息、WebSocket 连接解析成标准化的 ASGI 事件流,再交给 FastAPI、Starlette 这类异步框架去处理业务逻辑;框架处理完,它再把响应数据写回 socket。可以这么说:框架是大脑,Uvicorn 是手和嘴。
今天这篇文章,我打算把 Uvicorn 从里到外完整讲一遍——先讲它解决的协议问题,再讲它为什么快,然后是开发和生产环境下的完整配置清单,接着是我实际部署中踩过的几个坑,最后是它和其他 ASGI 服务器的选型边界。不管你是刚接触异步 Web 的新手,还是已经部署过几个服务但总在配置上吃亏的开发者,这篇应该都能帮上忙。
1. Uvicorn 是什么:异步 Web 请求的翻译官与看门人
1.1 WSGI 时代的瓶颈:一个请求一个线程的尴尬
要理解 Uvicorn 存在的意义,得先看一眼它替代的那个时代。Python Web 圈子里,WSGI(Web Server Gateway Interface)标准统治了非常多年,Django、Flask 这些经典框架都是建立在 WSGI 之上的。WSGI 的工作方式很简单粗暴:服务器收到一个 HTTP 请求,就创建一个线程(或者进程),然后调用你写的应用函数。应用函数内部如果碰了数据库查询、第三方 HTTP 调用、读写文件,那么这个线程就会卡在系统调用上,什么都干不了,只能等 IO 回来。
问题是,线程是有成本的。一个线程默认要占不少内存(栈空间、线程控制块),线程切换还要消耗 CPU 上下文切换时间。本地起个服务跑几千并发,线程池直接打满,后面来的请求只能排队。我见过不少 Django 项目在业务高峰时,Gunicorn 的 worker 数顶到几十上百个,内存吃紧,CPU 大量花在线程切换上。这不是代码写得不高效,而是同步模型的天花板就在那里:它没法表达"一个线程同时等好几个 IO,哪个先到先处理哪个"这种想法。asyncio 出现之后,Python 终于有了统一的事件循环原语,但 WSGI 规范本身根本不支持异步应用,于是 ASGI 规范应运而生。
1.2 ASGI 规范:从"函数调用"到"事件流"的转变
ASGI 的全称是 Asynchronous Server Gateway Interface,可以理解为 WSGI 的异步接替者。它不再规定"服务器调用一个普通函数"这么简单,而是重新定义了服务器和应用之间的通讯协议。ASGI 应用中,每个连接到来时,服务器会构造一个 scope 对象,里面包含请求方法、路径、头信息等元数据;应用是一个 async callable,它接收(scope, receive, send)三个东西,通过receive接收事件(比如 HTTP 请求体、WebSocket 消息),通过send发送事件(比如 HTTP 响应头、响应体、WebSocket 消息)。
这个规范带来的好处,首先是统一了 HTTP 和 WebSocket 的处理模型。WSGI 时代 WebSocket 基本是各玩各的,因为 WSGI 的函数调用模型根本没法定住一条长连接。其次是引入了 lifespan 协议,让应用在启动和关闭时可以执行初始化数据库连接池、清理资源这类生命周期代码。Uvicorn 就是这套规范的参考实现之一——它不写业务逻辑,专注把 socket 字节流翻译成 ASGI 事件,再把应用返回的事件写回 socket。
1.3 一次请求从网卡到 FastAPI 的完整旅程
我用一个最小例子串一遍。假设有个main.py,里面是:
from fastapi import FastAPI app = FastAPI() @app.get("/ping") async def ping(): return {"message": "pong"}命令行执行:
uvicorn main:app --reload拆开看这行命令:main:app是"模块名:应用实例名"的定位方式,Uvicorn 会去导入main模块,拿到名为app的 ASGI 应用对象;--reload是开发模式专用,开启文件监听,代码一改动就自动重启进程。
用户浏览器发来一个GET /ping请求后,链路是这样的:Uvicorn 的监听 socket 先收到 TCP 连接,接着它的 HTTP 解析器把请求头解析出来,按照 ASGI 协议构造一个 scope 字典,包括method="GET"、path="/ping"这些字段。然后 Uvicornawait app(scope, receive, send),把控制权交给 FastAPI。FastAPI 的星型路由根据 path 匹配到ping函数,执行完返回 JSON,应用调用send把响应事件发出来。Uvicorn 拿到响应头、响应体后,把它们编码成 HTTP 响应字节流写回 socket。整个过程里,Uvicorn 和 FastAPI 之间唯一的边界,就是你定义的app这个对象。
1.4 Uvicorn 与 Starlette、FastAPI 的生态定位
经常有人把 Uvicorn、Starlette、FastAPI 这三个词搞混,其实关系特别清楚。用汽车打个比方:Uvicorn 是发动机,Starlette 是车架和底盘,FastAPI 是在 Starlette 之上加了方向盘、仪表盘和智能驾驶系统的整车。FastAPI 依赖 Starlette,Starlette 是一个 ASGI 框架,但框架本身不能直接跑在网络上,必须有一个 ASGI 服务器把它拉起来,Uvicorn 就是这个发动机。你也可以写一个裸 ASGI 应用,不引入任何框架,然后用 Uvicorn 跑,完全合法。选型的时候记住一条主线:FastAPI/Starlette 官方文档默认推荐 Uvicorn,因为它是这些框架作者 Tom Christie 本人维护的,生态契合度最高。
2. Uvicorn 的快从哪来:uvloop、httptools 与协程模型
2.1 线程换成事件循环:等待不再占用劳力
Uvicorn 性能好的根源,首先在于它跑在事件循环模型上。传统同步服务器是"每个连接一个线程",连接多了就切换线程;事件循环则是在一个线程里调度成千上万个协程,谁在等 IO,就被挂起让出执行权;谁的数据到了,就被唤醒继续跑。
这个差异可以用餐厅来类比:同步服务器的模式是,每个客人配一个专属服务员,服务员站在旁边等着客人吃完一道菜再服务下一道,客人越多,需要雇的服务员越多,工资开销(内存)和互相走动避让(上下文切换)都很夸张。事件循环模式是,一个服务员同时服务几十桌客人,客人点了菜需要等待时,服务员就离开去服务其他桌,等菜做好了再回来端给对应的客人。所以单核 CPU 在事件循环模型下,能同时 hold 住成千上万个空闲连接,这在 IO 密集的 Web 场景里就是质变。
2.2 uvloop:把事件循环核心搬进 C 语言
不过,asyncio 自带的事件循环是纯 Python 实现的,虽然已经能用,但性能在追求极致的场景下还有提升空间。Uvicorn 的默认配置里,只要平台支持,会自动采用 uvloop 作为底层事件循环。uvloop 是 Cython 重写的事件循环实现,底层复用 libuv——就是 Node.js 用的那套 IO 库——你可以把它理解成"用 C 语言编写的事件循环内核"。
我在自己的项目里做过简单对比,同一个 FastAPI 服务,在纯 IO 接口压测下,uvloop 模式比原生 asyncio 模式吞吐量能高出 2 到 3 倍。Uvicorn 提供了--loop参数,可以手动指定用uvloop还是asyncio。需要注意的是,uvloop 在 Windows 平台上不可用,所以如果你在 Windows 上开发,Uvicorn 会自动回退到 asyncio;部署到 Linux 服务器后,它又会自动切到 uvloop。这个自动切换的行为很贴心,但也意味着你在 Windows 本地压出来的性能数据,和线上 Linux 会有明显差异。
2.3 httptools:HTTP 解析器的性能要诀
事件循环解决的是连接调度问题,但还有一块很容易被忽略的瓶颈,就是 HTTP 协议本身的解析。HTTP 请求头、请求体、分块传输编码,这些都需要逐字节解析,纯 Python 解析一帧数据要做的字节操作非常多。
Uvicorn 默认使用 httptools 作为 HTTP 解析器。httptools 是从 Node.js 的 http-parser 移植过来的 C 扩展,把解析请求头、解析分块编码这些高频动作全部下沉到了 C 层。Uvicorn 也支持--http h11切到纯 Python 实现的 h11。h11 的好处是代码完全可控、兼容性更好,适合排查协议级问题,但性能会打折。对于绝大多数线上服务,保持默认的 httptools 就行。如果你在日志里发现 Uvicorn 打印了类似"httptools 不可用,回退到 h11"的提示,那就要注意一下是不是服务器环境缺了编译依赖,导致 C 扩展没装成功。
2.4 异步服务器的正确使用姿势:别在协程里睡觉
说了这么多性能优势,有一个前提必须强调:Uvicorn 快在 IO 并发,而不是 CPU 并行。asyncio 是协作式调度,事件循环只有在你await一个真正会挂起的异步操作时,才会去调度其他协程。如果你在视图函数里写了同步阻塞代码,比如requests.get()、time.sleep(2),那整个 worker 的事件循环就会卡住,这个 worker 上所有的连接全部遭殃。
最常见的错误就是把同步库直接搬进 FastAPI 里。我给你看一个反面例子:
import requests from fastapi import FastAPI app = FastAPI() @app.get("/bad") async def bad(): # requests.get 是同步阻塞调用,会卡住整个事件循环 resp = requests.get("https://api.example.com/data") return resp.json()正确的做法是使用异步库:
import httpx from fastapi import FastAPI app = FastAPI() @app.get("/good") async def good(): async with httpx.AsyncClient() as client: resp = await client.get("https://api.example.com/data") return resp.json()这点直接决定了你用 Uvicorn 是体验到丝滑并发,还是体验到"并发一上去就超时"。很多性能问题排查到最后,根子不在服务器,而在应用代码里那些拖住事件循环的同步调用。
3. 从开发到生产:Uvicorn 配置清单与推荐部署方案
3.1 本地开发:一条好用的调试命令
开发阶段我的习惯命令是:
uvicorn main:app --reload --host 0.0.0.0 --port 8000--host 0.0.0.0是为了让局域网内其他设备(比如手机、另一台电脑)也能访问到本地服务,方便联调;--port 8000是 Uvicorn 默认端口,如果被占了就换一个。--reload会启用一个独立的 reloader 进程,监听当前目录下的文件变化,代码一保存就自动重启服务,省去手动重启的麻烦。它的底层依赖 watchfiles 这类监听库,文件事件发生时会重新导入应用模块。
需要特别注意的是,--reload和--workers不能同时用。如果你在一个命令里既加了--reload又加了--workers 4,Uvicorn 会打印一条警告,然后把 worker 数强制设为 1。这条警告很容易被忽略,但后果很隐蔽:你以为自己在 4 进程的负载均衡下调试,实际上只有一个 worker 在跑。开发模式下这问题不大,但如果这条命令被原封不动抄到生产环境,就有得好果子吃了,这个坑我在下一节会细说。
3.2 核心运行参数速查表
用 Uvicorn 久了之后,我发现真正需要熟练的参数其实就那么多。下面这张表是我自己整理的速查表,覆盖了本地开发和线上部署最常用到的一批配置:
| 参数名 | 默认值 | 作用 |
|---|---|---|
--host | 127.0.0.1 | 绑定监听地址,服务器部署一般设为 0.0.0.0 |
--port | 8000 | 监听端口 |
--workers | 1 | worker 进程数,多核场景按需增加 |
--loop | auto | 事件循环实现,auto 在可用时自动切 uvloop |
--http | auto | HTTP 解析器,auto 默认选 httptools |
--ws | auto | WebSocket 实现,auto 自动选 websockets / wsproto |
--env-file | 无 | 启动时加载 .env 文件中的环境变量 |
--log-level | info | 日志级别,生产可调为 warning |
--access-log | 开启 | 是否打印访问日志,压测时建议关闭 |
--proxy-headers | 开启 | 是否信任 X-Forwarded-* 头部 |
--forwarded-allow-ips | 127.0.0.1 | 可以被信任的代理 IP 列表,多个 IP 用逗号分隔 |
--timeout-keep-alive | 5 | keep-alive 连接最长空闲时间(秒) |
--limit-concurrency | 无限制 | 单 worker 最大并发连接数 |
--limit-max-requests | 无限制 | worker 处理多少请求后自动重启,能缓解内存泄漏 |
--timeout-graceful-shutdown | 无限制 | 优雅关停时最多等待多少秒 |
其中--proxy-headers和--forwarded-allow-ips是一对容易踩坑的参数,下章重点讲。
3.3 生产部署方案一:裸跑 Uvicorn 的推荐配置
如果你在容器化环境里部署,通常一个容器只跑一个 worker 进程,交给 Kubernetes 这类平台去水平伸缩,那直接裸跑 Uvicorn 是最简单的方案。我常用的生产命令长这样:
uvicorn main:app \ --host 0.0.0.0 \ --port 8000 \ --workers 2 \ --log-level warning \ --no-access-log \ --proxy-headers \ --forwarded-allow-ips 10.0.0.0/8,192.168.0.0/16 \ --timeout-keep-alive 5 \ --limit-concurrency 2048 \ --timeout-graceful-shutdown 30解释一下几个关键选择。--log-level warning是减少 info 级别日志刷屏,但注意它不影响访问日志,访问日志要单独用--no-access-log关掉。--forwarded-allow-ips必须按实际网络环境写,如果你不知道 Nginx 具体 IP,可以先写内网网段,但生产环境建议精确到 IP。--limit-concurrency 2048是给单 worker 设一个并发上限,防止慢客户端把进程资源吃满。--timeout-graceful-shutdown 30是给优雅退出留一个缓冲时间,下一章详细讲为什么这个参数很重要。
还有一个细节:在 Docker 里,如果 Uvicorn 是容器里的 1 号进程(PID 1),它会直接接收docker stop发出的 SIGTERM 信号,然后执行优雅关闭。这一点 Uvicorn 处理得很好,不需要额外包一个 tini。但如果你用了--workers多进程模式,Uvicorn 的主进程会负责转发信号给 worker,整体行为也还靠谱。
3.4 生产部署方案二:Gunicorn + UvicornWorker 的经典组合
另一种更经典的方案,是用 Gunicorn 管理 worker 进程,再让每个 worker 跑 Uvicorn 的 worker 类。命令是:
gunicorn -w 4 -k uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --timeout 60 \ --graceful-timeout 30 \ main:app这里的核心是-k uvicorn.workers.UvicornWorker:Gunicorn 作为进程管理器,负责 worker 的创建、监控、重启;每个 worker 内部通过 UvicornWorker 类跑完整的 ASGI 事件循环和高性能 HTTP 解析。这套组合的好处在于,Gunicorn 的进程管理机制比 Uvicorn 自带的--workers更成熟,尤其是在 worker 卡死自动重启、信号处理、平滑重载这些方面,久经生产考验。
什么时候选 Gunicorn 组合,什么时候直接裸跑 Uvicorn?我的经验是:在虚机或裸机上跑传统部署,用 Gunicorn 组合更稳;在 K8s 容器里,平台已经承担了进程调度和重启职责,就裸跑 Uvicorn 更简单,少一层依赖。两种方案没有高低之分,只有合不合适。
4. 部署踩坑实录:代理头、reload、优雅退出与 WebSocket
4.1 Nginx 反代后真实 IP 全变成 127.0.0.1
最常见的第一个坑,是 Nginx 反向代理 Uvicorn 之后,应用日志和访问日志里的客户端 IP 全变成了127.0.0.1。原因很好理解:浏览器先连 Nginx,Nginx 再连 Uvicorn,Uvicorn 看到的 socket 连接来源自然就是 Nginx。
解决办法是开--proxy-headers,它会让 Uvicorn 信任 Nginx 传进来的X-Forwarded-For、X-Forwarded-Proto这些头部,用里面的 IP 替代直连 IP。但这里有个安全细节:--proxy-headers默认是开启的,可 Uvicorn 默认只信任来自127.0.0.1和::1的代理。如果 Nginx 在另一台机器上,你就必须通过--forwarded-allow-ips显式把 Nginx 所在 IP 或网段加进去,否则开不开都一样,真实 IP 依然拿不到。
更需要注意的是,如果你的服务直接暴露在外网,没有任何前置代理,千万不要开着--proxy-headers信任任意来源的X-Forwarded-For,因为客户端可以自己伪造这个头,把假 IP 写到你的日志里,甚至影响一些基于 IP 的限流策略。这种伪造在日志分析时会非常误导人。所以,先搞清楚你的网络拓扑里到底有几层代理,再决定怎么配--forwarded-allow-ips。
4.2 reload 命令误上生产之后
第二个坑,是开发时那条带--reload的命令被直接复制到生产环境。症状往往很诡异:服务偶尔卡一下、代码文件一变更就"神秘重启"、压测时的 QPS 和 worker 数对不上。
如果你在启动日志里看到类似于"Started reloader process"这样的字样,那就说明 reload 模式已经启动了。reload 模式首先会多跑一个 reloader 父进程,专门负责监听文件变化,这本身就有额外的 CPU 和文件 IO 开销;其次,任何代码文件变动都会触发整个服务重启,生产环境如果有多个人在服务器上动过代码,服务就会莫名其妙地反复重启,连接大量中断。我之前排查过一个案例,对方的 Uvicorn 命令带了--reload又带了--workers 4,日志里明明打了警告,但由于部署脚本没有实时展示输出,这个警告被完全淹没了,结果是服务只有一个 worker,流量高峰期性能很差。排查完把--reload去掉、重新以多 worker 启动,问题立刻消失。
我的建议是:开发命令和生产命令写成两套独立的启动脚本,或者用环境变量区分,不要让--reload有机会混进生产环境的配置文件里。
4.3 优雅关闭:kill 之后请求没处理完就被切断
第三个坑和请求的优雅退出有关。你在服务器上kill一个 Uvicorn 进程,或者执行docker stop时,如果处理得不好,正在处理的请求会被直接切断,用户会看到 502 或者连接被重置。
Uvicorn 本身是支持优雅关闭的:它收到 SIGINT 或 SIGTERM 后,会停止接收新连接,让正在处理中的请求走完,然后退出。但有一个前提,就是要给它足够的时间。默认情况下--timeout-graceful-shutdown是无限制等待,这看起来很好,但实际部署时很多编排系统会设定强杀时限。Kubernetes 默认在发送 SIGTERM 后等 30 秒,超时就 SIGKILL;如果你有超过 30 秒的长请求,就会看到"明明优雅退出了,还是有请求被掐断"的现象。
解决方案是主动设置--timeout-graceful-shutdown 30之类的值,并且让这个值略小于编排系统的强杀时限。如果你用 Gunicorn 管理,则要关注--graceful-timeout参数,它控制 worker 在收到退出信号后最多等待的秒数。生产环境我一般把 Uvicorn 的优雅退出时间设在 30 秒以内,同时把 K8s 的 terminationGracePeriodSeconds 配到 45 秒以上,留出余量。
4.4 keep-alive 与并发限流:慢客户端和连接复用怎么平衡
第四个坑是和连接有关的权衡。--timeout-keep-alive控制的是,一个 keep-alive 连接在完成一次请求后,空闲多久会被 Uvicorn 关闭。默认值是 5 秒。
这个值设太短,客户端(比如浏览器)复用一个连接准备发第二个请求时,发现连接已经关了,就得重新建 TCP 连接,握手成本变高,吞吐受影响。设太长,又会占着 socket 不释放,在高并发慢客户端场景下,容易把 worker 的连接数打满。我的实践建议是:前端有 Nginx 时,Uvicorn 只需对 Nginx 保持连接,内网链路很快,5 秒到 10 秒都合理;如果服务直接面对公网,没有 Nginx 挡在前面,建议把 keep-alive 设得保守一些,比如 2 到 5 秒,同时务必设置--limit-concurrency。--limit-concurrency的作用是限制单 worker 同时建立的连接数,防止恶意或意外的慢连接把进程内存和文件描述符耗尽。它配合--limit-max-requests(worker 处理 N 个请求后自动重启)一起用,能有效缓解长时间运行导致的内存增长问题。
4.5 WebSocket 部署的额外功课
如果你用 Uvicorn 跑 WebSocket 服务,还有几个额外的点要注意。第一,WebSocket 是长连接,一个 worker 能同时承载的 WebSocket 连接数和它的文件描述符上限、内存直接相关,worker 数也不是越多越好,要根据业务并发评估。第二,多 worker 部署后,WebSocket 连接会被分发到不同 worker 进程,进程之间内存不共享,如果你在单个进程里维护了一份在线连接列表,那广播消息时只有那一个进程里的连接能收到通知。正确做法是引入 Redis pub/sub 或者消息队列,让所有 worker 订阅同一个频道,收到消息后各自向自己进程内的连接推送。
第三,Nginx 反代 WebSocket 时,需要显式配置 Upgrade 相关的头信息,否则握手游不成功。这里给一个最基本的 Nginx 配置片段参考:
location /ws/ { proxy_pass http://uvicorn_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }这些虽然不是 Uvicorn 本身的问题,但只要是 Uvicorn 部署,十有八九会和 Nginx 搭配,提前把这些配好,能少走很多弯路。
5. 选型边界:Uvicorn 与 Daphne、Hypercorn 的取舍
5.1 三款主流 ASGI 服务器的定位差异
Uvicorn 虽然是目前最主流的 ASGI 服务器,但它并不是唯一的选择。圈子里还有 Daphne 和 Hypercorn,各有各的擅长领域。我整理了一个简单的对比:
| 特性 | Uvicorn | Daphne | Hypercorn |
|---|---|---|---|
| 维护方 | Tom Christie / Encode | Django Channels 团队 | Quart 项目相关 |
| HTTP/1.1 | 支持(httptools 高性能) | 支持 | 支持 |
| HTTP/2 | 不支持原生端到端 | 支持 | 支持 |
| HTTP/3(QUIC) | 不支持 | 不支持 | 支持 |
| WebSocket | 支持 | 支持 | 支持 |
| 性能取向 | 高性能优先 | 稳定性优先 | 协议全面优先 |
| 推荐场景 | FastAPI/Starlette 生态 | Django + Channels | 需要 HTTP/2/3 的场景 |
Daphne 是 Django Channels 官方推荐的服务器,和 Django 生态的集成最顺,但更新节奏和性能表现相对中庸。Hypercorn 最大的卖点是协议支持非常全面,HTTP/2、HTTP/3 都能端到端处理,适合对现代协议有硬性需求的场景,不过配置和使用门槛也更高一些。对于绝大多数 FastAPI/Starlette 用户,Uvicorn 依然是最省心的默认选择,因为这三个框架本来就是同一套技术栈。
5.2 HTTP/2 的实际情况:Uvicorn 不背这个锅
很多人会问:"Uvicorn 支不支持 HTTP/2?"答案是目前并不原生支持。它默认的 httptools 解析器处理的是 HTTP/1.1,h11 也是 HTTP/1.1 方向,Uvicorn 目前没有端到端的 HTTP/2 协议栈。
但在实际生产架构里,这个能力缺口通常被上层代理填平了。标准做法是让 Nginx、Traefik 或者云上的负载均衡器终结客户端的 HTTP/2 连接,然后后端用 HTTP/1.1 转发给 Uvicorn。客户端看到的是 HTTP/2,性能和协议红利照样能吃到,Uvicorn 只需要处理上游代理的 HTTP/1.1 连接就够了。只有在内部微服务之间也需要端到端 HTTP/2 的极端场景下,才需要评估 Hypercorn。所以你在选型时,先想清楚 HTTP/2 到底要在哪一段生效,别为了一个前端已经解决的问题去换服务器。
5.3 什么时候根本不该用 Uvicorn
聊完优点,也该说说哪些场景我不推荐用 Uvicorn。
第一,纯同步 Django 老项目。如果你没有 WebSocket、没有异步视图、没有 Streaming 响应需求,那 Gunicorn 加普通同步 worker 反而更简单直接。强行引入 ASGI 服务器,并不会让同步 ORM 查询变快,反而多了一层复杂度。
第二,CPU 密集型的业务接口。Uvicorn 的协程模型在大量计算场景下帮不上忙,因为 GIL 和单线程事件循环决定了它只能跑满一个核。CPU 密集型任务更合适的架构是开多进程、或者把重计算丢到独立的任务队列里去,再异步回调结果。
第三,应用代码里大量使用同步阻塞调用的项目。如果一个项目里到处都是requests.get、time.sleep、同步数据库驱动的调用,那么把服务器换成 Uvicorn 只会让你看到一个更明确的"卡顿放大镜"——因为所有阻塞都会堵住整个 worker。要么先把这些同步调用换成异步版本,要么就不要硬上 Uvicorn,先老老实实用同步服务器。
5.4 版本管理:别让 Uvicorn 的升级变成惊喜
最后补一个经验:Uvicorn 的版本更新速度并不慢,升级时大版本之间的默认行为偶尔会有变化。我吃过一次亏,某个项目从 0.20 升到 0.30 之后,WebSocket 的实现由默认使用 wsproto 调整为自动优先选 websockets 库,压测时握手行为有一点差异,排查了两天才发现是版本切换带来的。
所以线上环境我强烈建议把 Uvicorn 版本锁死,写在requirements.txt或pyproject.toml的精确版本里,比如uvicorn==0.30.1,不要用uvicorn>=0.30这种宽松约束。升级前先在测试环境专门跑一遍 WebSocket、长连接、优雅退出这三类场景的回归,再决定要不要上生产。
最后分享我自己生产环境常用的组合拳:前端 Nginx,中间 Gunicorn 管理进程,worker 用uvicorn.workers.UvicornWorker,框架是 FastAPI。如果是容器化部署,则简化成裸跑 Uvicorn 单 worker,把进程调度交给 K8s。压测时记得加--no-access-log,或者把访问日志接到结构化日志采集里,否则压测过程中的 access log 刷屏本身就能拖慢不少性能。再补一个小技巧:启动命令里强烈建议加上--limit-concurrency,按内存和业务情况设一个合理的并发上限,宁可让多余的请求排队,也不要让慢客户端把整个 worker 的内存拖爆。Uvicorn 是一台非常称职的异步发动机,但驾驶它的还是你——把事件循环里那些同步阻塞的毛病改掉、把代理头和优雅退出的配置弄对,它才能真正跑出你预期中的性能。