Python 生产环境部署实战——Gunicorn、Uvicorn、进程管理完整方案
Python 生产环境部署实战——Gunicorn、Uvicorn、进程管理完整方案
适读人群:有 Python 服务开发经验、即将或已经在管理生产部署的工程师 | 阅读时长:约15分钟 | 核心价值:搞清楚 Gunicorn 和 Uvicorn 该怎么选、怎么配,进程管理踩坑全记录
上个月有个同学私信我,说他们的 FastAPI 服务部署在生产上,单台 8 核机器,每秒只能处理大概 87 个请求,稍微有点流量就开始排队。他以为是代码问题,找了半天没找到。
我让他把部署命令发过来,一看:uvicorn main:app --host 0.0.0.0 --port 8000
就这一行。单进程,没有 worker,CPU 跑到 12%,其他7个核全在睡觉。
这不是代码问题,这是部署问题。
先搞清楚几个概念
很多人分不清 Gunicorn 和 Uvicorn 的关系,先说清楚。
WSGI vs ASGI:
- WSGI(Web Server Gateway Interface):同步接口,Django、Flask 用这个
- ASGI(Asynchronous Server Gateway Interface):异步接口,FastAPI、Starlette、Django 3.1+ 用这个
Gunicorn:
- WSGI server,用于同步框架
- 支持多 worker 进程(prefork model)
- 有一个 master 进程管理多个 worker 进程
- 通过安装
uvicorn[standard]提供的 worker class,也能跑 ASGI 应用
Uvicorn:
- ASGI server,专门跑异步应用
- 单独运行只有单进程(可以加
--workers但功能没 Gunicorn 强) - 通常在生产中作为 Gunicorn 的 worker class 使用
生产环境推荐组合:
- 同步框架(Flask/Django):Gunicorn + sync worker
- 异步框架(FastAPI/Starlette):Gunicorn + UvicornWorker
Gunicorn 配置完整方案
不要把配置写命令行,写配置文件,方便维护和 review。
# gunicorn.conf.py
import multiprocessing
import os
# ---- 基本设置 ----
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1 # 经典公式,CPU密集型可以调低
worker_class = "uvicorn.workers.UvicornWorker" # 异步框架用这个
worker_connections = 1000 # 每个 worker 最大并发连接数(仅异步 worker 有效)
timeout = 120 # worker 处理请求超时时间,超时会被 master 杀掉重启
keepalive = 5 # keep-alive 连接保持时间(秒)
graceful_timeout = 30 # 优雅关闭等待时间
# ---- 进程设置 ----
daemon = False # 不要用 daemon 模式,交给 systemd/supervisor 管理
pidfile = "/var/run/myapp/gunicorn.pid"
user = "www-data"
group = "www-data"
# ---- 日志设置 ----
accesslog = "/var/log/myapp/access.log"
errorlog = "/var/log/myapp/error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# 最后的 %(D)s 是响应时间(微秒),非常有用
# ---- 性能调优 ----
max_requests = 1000 # worker 处理这么多请求后自动重启,防内存泄漏
max_requests_jitter = 100 # 加随机抖动,避免所有 worker 同时重启
preload_app = True # 在 fork 前加载应用,节省内存(Copy-on-Write)
# ---- 钩子 ----
def on_starting(server):
print(f"Gunicorn master starting, workers={workers}")
def worker_exit(server, worker):
# worker 退出时的清理工作,比如关闭数据库连接
print(f"Worker {worker.pid} exited")
def post_fork(server, worker):
# fork 之后,每个 worker 里初始化一些不能在 master 里共享的资源
# 比如重新创建数据库连接池
pass启动命令:
gunicorn -c gunicorn.conf.py main:appworker 数量怎么定
这个没有万能公式,但有方向:
CPU 密集型任务(数学计算、图像处理):
workers = CPU核心数 + 1不要超过核心数太多,线程切换开销大于收益。
I/O 密集型任务(数据库查询、HTTP调用):
workers = CPU核心数 × 2 + 1 # 同步框架
workers = CPU核心数 × 1 + 1 # 异步框架(因为每个worker内部可以并发)异步框架的 worker 不需要很多,因为单个 worker 就能处理大量并发 I/O。
实际上线后怎么验证:
# 压测工具:locust
# locustfile.py
from locust import HttpUser, task, between
class MyUser(HttpUser):
wait_time = between(0.1, 0.5)
@task
def api_call(self):
self.client.get("/api/data")
# 运行:locust --host=http://localhost:8000 --users=200 --spawn-rate=20看压测期间的 CPU 使用率:
- 如果 CPU 跑满了,再加 worker 也没用,是代码问题
- 如果 CPU 很低但吞吐量上不去,可能 worker 之间有竞争,或者 I/O 是瓶颈
踩坑实录一:preload_app 引发的数据库连接问题
现象: 用了 preload_app = True 之后,偶尔出现数据库报错 "SSL connection has been closed unexpectedly",而且重启之后立刻好。
原因: preload_app = True 会在 master 进程里初始化应用,包括建立数据库连接池。然后 fork 出多个 worker 进程,这些 worker 继承了 master 的文件描述符,包括数据库连接。多个进程共享同一个数据库连接,数据包交叉发送,导致 SSL 状态混乱。
解法: 在 post_fork 钩子里重新初始化连接池:
# gunicorn.conf.py
def post_fork(server, worker):
"""fork 之后,每个 worker 重新建立数据库连接池"""
from myapp.database import engine
engine.dispose() # 关闭从 master 继承来的连接
# 下次使用时会自动重建连接池
print(f"Worker {worker.pid}: database connection pool reset")同样的问题也会出现在 Redis 客户端上,post_fork 里也要重置。
踩坑实录二:timeout 设太短导致正常请求被杀
现象: 有个数据导出接口,用户上传几千行数据,服务端处理要40秒,结果经常出现 "[CRITICAL] WORKER TIMEOUT",worker 被杀掉,用户拿到 502。
原因: timeout=30 设太短了,超过30秒没有响应的 worker 会被 master 认为卡死,强制 kill 掉。
解法:
方案A:加长 timeout。但这会让真正卡死的 worker 更久才被发现。
方案B(推荐):把耗时任务异步化。接口收到请求立刻返回 task_id,把实际处理放到 Celery worker 里,前端轮询任务状态。
方案C:如果必须同步,用 --worker-class gevent 或异步 worker,这类 worker 的 timeout 机制不一样,不会因为正在等 I/O 就被杀。
踩坑实录三:worker 重启风暴
现象: 服务偶尔会短暂不可用,日志里看到大量 worker 在同一时间重启。
原因: 所有 worker 都有相同的 max_requests=1000,如果它们是同时启动的(比如服务刚上线),处理了大约 1000 个请求后会同时触发重启。在重启的那几秒里,所有 worker 都不可用,请求全部失败。
解法: 加 max_requests_jitter:
max_requests = 1000
max_requests_jitter = 200 # 每个 worker 在 1000±200 随机值处重启这样 worker 的重启时间会分散开,不会出现同时重启的情况。
用 systemd 管理进程
不要用 nohup,不要用 screen,用 systemd。
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Python Service
After=network.target
Requires=network.target
[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
Environment="PATH=/opt/myapp/.venv/bin"
EnvironmentFile=/opt/myapp/.env.production
ExecStart=/opt/myapp/.venv/bin/gunicorn -c gunicorn.conf.py main:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target常用命令:
systemctl enable myapp # 开机自启
systemctl start myapp # 启动
systemctl status myapp # 查状态
systemctl reload myapp # 零停机重载(发送 SIGHUP,master 滚动重启 worker)
journalctl -u myapp -f # 实时查日志ExecReload 发送 SIGHUP 信号,Gunicorn master 收到后会滚动重启 worker(一个一个重启,期间保持服务可用),这是零停机部署的关键。
容器化部署注意事项
在 Docker/K8s 里,进程管理有些不同:
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 不要用 root 运行
RUN useradd -m appuser
USER appuser
# 不需要 daemon 模式,容器本身就是进程边界
CMD ["gunicorn", "-c", "gunicorn.conf.py", "main:app"]K8s deployment 配置:
spec:
containers:
- name: myapp
resources:
requests:
cpu: "1"
memory: "512Mi"
limits:
cpu: "2"
memory: "1Gi"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # 等 LB 摘流量再关进程
terminationGracePeriodSeconds: 40最终方案总结
| 场景 | 推荐方案 |
|---|---|
| Flask/Django 生产 | Gunicorn + sync worker |
| FastAPI/Starlette 生产 | Gunicorn + UvicornWorker |
| 开发调试 | uvicorn --reload |
| 进程管理(物理机/VM) | systemd |
| 进程管理(容器) | K8s Deployment + readiness probe |
| 零停机重启 | systemctl reload / kill -HUP |
部署这件事,细节决定稳定性。一行部署命令跑 dev 可以,但生产环境必须把每个配置项的含义搞清楚。
