很多人写了自己的 Python Web 应用——可能是个小工具、一个 API 服务、或者一个自动化后台——然后在服务器上直接用 python app.py 跑起来,终端一关就断,重启服务器就没了。这种做法只能叫"能跑",离"能上线"还差得远。本文把从裸奔脚本到生产可用的完整链路讲清楚:Gunicorn 作为应用服务器,Nginx 作为反向代理与静态资源服务器,systemd 负责进程守护与开机自启。
为什么不能直接用 python app.py
Flask 自带的开发服务器(flask run)、Django 的 runserver,官方文档里都明确写着"仅供开发使用"。原因有三:
- 性能极差:开发服务器单进程、单线程、不支持并发,一个请求卡住全部阻塞。生产环境需要多进程/多协程模型。
- 没有安全加固:不处理慢请求、不做请求体大小限制、异常处理粗糙,容易被拖垮。
- 不能托管静态资源:Python 直接吐 CSS、图片、JS 效率极低,而这些恰恰是站点访问量最大的部分。
正确的架构是三层:Nginx(最外层,处理静态资源、TLS、限流、转发)→ Gunicorn(WSGI 应用服务器,跑 Python 代码,多 worker)→ 你的应用。
Gunicorn 的角色与 worker 模型
Gunicorn(Green Unicorn)是一个成熟的 WSGI HTTP 服务器。它本身不做反向代理,也不处理静态文件,只负责按照 WSGI 协议调用你的应用,并管理多个 worker 进程。
它的 worker 类型是性能调优的核心:
- sync:同步 worker,一次处理一个请求,简单稳定,适合 CPU 密集或逻辑简单、响应快的应用。默认类型。
- gthread:带线程池的同步 worker,一个进程内多线程并发,适合有阻塞 I/O(比如调外部 API、读数据库)的应用,是很多场景下的稳妥选择。
- gevent / eventlet:基于协程的异步 worker,能支撑高并发长连接,但需要代码兼容(monkey patch 有坑)。
- uvicorn worker:如果你的应用是 ASGI(FastAPI、Starlette),用
uvicorn.workers.UvicornWorker才能发挥异步优势。
worker 数量的经典公式是 (2 × CPU 核数)+ 1。但这不是铁律——如果应用内存占用大,就要减少 worker 数避免 OOM;如果是 I/O 密集,可以适当增加线程数而非进程数。先用默认公式起步,再根据实际负载调整。
安装与最简启动
sudo apt update sudo apt install python3-venv python3-pip nginx -y
强烈建议在虚拟环境里装应用依赖,而不是污染系统 Python:
sudo mkdir -p /srv/myapp cd /srv/myapp sudo python3 -m venv venv sudo ./venv/bin/pip install --upgrade pip sudo ./venv/bin/pip install gunicorn flask # 把你的应用放进来,假设入口是 app.py 里的 app 对象
手动测试一下 Gunicorn 能否拉起应用:
cd /srv/myapp ./venv/bin/gunicorn --bind 127.0.0.1:8000 app:app
这里的 app:app 意思是"模块 app.py 里的 WSGI 可调用对象 app"。如果是 Django 项目,通常是 项目名.wsgi:application。确认能访问后再继续。
写一个生产级的 Gunicorn 配置文件
把所有参数塞进命令行不利于维护。新建 /srv/myapp/gunicorn.conf.py:
# 监听本地回环,外部访问一律走 Nginx bind = "127.0.0.1:8000" # worker 数量:2*CPU+1,按实际调整 workers = 3 worker_class = "gthread" threads = 4 worker_tmp_dir = "/dev/shm" # 超时与重启 timeout = 60 graceful_timeout = 30 keepalive = 5 max_requests = 1000 max_requests_jitter = 100 # 日志 accesslog = "/var/log/myapp/gunicorn_access.log" errorlog = "/var/log/myapp/gunicorn_error.log" loglevel = "info" # 进程管理 proc_name = "myapp" pidfile = "/run/myapp/gunicorn.pid"
几个参数的用意值得解释:
- worker_tmp_dir = /dev/shm:gthread 和 sync worker 需要心跳临时文件,放到内存盘能减少磁盘 I/O,避免某些文件系统下的性能问题。
- max_requests:每个 worker 处理若干请求后自动重启,能有效缓解潜在的内存泄漏。配一个
jitter让各 worker 错峰重启,避免同一时刻全部下线。 - graceful_timeout:收到重启信号后,等待正在处理的请求完成再退出,实现平滑重启。
- timeout:worker 处理超过这个秒数没响应就被杀掉重启,防止偶发的死锁把整个服务拖垮。
用 systemd 守护进程
不要用 nohup,也不要用 screen。用 systemd 才是正道。新建 /etc/systemd/system/myapp.service:
[Unit] Description=My Python Web App (Gunicorn) After=network.target [Service] Type=notify User=www-data Group=www-data WorkingDirectory=/srv/myapp Environment="PATH=/srv/myapp/venv/bin" ExecStart=/srv/myapp/venv/bin/gunicorn -c /srv/myapp/gunicorn.conf.py app:app ExecReload=/bin/kill -s HUP $MAINPID KillMode=mixed TimeoutStopSec=5 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
说明几点:
Type=notify需要 Gunicorn 检测到 systemd 支持;如果报错可改成Type=simple。- 用
User=www-data降权运行,绝不要用 root 跑 Web 应用。 ExecReload发 HUP 信号给主进程,Gunicorn 会优雅重启 worker(零停机重载代码)。Restart=on-failure让进程意外退出时自动拉起。
启动并设置开机自启:
sudo mkdir -p /var/log/myapp /run/myapp sudo chown -R www-data:www-data /var/log/myapp /run/myapp /srv/myapp sudo systemctl daemon-reload sudo systemctl enable --now myapp sudo systemctl status myapp
Nginx 反向代理配置
应用只监听 127.0.0.1:8000,真正对外的是 Nginx。新建 /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name example.com www.example.com;
client_max_body_size 20m;
# 静态资源由 Nginx 直接处理,不经过 Python
location /static/ {
alias /srv/myapp/static/;
expires 30d;
access_log off;
add_header Cache-Control "public";
}
location / {
proxy_pass http://127.0.0.1:8000;
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;
proxy_redirect off;
# 长连接与超时
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 60s;
proxy_connect_timeout 5s;
}
}关键点:
X-Forwarded-*头必须传,否则你的应用里拿到的客户端 IP 全是 127.0.0.1,日志和风控全废。应用侧还要正确读取这些头(比如 Flask 用 ProxyFix 中间件)。- 静态资源交给 Nginx,这是本架构最重要的性能收益。
alias路径注意结尾的斜杠,location /static/配alias /srv/myapp/static/,否则路径会拼错导致 404。 proxy_http_version 1.1+ 清空Connection头,启用与 Gunicorn 之间的 keepalive 连接,减少 TCP 握手开销,配合 Gunicorn 的keepalive参数效果明显。
启用站点并重载:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx
加上 HTTPS 与平滑部署
用 Certbot 一行搞定证书:
sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d example.com -d www.example.com
更新代码时的正确姿势(不停机):
cd /srv/myapp git pull ./venv/bin/pip install -r requirements.txt sudo systemctl reload myapp # 发 HUP,Gunicorn 平滑重启 worker
systemctl reload 正是依靠前面 service 文件里的 ExecReload=/bin/kill -s HUP。Gunicorn 收到 HUP 后会启动新 worker、等旧 worker 处理完手头请求再退出,用户几乎感知不到。
深入一点:worker 模型怎么选才对
前面提到 worker 类型,这里展开讲怎么根据你的应用特征做选择,这是 Gunicorn 调优里最容易拍脑袋、也最影响结果的一环。
判断标准就一条:你的请求主要时间花在哪儿。如果花在 CPU 计算上(图片处理、加解密、复杂计算),那并发再高也没用,必须靠更多进程横向堆 CPU,选 sync、进程数接近或略超过核数即可。如果花在等待上(查数据库、调第三方 API、读文件),那进程大部分时间在"闲等",此时 gthread 配适量线程更划算——一个进程内多个线程可以在等 I/O 时切换去处理别的请求。如果并发量特别大且是 I/O 密集,gevent 协程模型吞吐最高,但要小心 monkey patch 与某些 C 扩展库不兼容。
一个实用的验证方法:先用 sync 跑基线,然后用 wrk 或 ab 压测,记录 QPS 和响应时间;再换成 gthread、调整线程数,重复压测对比。数据会告诉你哪种更适合,而不是靠网上抄来的公式。压测命令示例:
wrk -t4 -c100 -d30s http://127.0.0.1:8000/ # 观察 Results 里的 Requests/sec 和 Latency 分布
另外要盯住系统层面的指标:top -H -p $(cat /run/myapp/gunicorn.pid) 看每个 worker 的 CPU 占用,free -m 看内存是否吃紧。如果一个 worker 长期 100% CPU,说明它被某个慢请求卡死了,这时候 timeout 和 max_requests 就是你的保险丝。
日志与监控:别等出事才翻记录
Gunicorn 默认的 accesslog 格式信息有限。建议自定义日志格式,把处理耗时加进去,这样能一眼看出哪些请求慢:
access_log_format = '%({x-forwarded-for}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
其中 %(D)s 是请求处理的微秒数,%(b)s 是响应字节数。%(f)s 和 %(a)s 是 Referer 和 User-Agent。有了耗时字段,用 awk 一秒筛出最慢的请求:
awk -F'[ ]' '$(NF) > 1000000 {print}' /var/log/myapp/gunicorn_access.log | tail -20同时把 Nginx 的 access log 也打开并统一格式。两层日志配合,可以判断慢是慢在 Nginx 转发、Gunicorn 排队、还是应用内部逻辑——这对定位性能问题至关重要。
再往前走一步:用 systemd 的 journal 收集 Gunicorn 的输出(journalctl -u myapp -f),或者接入更专业的日志聚合。当服务半夜崩了、早上你只知道"网站打不开"时,一份带时间戳和耗时的日志能省下大量排查时间。
排查与常见坑
- 502 Bad Gateway:Nginx 连不上 Gunicorn。先
systemctl status myapp看应用是否活着,再ss -tlnp | grep 8000确认端口在听,最后看gunicorn_error.log。最常见原因是应用启动报错(比如导入路径不对、缺依赖)。 - 改了代码不生效:Gunicorn 不会自动重载。要么 reload,要么之前根本没 pull 成功。开发阶段可以加
--reload,生产环境绝不要开。 - 静态文件 404:检查
alias路径结尾斜杠、目录权限(www-data 要能读)。Django 还要先跑collectstatic。 - 拿到真实 IP 全是 127.0.0.1:应用没解析
X-Forwarded-For。Flask 加ProxyFix,Django 配USE_X_FORWARDED_HOST和SECURE_PROXY_SSL_HEADER。 - worker 数配太多导致 OOM:每个 worker 都独立加载一份应用,内存是叠加的。
free -m看剩余内存,用2*CPU+1起步,内存紧张就减。 - 上传大文件失败:
client_max_body_size默认为 1m,Nginx 会直接 413。按需调大,并注意 Gunicorn 的limit_request_line等参数。
把这套搭好之后,你的 Python 应用才真正具备"生产可用"的素质:能开机自启、崩溃自恢复、支持并发、静态资源高效、代码可平滑热更新。它带来的稳定性提升是质变性的——以前终端一关服务就断、服务器一重启应用就没了、线上出问题只能靠猜;现在这些都不再是问题。整套配置加起来不过几个文件、几十行内容,却是一套可以长期复用、复制到任何新项目的基础设施。对每个用 Python 做服务、又想让站点稳定运行的站长来说,这都是一次性投入、长期受益的必修课,值得认真对待。