Supervisor 进程守护实战:给非 systemd 的长驻脚本做自启、崩溃自动拉起与 Web 界面管理

Supervisor 到底解决什么问题:当 systemd 不适用时的进程守护

个人站长跑的东西很杂:一个 Python 爬虫要常驻、一个 Node.js 小服务要自启、一个队列消费者断了要自动拉起。很多人第一反应是 systemd,但实际用起来常遇到几个坎——脚本跑在 Docker 容器里没有 systemd、进程是前台交互式的、或者你就想要一个能看日志能一键重启的 Web 界面。这时候 Supervisor 是更顺手的选择。

Supervisor 是一个用 Python 写的进程控制系统,核心能力就三样:把进程变成后台守护进程、进程挂了自动重启、统一管理日志与状态。它比 systemd 直观,配置全在一个 INI 文件里;又比 nohup/screen 靠谱,因为它是真正的守护进程管理器。本文从零开始,在一个 Debian/Ubuntu 服务器上搭一套能用的 Supervisor,并解决几个最常见的坑。

安装:系统包还是 pip,怎么选

Debian/Ubuntu 直接装最省心,会同时帮你配好 systemd 服务单元:

apt update
apt install -y supervisor
systemctl status supervisor

装完后你会得到两个关键路径:

  • 主配置:/etc/supervisor/supervisord.conf
  • 子配置目录:/etc/supervisor/conf.d/(你的每个进程写一个 .conf 放这里)

如果你在容器里或想用最新版,才用 pip:pip install supervisor,然后手动生成配置 echo_supervisord_conf > /etc/supervisord.conf。但对绝大多数站长,apt 版足够,而且自带开机自启。

写第一个 program 配置块

假设你有一个 Python 脚本 /opt/myapp/worker.py,需要常驻运行。在 /etc/supervisor/conf.d/myapp.conf 里写:

[program:myapp]
directory=/opt/myapp
command=/opt/myapp/venv/bin/python worker.py
autostart=true
autorestart=true
startsecs=5
startretries=3
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/myapp.out.log
stderr_logfile=/var/log/supervisor/myapp.err.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=5
environment=PYTHONUNBUFFERED="1",ENV="prod"

逐行拆解这些参数,它们决定了守护是否可靠:

  • directory:进程的工作目录,很多脚本用相对路径读文件,配错会直接启动失败。
  • command:必须是绝对路径。Supervisor 不读取你的 shell 环境,PATH 里找不到命令是新人第一大坑。
  • autostart / autorestart:开机自启 + 崩溃自动拉起,这是守护的核心价值。
  • startsecs=5:进程启动后要稳定运行 5 秒才算"启动成功",防止那种秒退的假启动被误判。
  • startretries=3:连续启动失败 3 次后放弃并标记为 FATAL,避免无限重启刷爆日志。
  • user:用非 root 用户运行,安全基线。注意它不会自动创建用户,得先 useradd。
  • redirect_stderr=true:把 stderr 合并进 stdout 的日志文件,省一个文件,排查时也不用两头看。
  • PYTHONUNBUFFERED=1:Python 必须加,否则输出被缓冲,日志半天不刷新,你会误以为程序卡死。

加载配置:update 与 reread 的区别

很多人改完配置发现没生效,是因为用错了命令。Supervisor 的配置重载分两步,理解它们的区别很关键:

# 只重新读取磁盘上的配置文件,不改变运行中的进程
supervisorctl reread

# 应用改动:启动新增的程序、重启配置变化的程序,并显示发生了什么
supervisorctl update

# 查看所有进程状态
supervisorctl status

reread 只是让 Supervisor 知道"文件变了",update 才真正执行变更。正常流程是 reread 接着 update。如果只是改了某个程序的参数(比如日志级别),update 会自动重启它;如果只是新增程序,update 会启动它。状态里几个常见的词要认识:

  • RUNNING:正常运行,这是你要的。
  • FATAL:启动失败次数超限,一般是命令写错或依赖缺失,去看 err.log。
  • BACKOFF:正在重试启动的过渡态。
  • STOPPED:被手动停了,或 autostart=false 且还没启。

用 Web 界面统一管理(可选但香)

Supervisor 自带一个 HTTP 管理界面,对不熟悉命令行的站长或者想远程看状态的人很友好。开启方法是在主配置的 [inet_http_server] 段:

[inet_http_server]
port=127.0.0.1:9001
username=admin
password=你的强密码

注意默认是 127.0.0.1,只能本机访问。千万不要直接开 0.0.0.0 暴露公网——它只是个 HTTP 服务,没有 TLS,暴露出去等于把服务器控制权送人。正确做法是用 Nginx 反代并加 Basic Auth:

location /supervisor/ {
    proxy_pass http://127.0.0.1:9001/;
    proxy_set_header Host $host;
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
}

生成密码文件用 htpasswd -c /etc/nginx/.htpasswd admin,没装就 apt install apache2-utils。

日志管理:别让日志把磁盘撑满

Supervisor 不会自动 rotate 日志——stdout_logfile_maxbytes 和 stdout_logfile_backups 就是它的内置轮转机制。设 maxbytes=50MB、backups=5 意味着最多占 250MB 循环覆盖。如果你更习惯系统的 logrotate,也可以关掉内置轮转,改用 /etc/logrotate.d/ 配置。但二选一即可,两个都开容易互相打架。查日志时:

tail -f /var/log/supervisor/myapp.out.log
# 或者用 supervisorctl 直接看(会抓最后一段)
supervisorctl tail -f myapp stdout

用 eventlistener 做进程崩溃告警

Supervisor 有个很多人不知道的能力:事件监听器。当进程状态变化(如从 RUNNING 变 FATAL、或反复重启)时,Supervisor 会发出事件,你可以写一个监听器脚本,捕获后发通知。对个人站长来说,最实用的就是"进程挂了给我发邮件/发消息"。

主配置里定义监听器:

[eventlistener:crashmail]
command=/usr/local/bin/crashmail.py
events=PROCESS_STATE_FATAL,PROCESS_STATE_EXITED
buffer_size=10

然后写 /usr/local/bin/crashmail.py。事件监听器的协议是:从 stdin 读取头部 ver:3.0 server:supervisor ...,然后回一行 READY\n 表示就绪,Supervisor 再推送事件体,处理完要回 RESULT 2\nOK。简化的脚本骨架:

#!/usr/bin/env python3
import sys, subprocess

def write_stdout(s):
    sys.stdout.write(s); sys.stdout.flush()

def write_stderr(s):
    sys.stderr.write(s); sys.stderr.flush()

def main():
    while True:
        write_stdout('READY\n')          # 告诉 supervisor 我准备好了
        line = sys.stdin.readline()       # 读取事件头部
        headers = dict(kv.split(':') for kv in line.strip().split(' '))
        payload = sys.stdin.read(int(headers['len']))
        if 'FATAL' in payload or 'EXITED' in payload:
            subprocess.run(['/usr/bin/curl', '-s', '-m', '10',
                '-d', 'process down: ' + payload[:200],
                '你的告警webhook地址'])
        write_stdout('RESULT 2\nOK')      # 确认处理完成

if __name__ == '__main__':
    main()

有了它,你就不必天天盯着 supervisorctl status,进程一崩立刻收到风声。注意监听器脚本自身也必须由 Supervisor 管理(它就是一个 program),否则主进程重启时它也会丢。

资源限制:别让守护进程拖垮服务器

Supervisor 启动的子进程默认继承系统资源限制。如果你的应用有内存泄漏,它可能一路吃到 OOM。可以在 program 段里加限制(需要 Supervisor 较新版本支持 minfds 等):

[program:myapp]
command=/opt/myapp/venv/bin/python worker.py
minfds=1024
minprocs=200
# 用 systemd-run 包裹来限制内存(更细粒度)
# command=/usr/bin/systemd-run --scope -p MemoryMax=512M /opt/myapp/venv/bin/python worker.py

更彻底的做法是配合 systemd-run 给进程加 cgroup 内存上限,这样即使程序泄漏也只会杀掉自己而不是整台机器。对 1G 内存的小 VPS 来说,这条保险非常值。

优雅停止:stopasgroup 与 killasgroup

很多程序会 fork 子进程(比如 Gunicorn、Celery)。如果只停父进程,子进程可能变成孤儿继续跑,占着端口导致下次启动失败。这两个参数就是解药:

[program:celery]
command=/opt/app/venv/bin/celery -A app worker
stopasgroup=true
killasgroup=true
stopsignal=TERM
stopwaitsecs=30
  • stopasgroup=true:停止时把信号发给整个进程组,子进程一起收。
  • killasgroup=true:stopwaitsecs 超时后,用 SIGKILL 强杀整组。
  • stopsignal=TERM:先发 TERM 让程序优雅退出,配合 stopwaitsecs 给它 30 秒收尾。
  • stopwaitsecs=30:等待优雅退出的秒数,处理长任务要调大。

没有这几个参数,Celery 这类挂着子进程的服务会反复出现"端口被占用"的诡异问题,排查起来极费时间。

从 systemd 迁移到 Supervisor 的对照表

如果你以前用 systemd 管理服务,迁移到 Supervisor 只需对照几个概念。下面这张表能帮你快速完成心智转换:

  • systemd 的 [Service] ExecStart= → Supervisor 的 command=(都要求绝对路径)。
  • systemd 的 Restart=always → Supervisor 的 autorestart=true。
  • systemd 的 WorkingDirectory= → Supervisor 的 directory=。
  • systemd 的 User= → Supervisor 的 user=(注意不会自动建用户)。
  • systemd 的 systemctl status/restart → supervisorctl status/restart 名字。
  • systemd 的 systemctl daemon-reload → supervisorctl reread 加 update。

理解了这个映射,你会发现 Supervisor 并不是 systemd 的替代品,而是它的一个更轻、更集中、更适合"管理一堆异构脚本"的互补工具。两者的日志模型也不同:systemd 默认把日志交给 journald,Supervisor 则自己写文件,所以前面讲的日志轮转配置格外重要。

五个高频故障排查

  1. 程序起不来,状态 FATAL:99% 是 command 路径写错,或者用了相对路径。手动执行一遍那条完整命令,看能不能跑。
  2. Docker 容器里 Supervisor 不能自启:容器没有 systemd,需在启动脚本或 Dockerfile 的 ENTRYPOINT 里手动拉 supervisord -c /etc/supervisor/supervisord.conf。
  3. 改了配置 update 后没反应:检查缩进和拼写,Supervisor 的 INI 对空格敏感度不高但对键名严格。用 supervisorctl status 确认状态,再看主配置里 [include] 段是否包含了你的 conf.d 目录。
  4. 日志文件不更新:Python 没加 PYTHONUNBUFFERED,输出还在缓冲区里。加环境变量重启即可。
  5. 进程显示 RUNNING 但实际没干活:进程可能卡死但没退出。Supervisor 不管"活着但僵死"的情况,可以加 stopasgroup=true 并在程序内部加心跳自检,或者上 healthchecks 类的外部监控。

总结:什么时候该用 Supervisor

如果你在裸机上跑服务,systemd 通常是更第一优先、更现代的选择。但当你面临容器环境、需要 Web 界面、或管理一堆异构脚本任务时,Supervisor 的集中式配置和直观状态管理会让你省心很多。记住三个核心原则:command 用绝对路径、改配置走 reread + update、Web 界面绝不裸奔公网。把这三条守住,Supervisor 就能成为你服务器上一个安静可靠的"保姆"。

Last modification:October 3rd, 2026 at 01:24 pm

Leave a Comment