雷灵模板

进程莫名其妙挂了?我配 systemd+Supervisor 两套方案踩过的坑

author
·
11
0
🤖AI摘要
文章探讨了在服务器运行过程中,如何利用systemd和Supervisor来管理和守护进程。文章详细介绍了通过systemd管理系统级服务以及使用Supervisor守护应用级进程的方案,分析了在配置过程中的常见问题,如Type配置错误、Restart策略理解误区和用户权限和环境变量问题,提供了详细的配置指南和代码示例,以帮助读者更好地利用这两套系统提高服务器进程的稳定性。

进程莫名其妙挂了?我配 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)