进程莫名其妙挂了?我配 systemd+Supervisor 两套方案踩过的坑
进程莫名其妙挂了?我配 systemd+Supervisor 两套方案踩过的坑
上个月一台跑着定时任务的服务器半夜挂了,第二天早上我才发现数据缺了一大截。查日志发现是 OOM killer 把 Python 进程干掉了,重启脚本没生效——因为我用 nohup 起的,压根没配进程守护。这就跟我之前以为"nohup & 就够了"一样,属于典型的运维幻觉。
这篇文章把我后来用的两套方案整理出来:systemd 管系统级服务,Supervisor 管应用级进程。都是我自己踩坑后定下来的配置,不复杂,但有几处细节不注意就会翻车。
systemd:给进程上一份正式编制
systemd 现在是 Linux 标准 init 系统,CentOS 7+、Ubuntu 16.04+ 默认都有。用它管进程最大的好处是:开机自启、异常退出自动重启、日志统一用 journalctl 查。
基础 unit 文件
在 /etc/systemd/system/ 下面建一个 .service 文件,以我之前管一个 Flask 应用为例:
[Unit]
Description=My Flask App
After=network.target
[Service]
Type=simple
User=www
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
然后:
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp
看起来简单,但我第一次配的时候踩了几个坑。
坑一:Type=forking 还是 Type=simple
这个选错了进程状态会乱。简单说:
- Type=simple:ExecStart 启动的进程本身就是主进程。绝大多数场景用这个。
- Type=forking:ExecStart 启动后 fork 一个子进程跑,父进程退出。老式的 Nginx、PHP-FPM 默认行为用这个。
我当时给一个 Python 脚本配了 forking,结果 systemd 以为进程启动了实际上 fork 失败,状态一直卡在 activating,systemctl status 显示"code=exited, status=0/SUCCESS"——明明没跑起来,systemd 却说成功了。
一个快速判断方法:直接跑你的 ExecStart 命令,看它是在前台 block 住还是执行完就退出。block 住 = simple,立即退出 = 大概率 forking(需要配合 PIDFile)。
坑二:Restart=always 并不总是重启
这个我之前理解错了。Restart=always 的意思是"进程退出时总是重启",但它有一个前提——进程确实退出了。如果你的进程卡死但不退出,systemd 不会管你。
解决方法是加超时和看门狗:
[Service]
...
Restart=always
RestartSec=5
WatchdogSec=30
TimeoutStartSec=60
TimeoutStopSec=30
WatchdogSec=30:应用需要每 30 秒给 systemd 发一个 sd_notify 心跳。不发就认为卡死了,强制重启。- 你的程序需要支持 sd_notify。Python 可以这么写:
import sdnotify
n = sdnotify.SystemdNotifier()
n.notify("READY=1")
# 在循环里定时调 n.notify("WATCHDOG=1")
如果懒得改代码,也可以用 Restart=on-failure + RuntimeMaxSec 设最大运行时间,到时间强制重启——虽然粗暴,但确实能解决卡死不退出的问题。
坑三:User 权限和环境变量
systemd 启动的进程环境非常干净,不读 .bashrc、.profile。我之前在 cron 里能跑的 Python 脚本,搬到 systemd 里就因为找不到 venv 路径挂了。
解决:在 unit 文件里显式设环境变量:
[Service]
Environment="PATH=/opt/myapp/venv/bin:/usr/local/bin:/usr/bin:/bin"
Environment="APP_ENV=production"
EnvironmentFile=-/etc/default/myapp
加 - 前缀表示文件不存在也不报错。
Supervisor:管 Python 脚本更顺手
systemd 功能强但配起来重。如果只是管几个 Python 脚本、队列 worker、定时任务,Supervisor 更轻量。它的好处是自带 web 管理面板,可以手动起停单个进程,不用每次都 ssh 上去敲 systemctl。
安装和基础配置
pip install supervisor
echo_supervisord_conf > /etc/supervisord.conf
基础配置:
[unix_http_server]
file=/var/run/supervisor.sock
[supervisord]
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/supervisor
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
[inet_http_server]
port=127.0.0.1:9001
username=admin
password=your_password_here
然后每个进程单独写一个 conf,丢到 /etc/supervisor/conf.d/:
[program:task-worker]
command=/opt/myapp/venv/bin/python /opt/myapp/worker.py
directory=/opt/myapp
user=www
autostart=true
autorestart=true
startsecs=5
stopwaitsecs=10
redirect_stderr=true
stdout_logfile=/var/log/supervisor/worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
坑四:Supervisor 本身挂了怎么办
这是我在生产上真遇到的——Supervisor 进程 OOM 被杀,下面所有进程全没了。因为 Supervisor 是单点,它一挂,所有子进程就被 init 接管,没人帮你重启了。
解决方案:用 systemd 管 Supervisor 本身。
[Unit]
Description=Supervisor daemon
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/bin/supervisord -c /etc/supervisord.conf
ExecReload=/usr/local/bin/supervisorctl reload
ExecStop=/usr/local/bin/supervisorctl shutdown
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
这样就算 Supervisor 挂了,systemd 会在 10 秒后拉起来。
坑五:stopwaitsecs 设太短
Supervisor 默认发 SIGTERM 后等 10 秒,然后发 SIGKILL。如果你的 worker 处理一个任务需要 30 秒,10 秒不够优雅退出就会丢数据。
调大:
stopwaitsecs=60
stopsignal=INT
stopsignal=INT 让 Python 收到 KeyboardInterrupt,可以在代码里 catch 它做收尾:
import signal, sys
def graceful_shutdown(signum, frame):
print("收到退出信号,正在保存当前任务...")
# 保存状态,关闭连接
sys.exit(0)
signal.signal(signal.SIGINT, graceful_shutdown)
systemd vs Supervisor:什么时候用哪个
| 场景 | systemd | Supervisor |
|---|---|---|
| Web 服务(Nginx/Gunicorn) | ✅ 首选 | 也行但重 |
| 系统级守护进程 | ✅ | ❌ |
| Python 脚本/Worker | 可以 | ✅ 更顺手 |
| 需要 Web 面板手动管理 | 不行 | ✅ |
| 开机自启 | ✅ | 需要 systemd 兜底 |
| 资源限制(CPU/内存) | ✅ cgroup 原生 | ❌ |
实际我是混着用的。Nginx、Redis、MySQL 走 systemd;自己写的 Python 脚本、Celery worker、定时任务走 Supervisor。然后 Supervisor 本身用 systemd 管——套娃,但稳。
总结
进程守护这件事,本质上是"别让你的进程裸奔"。nohup & 不是方案,是临时的测试手段。正经跑在服务器上的东西,要么进 systemd,要么进 Supervisor。
几个最容易犯的错再说一遍:
- Type=simple vs forking 别选错
- Restart=always 不防卡死,加 WatchdogSec
- systemd 不读环境变量,手动配 Environment
- Supervisor 用 systemd 兜底
- stopwaitsecs 要根据任务执行时间调
我后来把服务器上所有 nohup 起的进程全部迁到了这两套方案里,再也没半夜被报警吵醒过。
配完记得 systemctl daemon-reload && systemctl restart supervisord,然后去睡个好觉。
评论 (0)