Django 上线最后一公里:Gunicorn 与 systemd 要配好的几件事

本地开发时 runserver 一跑,一切正常;到了服务器上,很多人还是靠这两招过活——要么继续 runserver,要么 nohup 挂个后台。前者官方文档明确写了不要用于生产,后者在 SSH 断开、进程崩溃、服务器重启这三个时刻必挂无疑。应用代码写完只算走完九十九步,让它在服务器上活着、崩溃能自愈、重启不丢人才是最后一公里。这篇文章把这一公里要配的东西理清楚:Gunicorn 管并发,systemd 管进程,两边的关键配置其实没有几个。
生产环境跑 Django 的标准分工是:Gunicorn 是应用服务器,负责把请求分给多个进程跑你的代码;systemd 是进程管理器,负责把它拉起来、盯住它、崩了重启。nginx(或 Caddy)再站在前面做 TLS 和静态文件,这一层的选型另一篇讲过,本文不重复。
对照之下,两招「土办法」缺的正是这两层:
| 维度 | runserver | nohup 挂后台 | Gunicorn + systemd |
|---|---|---|---|
| 并发能力 | 单进程开发用 | 取决于里面跑什么 | 多 worker 进程池 |
| 崩溃自愈 | 无 | 无,挂了就挂了 | systemd 自动拉起 |
| 开机自启 | 无 | 无 | enable 一条命令 |
| 官方态度 | 明确禁止用于生产 | 无人看护 | Gunicorn 官方推荐部署方式 |
Gunicorn 官方部署文档给的方案就是 systemd:写一个 unit 文件,交给系统托管。这不是可选项,是默认答案。
Gunicorn 的默认配置是给开发调试用的:1 个 worker、sync 模式、30 秒超时。直接搬上生产,流量稍大就会排队。真正要动的是三个参数。
**第一个是 worker 数量。**官方文档给的起点公式是 CPU 核数乘二加一:两核机器配 5 个,四核配 9 个。多数请求的时间花在等数据库、等外部接口上,一个 worker 在等的时候别的 worker 还能干活。但这个公式只是起点,真正的上限是内存——每个 worker 是一个完整进程,Django 项目通常每个占 50–150MB,小内存机器上多配几个就可能被 OOM 杀掉。配完看一眼实际内存,超了就减。
**第二个是超时。**这是最容易踩的坑:timeout 默认 30 秒,但它不是「请求最多跑 30 秒」的时间上限,而是 worker 心跳静默超过 30 秒就会被主进程杀掉重启。sync worker 处理请求时会完全占住自己,一个要跑 40 秒的合法慢请求,就能让 worker 静默 40 秒——于是被杀、请求作废、用户拿到 502,日志里一行 WORKER TIMEOUT。
正确做法分两层:把 timeout 调到比最慢的合法请求略大,这是止血;根治是把长任务挪到后台队列,别让 HTTP 请求扛着跑。
**第三个是 worker 类型。**sync 是默认值,一个 worker 同一时刻只处理一个请求;请求大多是等数据库、等接口的 I/O 型时,换 gthread 更划算——每个 worker 内开线程池,同样的进程数扛住几倍并发。两种模式的取舍:
| 维度 | sync | gthread |
|---|---|---|
| 并发模型 | 每进程一次一个请求 | 每进程一个线程池 |
| 内存 | 进程多,内存高 | 进程少,内存省 |
| 隔离性 | 请求之间完全隔离 | 同进程线程共享状态 |
| 适合 | CPU 型、追求稳 | I/O 型、小内存机器 |
三个参数合起来,最小可用配置长这样:
max_requests 让 worker 每处理一千个请求就换一个新进程,是给潜在内存泄漏上的保险;加 jitter 是为了错开重启时间,别让几个 worker 同一瞬间集体下线。
Gunicorn 自己不管自己的死活,托管这件事归 systemd。unit 文件的最小版本:
几个字段值得逐个说清:
启用并开机自启:
日志不用自己管,systemd 直接收进 journal:journalctl -u gunicorn -f 实时看,-n 100 看最近一百行。排查问题的第一现场就在这里。
**坑一:reload 和 restart 用混。**发完新代码跑 systemctl reload,新代码没生效——因为 reload 只重载 Python 代码,不重读 unit 文件。改了 ExecStart 里的参数(比如 worker 数)必须 daemon-reload 加 restart。判断标准很简单:改代码用 reload,改 unit 用 restart。
**坑二:502 排查没有章法。**nginx 报 502 时按固定顺序查三层:先 systemctl status gunicorn 看服务活着没有;再对 socket 或端口的路径和权限——nginx 用户连不上 socket 是高频原因;最后才看应用本身有没有启动报错。从外往里查比瞎猜快得多。
**坑三:指望 Gunicorn 顺便伺候静态文件。**它不做这件事。上线前 collectstatic 收拢静态文件,nginx 直接接管 /static/ 路径;不想起 nginx 的极简场景,WhiteNoise 让应用自己带静态文件服务。漏掉这一步的症状很典型:站点通了,但页面裸奔没有样式。
这一公里的投入有个特点:一次配好,之后每次发版都是两条命令的事。Gunicorn 三个参数按公式起步、按内存封顶,systemd 一个 unit 文件换来崩溃自愈和开机自启——总共不到二十行配置,换的是半夜不用爬起来拉进程。
如果这篇文章帮你把站点从「nohup 勉强活着」解救出来,欢迎转发给还在这么干的朋友,也欢迎在评论区聊聊你的部署方案。
参考文章
#Django #Gunicorn #systemd #PythonWeb #后端部署 #运维实战