FastAPI 生产部署实战:Gunicorn + Uvicorn 与进程管理
文章目录
在日常开发阶段,我们通常用 uvicorn main:app --reload 跑一个 FastAPI 应用就够用了。但一旦要上线到生产环境,单进程、无进程守护、缺少并发能力的开发服务器就远远不够了。本文系统讲解如何用 Gunicorn + Uvicorn worker 在生产环境中稳定运行 FastAPI,并覆盖 Worker 调优、优雅关闭、Docker 打包与 Nginx 反向代理等核心知识点。
为什么生产环境不能用 uvicorn 单进程
uvicorn main:app --reload 虽然方便,但它本质上是单进程、单事件循环,存在几个生产环境无法接受的问题:
- 单核利用:一个 Uvicorn 进程只能吃满一个 CPU 核,多核机器直接浪费。
- 没有进程守护:进程崩溃后不会自动重启,需要外部 supervisor。
- 没有 reload 之外的重载策略:无法做 zero-downtime 滚动重启。
--reload自身有性能开销:文件监听本身消耗资源,绝不能带到生产。
正确的做法是用一个进程管理器来拉起多个 Uvicorn worker。FastAPI 官方推荐的就是 Gunicorn 配合 uvicorn.workers.UvicornWorker。
Gunicorn + Uvicorn Worker 基本用法
Gunicorn 是一个成熟的 WSGI 进程管理器,但 FastAPI 是 ASGI 应用。好在 Uvicorn 提供了一个 Gunicorn worker 类,让 Gunicorn 能管理 ASGI 进程。
最小启动命令
假设应用入口是 main.py 中的 app 对象:
1 | gunicorn main:app \ |
参数含义:
| 参数 | 说明 |
|---|---|
-w 4 |
启动 4 个 worker 进程 |
-k uvicorn.workers.UvicornWorker |
使用 Uvicorn 提供的 ASGI worker |
-b 0.0.0.0:8000 |
监听地址与端口 |
--access-logfile - |
访问日志输出到标准输出 |
--error-logfile - |
错误日志输出到标准错误 |
目录结构示例
一个典型的小型 FastAPI 项目结构如下:
1 | myapp/ |
main.py 示例:
1 | from fastapi import FastAPI |
Worker 数量怎么定
Gunicorn 官方给出的经验公式是:
1 | workers = (2 * CPU 核数) + 1 |
但这是针对 WSGI 同步应用的经验值。FastAPI 是异步的,单个 worker 已经能通过事件循环处理大量并发 IO,因此不必盲目堆 worker 数量。一般建议:
- CPU 密集型任务较多(如同步调用 CPU 算法):用
(2 * CPU) + 1,并把同步任务丢到线程池(run_in_threadpool)。 - 纯 IO 密集型(数据库、HTTP 调用):worker 数可以等于 CPU 核数甚至更少,靠事件循环扛并发。
查看机器核数:
1 | nproc |
不要忽视内存上限
每个 worker 是独立进程,会各自加载应用代码、模型、连接池。如果应用里加载了大模型或大字典,4 个 worker 就是 4 倍内存。生产前务必用 docker stats 或 htop 实测单 worker 内存占用,再反推合理的 worker 数。
异步与同步混用的坑
FastAPI 路由可以是 async def 也可以是普通 def:
async def路由在事件循环里直接执行,不要在里面写阻塞调用(如time.sleep、同步requests、同步文件 IO),否则会卡住整个 worker。- 普通
def路由会被 Uvicorn 自动丢到线程池(anyio.to_thread.run_sync),不会阻塞事件循环。
1 | import time |
如果必须在 async def 里调用阻塞库,可以用 run_in_threadpool:
1 | from fastapi.concurrency import run_in_threadpool |
优雅关闭与超时控制
生产环境部署更新时,不希望旧请求被直接掐断。Gunicorn 提供了几个关键参数:
| 参数 | 默认 | 建议 | 说明 |
|---|---|---|---|
--graceful-timeout |
30 | 30 | worker 收到停止信号后,给多少秒完成现有请求 |
--timeout |
30 | 30-120 | 单个 worker 超过该时间未响应心跳就被强杀 |
--keep-alive |
2 | 5 | keep-alive 连接保持秒数 |
推荐的完整启动配置(写入 gunicorn_conf.py):
1 | import multiprocessing |
启动:
1 | gunicorn main:app -c gunicorn_conf.py |
注意:
preload_app = True会在 fork worker 之前加载一次应用代码。好处是多个 worker 共享同一份导入的模块内存(写时复制),坏处是数据库连接池等需要在 fork 后重新建立,否则子进程会共享父进程的 socket 句柄导致错误。对于 Tortoise ORM、SQLAlchemy async 等,建议在 worker 启动钩子里重新初始化连接。
Docker 打包实践
把 FastAPI 打包成 Docker 镜像时,推荐多阶段构建 + 非 root 用户运行。
Dockerfile
1 | # ---------- 构建阶段 ---------- |
对应的 requirements.txt:
1 | fastapi==0.111.0 |
构建并运行:
1 | docker build -t myapp:latest . |
Nginx 反向代理
Gunicorn 直接对外暴露 8000 端口在生产中不推荐,前面通常加一层 Nginx 处理 TLS、静态文件、限流和 WebSocket 代理。
1 | upstream fastapi_backend { |
关键点:
proxy_set_header X-Forwarded-Proto $scheme让 FastAPI 知道原始协议是 https,配合--proxy-headers使用。- WebSocket 路由必须设置
Upgrade和Connection头,否则握手失败。 - Uvicorn 启动时加
--proxy-headers --forwarded-allow-ips='*'(或在 Gunicorn 配置里),才能正确解析X-Forwarded-*。
平滑重启与日志管理
平滑重启
向主进程发送 SIGHUP,Gunicorn 会重新加载配置并逐个重启 worker,实现 zero-downtime:
1 | kill -HUP $(cat /var/run/myapp.pid) |
如果只更新了应用代码而不改配置,用 SIGUSR2 触发热重载(配合 --preload 时不可用)。
日志切割
Gunicorn 的访问日志直接输出到 stdout,在 Docker 中由 Docker 日志驱动收集;在裸机上建议用 logrotate:
1 | /var/log/myapp/*.log { |
常见问题速查
- worker 启动后立刻被杀:多半是
--timeout太短,应用启动慢(比如连数据库超时)。检查--timeout与启动耗时。 - **数据库连接报错
MySQL server has gone away**:连接池里的连接被服务端断开。设置pool_recycle小于服务端wait_timeout。 - 内存持续上涨:检查是否有未关闭的
httpx.AsyncClient、未限流的队列、或循环引用。 - WebSocket 502:Nginx 没加
Upgrade/Connection头,或proxy_read_timeout太短。 preload_app后子进程共享了数据库连接:在 worker 的@app.on_event("worker_init")(Uvicorn)或 lifespan 中重新建立连接池。
小结
生产部署 FastAPI 的核心组合是 Gunicorn(进程管理)+ UvicornWorker(ASGI 执行)+ Nginx(反向代理)。掌握以下几点就能稳住大多数场景:
- 不要用
--reload上生产,用 Gunicorn 管理 worker。 - Worker 数量按 CPU 核数和内存上限权衡,异步应用不必盲目堆多。
async def里严禁阻塞调用,必要时用run_in_threadpool。- 用
--graceful-timeout和SIGHUP实现优雅关闭与平滑重启。 - Docker 打包用多阶段构建,前面加 Nginx 处理 TLS 与静态资源。
把这些配置固化到项目的 gunicorn_conf.py、Dockerfile 和 Nginx 配置里,部署就成了一件可重复、可回滚的工程化操作。