在日常开发阶段,我们通常用 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
2
3
4
5
6
gunicorn main:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8000 \
--access-logfile - \
--error-logfile -

参数含义:

参数 说明
-w 4 启动 4 个 worker 进程
-k uvicorn.workers.UvicornWorker 使用 Uvicorn 提供的 ASGI worker
-b 0.0.0.0:8000 监听地址与端口
--access-logfile - 访问日志输出到标准输出
--error-logfile - 错误日志输出到标准错误

目录结构示例

一个典型的小型 FastAPI 项目结构如下:

1
2
3
4
5
6
7
8
9
10
myapp/
├── main.py # 应用入口,定义 app = FastAPI()
├── routers/
│ ├── users.py
│ └── items.py
├── core/
│ ├── config.py # 配置管理
│ └── database.py # 数据库连接
├── requirements.txt
└── Dockerfile

main.py 示例:

1
2
3
4
5
6
7
from fastapi import FastAPI
from routers import users, items
from core.config import settings

app = FastAPI(title=settings.APP_NAME)
app.include_router(users.router)
app.include_router(items.router)

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
2
nproc
python3 -c "import os; print(os.cpu_count())"

不要忽视内存上限

每个 worker 是独立进程,会各自加载应用代码、模型、连接池。如果应用里加载了大模型或大字典,4 个 worker 就是 4 倍内存。生产前务必用 docker statshtop 实测单 worker 内存占用,再反推合理的 worker 数。

异步与同步混用的坑

FastAPI 路由可以是 async def 也可以是普通 def

  • async def 路由在事件循环里直接执行,不要在里面写阻塞调用(如 time.sleep、同步 requests、同步文件 IO),否则会卡住整个 worker。
  • 普通 def 路由会被 Uvicorn 自动丢到线程池(anyio.to_thread.run_sync),不会阻塞事件循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
import httpx
from fastapi import FastAPI

app = FastAPI()


@app.get("/sync-block")
async def bad_blocking():
# 危险:阻塞事件循环,整个 worker 卡住
time.sleep(2)
return {"ok": True}


@app.get("/async-correct")
async def good_async():
async with httpx.AsyncClient() as client:
resp = await client.get("https://httpbin.org/get")
return {"status": resp.status_code}


@app.get("/sync-def")
def sync_def():
# 普通函数,自动放到线程池,安全
time.sleep(2)
return {"ok": True}

如果必须在 async def 里调用阻塞库,可以用 run_in_threadpool

1
2
3
4
5
6
from fastapi.concurrency import run_in_threadpool

@app.get("/safe-block")
async def safe_block():
await run_in_threadpool(time.sleep, 2)
return {"ok": True}

优雅关闭与超时控制

生产环境部署更新时,不希望旧请求被直接掐断。Gunicorn 提供了几个关键参数:

参数 默认 建议 说明
--graceful-timeout 30 30 worker 收到停止信号后,给多少秒完成现有请求
--timeout 30 30-120 单个 worker 超过该时间未响应心跳就被强杀
--keep-alive 2 5 keep-alive 连接保持秒数

推荐的完整启动配置(写入 gunicorn_conf.py):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import multiprocessing
import os

bind = "0.0.0.0:8000"
workers = int(os.getenv("WEB_CONCURRENCY", multiprocessing.cpu_count() * 2 + 1))
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 60
graceful_timeout = 30
keepalive = 5

# 日志
accesslog = "-"
errorlog = "-"
loglevel = "info"

# 预加载应用:节省内存、加快启动,但注意全局状态会被所有 worker 共享读
preload_app = True

启动:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# ---------- 构建阶段 ----------
FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# ---------- 运行阶段 ----------
FROM python:3.11-slim

WORKDIR /app

# 拷贝依赖
COPY --from=builder /root/.local /root/.local
COPY . .

ENV PATH=/root/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1

# 安装 uvicorn worker 依赖
# requirements.txt 应包含: fastapi, uvicorn[standard], gunicorn

EXPOSE 8000

CMD ["gunicorn", "main:app", \
"-w", "4", \
"-k", "uvicorn.workers.UvicornWorker", \
"-b", "0.0.0.0:8000", \
"--timeout", "60", \
"--graceful-timeout", "30"]

对应的 requirements.txt

1
2
3
fastapi==0.111.0
uvicorn[standard]==0.30.1
gunicorn==22.0.0

构建并运行:

1
2
docker build -t myapp:latest .
docker run -d --name myapp -p 8000:8000 --env WEB_CONCURRENCY=4 myapp:latest

Nginx 反向代理

Gunicorn 直接对外暴露 8000 端口在生产中不推荐,前面通常加一层 Nginx 处理 TLS、静态文件、限流和 WebSocket 代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
upstream fastapi_backend {
server 127.0.0.1:8000;
# 多机部署时加更多 server
# server 10.0.0.12:8000;
}

server {
listen 80;
server_name api.example.com;

# 静态文件直接由 Nginx 处理
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
}

# 反向代理到 Gunicorn
location / {
proxy_pass http://fastapi_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# WebSocket 支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# 超时
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}

关键点:

  • proxy_set_header X-Forwarded-Proto $scheme 让 FastAPI 知道原始协议是 https,配合 --proxy-headers 使用。
  • WebSocket 路由必须设置 UpgradeConnection 头,否则握手失败。
  • 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
2
3
4
5
6
7
8
9
10
11
/var/log/myapp/*.log {
daily
rotate 14
compress
missingok
notifempty
sharedscripts
postrotate
kill -HUP $(cat /var/run/myapp.pid) 2>/dev/null || true
endscript
}

常见问题速查

  • 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(反向代理)。掌握以下几点就能稳住大多数场景:

  1. 不要用 --reload 上生产,用 Gunicorn 管理 worker。
  2. Worker 数量按 CPU 核数和内存上限权衡,异步应用不必盲目堆多。
  3. async def 里严禁阻塞调用,必要时用 run_in_threadpool
  4. --graceful-timeoutSIGHUP 实现优雅关闭与平滑重启。
  5. Docker 打包用多阶段构建,前面加 Nginx 处理 TLS 与静态资源。

把这些配置固化到项目的 gunicorn_conf.pyDockerfile 和 Nginx 配置里,部署就成了一件可重复、可回滚的工程化操作。