用 systemd 托管 Python Web 应用:Gunicorn + Flask 开机自启与崩溃自动重启实战

举报
yd_276596855 发表于 2026/11/08 11:34:16 2026/11/08
【摘要】 很多人在云服务器上用 nohup 或 screen 跑 Python 服务,一重启就掉线,进程崩了也没人管。本文以 Flask + Gunicorn 为例,手把手演示如何用 systemd 把应用变成系统服务,实现开机自启、崩溃自动拉起、日志集中查看、平滑重载和资源限制,并附上完整配置文件与常见排错方法。

一、为什么不要再用 nohup 跑服务

刚买云服务器时,最常见的部署方式是这样一行命令:

nohup python 入口文件 > app.log 2>&1 &

它能跑,但问题很多:服务器重启后服务不会自动启动;进程因为异常退出后没人重新拉起;日志文件越写越大,没有轮转;想重启服务还得先 ps 找进程号再 kill。生产环境里,这些都是隐患。

现在主流 Linux 发行版(Ubuntu、Debian、CentOS、EulerOS、openEuler 等)都使用 systemd 作为初始化系统。只要写一个几十行的 unit 文件,就能让应用享受到和 nginx、sshd 一样的待遇:开机自启、失败重启、统一日志、权限隔离。

二、准备一个示例应用

假设项目放在 /opt/myapp,先创建独立的运行用户,避免用 root 跑应用:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp
sudo chown -R myapp:myapp /opt/myapp

创建虚拟环境并安装依赖:

cd /opt/myapp
sudo -u myapp python3 -m venv venv
sudo -u myapp ./venv/bin/pip install flask gunicorn

写一个最简单的 Flask 应用(项目目录下的入口文件 app):

from flask import Flask, jsonify
import os, socket

app = Flask(__name__)

@app.route("/")
def index():
    return jsonify(status="ok", host=socket.gethostname(), pid=os.getpid())

@app.route("/health")
def health():
    return "healthy", 200

先手动验证 Gunicorn 能正常启动:

sudo -u myapp ./venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 入口文件:app

另开一个终端执行 curl 访问本机 8000 端口,返回 JSON 说明应用没问题,按 Ctrl+C 停止。

三、编写 Gunicorn 配置文件

把参数写进配置文件比堆在命令行里更清晰。创建 /opt/myapp/gunicorn.conf(Gunicorn 配置文件):

import multiprocessing

bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
timeout = 30
graceful_timeout = 30
keepalive = 5
accesslog = "-"   # 输出到标准输出,交给 journald 收集
errorlog = "-"
loglevel = "info"

注意 accesslog 和 errorlog 都设置为 “-”,日志会写到标准输出,systemd 会自动收进 journald,后面统一用 journalctl 查看,不用再自己管理日志文件。小内存的云服务器(比如 2 核 2G)可以把 workers 手动设为 3 左右,避免内存吃紧。

四、编写 systemd unit 文件

创建 /etc/systemd/system/myapp.service:

[Unit]
Description=MyApp Flask service (Gunicorn)
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
Environment="PYTHONUNBUFFERED=1"
EnvironmentFile=-/opt/myapp/.env
ExecStart=/opt/myapp/venv/bin/gunicorn -c /opt/myapp/gunicorn.conf 入口文件:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30

Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5

MemoryMax=512M
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true

[Install]
WantedBy=multi-user.target

几个关键配置说明:

  1. Type=notify:Gunicorn 20 以上版本支持 systemd 通知,主进程准备就绪后才算启动成功。如果你的版本不支持,改成 Type=simple 即可。
  2. EnvironmentFile:数据库密码、密钥等敏感配置放在 .env 文件里,前面的减号表示文件不存在也不报错。记得 chmod 600 这个文件。
  3. ExecReload:给 Gunicorn 主进程发 HUP 信号,会平滑重启所有 worker,正在处理的请求不会被中断,适合更新代码后使用。
  4. Restart=on-failure 与 RestartSec=5:进程异常退出 5 秒后自动拉起;StartLimit 两项表示 60 秒内最多重启 5 次,防止程序有致命 bug 时无限重启刷屏。
  5. MemoryMax:限制服务最多使用 512M 内存,防止内存泄漏把整台机器拖死。
  6. NoNewPrivileges、ProtectSystem、PrivateTmp:简单的安全加固,限制进程提权、把系统目录设为只读、使用独立的临时目录。

五、启动并设置开机自启

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

看到 active (running) 就说明成功了。常用管理命令:

sudo systemctl restart myapp   # 重启
sudo systemctl reload myapp    # 平滑重载(更新代码后推荐)
sudo systemctl stop myapp      # 停止
systemctl is-enabled myapp     # 查看是否开机自启

六、用 journalctl 查看日志

journalctl -u myapp -f                    # 实时跟踪日志
journalctl -u myapp --since "1 hour ago"  # 最近一小时
journalctl -u myapp -p err                # 只看错误级别
journalctl -u myapp -n 100 --no-pager     # 最近 100 行

journald 默认会自动轮转。如果担心日志占满磁盘,可以编辑 journald 配置文件,设置 SystemMaxUse=500M,然后执行 sudo systemctl restart systemd-journald。

七、验证崩溃自动重启

我们来模拟一次进程被意外杀死:

sudo systemctl show -p MainPID myapp
sudo kill -9 <上一步得到的 PID>
sleep 6
systemctl status myapp

可以看到服务已经被重新拉起,PID 变了,日志里也会记录一条 “Scheduled restart job”。再执行 sudo reboot 重启服务器,登录后 curl 一下,服务同样已经自动运行。

八、配合 Nginx 对外提供服务

Gunicorn 只监听 127.0.0.1,对外由 Nginx 反向代理,在 Nginx 站点配置里加入:

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;
}

这样应用端口不直接暴露在公网,安全组里只需放行 80 和 443。

九、一键更新部署脚本

把更新流程写成脚本 /opt/myapp/deploy,以后发布只要运行一次:

#!/usr/bin/env bash
set -euo pipefail
cd /opt/myapp
sudo -u myapp git pull --ff-only
sudo -u myapp ./venv/bin/pip install -r requirements.txt
sudo systemctl reload myapp
sleep 2
curl -fsS http://127.0.0.1:8000/health && echo " 部署成功"

十、常见问题排查

  1. status 显示 code=exited, status=203/EXEC:ExecStart 路径写错或文件没有执行权限,检查虚拟环境路径是否正确。
  2. status=217/USER:User 指定的用户不存在,先用 useradd 创建。
  3. Permission denied:运行用户对项目目录没有权限,执行 chown -R myapp:myapp /opt/myapp。
  4. 修改了 service 文件没生效:每次修改后都要先执行 sudo systemctl daemon-reload。
  5. 启动超时:Type=notify 但 Gunicorn 版本过旧不发送通知,升级 Gunicorn 或改为 Type=simple。
  6. 服务反复重启后停止:触发了 StartLimitBurst,先用 journalctl 找到根本错误,修复后执行 sudo systemctl reset-failed myapp 再启动。

总结

用 systemd 托管 Python 应用几乎没有额外成本:一个 unit 文件、几条命令,就能获得开机自启、崩溃自愈、统一日志、平滑重载和资源限制这些生产级能力。这套方法同样适用于 FastAPI(把 Gunicorn 的 worker_class 换成 uvicorn.workers.UvicornWorker)、Django、Celery worker、定时脚本等各种后台进程。下次再部署服务时,不妨和 nohup 说再见。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。