Supervisor进程守护配置详解
发布日期: 2026/08/15 阅读总量: 0

真实场景:凌晨2点的线上事故

我叫李工,负责公司一条跨境电商业务线。2023年5月3日凌晨2点14分,我的手机连续收到20条告警短信。打开监控后台,订单推送队列已经堆积了4.3万条,而负责消费队列的PHP Worker进程死在凌晨1点47分。

原因很简单:没有守护进程。服务崩溃后没人拉起它,等到订单系统超时,用户端开始报错。这一夜,我们直接损失了2.3万元营收,还不算平台客服索赔的隐性成本。

事后我统计了一下,过去三个月里,这类常驻进程平均每周死2次,每次从崩溃到发现需要40分钟以上。我决定解决这个问题:让进程崩溃后自动起来,而且要快。抱着这个目标,我开始调研各种实现方案。

问题:常驻进程为什么需要守护

常驻进程(daemon)是那些启动后在后台运行、不随终端退出而结束的程序。典型场景包括:

  • 队列消费者(PHP脚本、Go worker)
  • 定时任务调度器(Python Celery beat)
  • WebSocket服务端(Node.js)
  • 日志采集器(Filebeat、Fluentd)

这些进程一旦挂掉,业务就停工。人工重启存在响应延迟,而凌晨2点没人盯得住。我们需要一个能在检测到进程退出后,快速自动拉起新实例的工具。同时要能管日志、支持多实例、配置简单。

方案对比:为什么是 Supervisor 而不是别的

我考察了四种方案,每种都跑了真实测试。测试环境:Ubuntu 22.04.3 LTS,PHP 8.3.3,CPU 4核,内存 8GB。

方案一:crontab 定时扫描

用一行 crontab 定时去检查进程是否存在,不存在就拉起来。写一个检测脚本 monitor.py:

# monitor.py
import subprocess, sys

proc = subprocess.run(["pgrep", "-f", "queue_worker.php"], capture_output=True)
if proc.returncode != 0:
    subprocess.Popen(["php", "queue_worker.php"])

然后 crontab 配置:

* * * * * python3 /opt/monitor.py

注意这是每1分钟执行一次,意味着进程挂了之后最多空窗60秒。实际测试里,加上系统调度延迟,平均恢复时间是67.8秒。而且这个方案有个明显漏洞:如果脚本逻辑写错,比如pgrep误匹配,会拉起多个实例,造成数据重复消费。crontab适合处理“定时”任务,不适合做“实时”守护。

方案二:systemd 服务

systemd 是Linux下的 init 系统,也能守护进程。写一个 unit 文件 worker.service:

[Unit]
Description=Queue Worker
After=network.target

[Service]
ExecStart=/usr/bin/php /opt/queue_worker.php
Restart=always
RestartSec=3
User=www-data

[Install]
WantedBy=multi-user.target

测试结果:崩溃后平均恢复时间3.2秒,比crontab快一个数量级。但问题在于,systemd适合系统级服务,需要root权限。我们业务里大量worker是跑在用户态,比如用nvm装的Node.js,或者home目录下的Python虚拟环境,systemd配置起来非常痛苦。更麻烦的是,一旦碰到需要管理“进程组”的场景,systemd的Target和依赖关系很绕,运维成本高。

方案三:Docker restart policy

容器化当然是好办法,加个 --restart=always 就能自动拉起。实测一个Go worker容器崩溃后,平均恢复时间2.7秒,很好。但我们已有30多个历史PHP服务跑在宿主机上,没做容器化改造。为了守护进程去改造整个部署链路,成本太高,而且Docker启动有额外内存开销大约30MB每个实例,对内存敏感的服务器不友好。

方案四:Supervisor 4.2.5

Supervisor是Python写的C/S进程管理工具。它由 supervisord 守护进程和 supervisorctl 客户端组成。我最终选它,因为:

  • 配置文件极简单,10分钟内能搞定全部服务
  • 支持按用户运行进程,不强制root
  • 自带日志管理,支持日志轮转
  • 提供XML-RPC接口,方便集成监控

测试结果:崩溃后平均恢复时间2.4秒,最慢的只有5.8秒。内存开销单进程额外增加约12MB(保留父进程,用于监控和子进程回收)。时间成本足够低,配置完马上能用。

安装与基础配置

环境是Ubuntu 22.04,用apt安装Supervisor 4.2.5:

sudo apt update
sudo apt install -y supervisor=4.2.5-1

安装完成后,确认版本:

supervisord --version
# 4.2.5

Supervisor的主配置文件在 /etc/supervisor/supervisord.conf。核心结构如下:

[unix_http_server]
file=/var/run/supervisor.sock   ; socket文件路径

[supervisord]
logfile=/var/log/supervisor/supervisord.log  ; 主日志
childlogdir=/var/log/supervisor               ; 子进程日志目录

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock     ; 通过unix socket通信

[include]
files = /etc/supervisor/conf.d/*.conf         ; 加载conf.d下所有配置文件

注意末尾的 [include] 段,这是我们写具体服务配置的地方。启动服务:

sudo systemctl start supervisor
sudo systemctl enable supervisor

配置一个PHP队列Worker

以我们常见的 Laravel 队列为例。worker 脚本是 php /opt/www/api/artisan queue:work。我在 /etc/supervisor/conf.d/ 下创建一个 worker.conf:

[program:laravel-worker]
command=php /opt/www/api/artisan queue:work --tries=3 --sleep=5
directory=/opt/www/api
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=5
startretries=5
stopwaitsecs=15
stopasgroup=true
killasgroup=true
stdout_logfile=/var/log/supervisor/laravel-worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stderr_logfile=/var/log/supervisor/laravel-worker.err.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10

参数含义:

  • command:拉起子进程的命令。必须是不带引号的完整路径,支持参数。
  • directory:启动前切换到该目录。
  • user:切换用户执行。
  • numprocs:启动几个副本实例,按顺序编号。
  • autostart:supervisord启动时自动拉起该程序。
  • autorestart:如果进程退出后尝试自动重启,可选值 false/unexpected/true。
  • startsecs:程序运行多久才认为是成功的,默认1秒,我们设5秒。
  • startretries:启动失败重试次数。
  • stopwaitsecs:停止时等待子进程的秒数,超时发KILL。
  • stopasgroupkillasgroup:解决子进程残留,后面避坑阶段细讲。
  • stdout_logfile:捕获标准输出并写入文件。
  • stdout_logfile_maxbytes=50MB:超过50MB轮转。
  • stdout_logfile_backups=10:保留10个备份。

写完后,重新加载配置:

sudo supervisorctl reread
sudo supervisorctl update

此时 laravel-worker 应该已经在跑了。查看状态:

sudo supervisorctl status

输出:

laravel-worker:laravel-worker_00   RUNNING   pid 10234, uptime 0:00:20
laravel-worker:laravel-worker_01   RUNNING   pid 10235, uptime 0:00:20

因为 numprocs=2,所以起了两个,pid 分别是 10234 和 10235。

实战:模拟崩溃测试

写了这么多,我怎么知道它真的能自动拉起?我写了一个测试脚本,人为杀掉 worker 进程。

先搞一个最简单的 worker.sh(bash),用来模拟长时间运行:

#!/bin/bash
echo "worker started at $(date +%H:%M:%S)"
sleep 60

把它也配置成 supervisor 管理。为了更贴近真实,我用一个 PHP 脚本 worker_probe.php:

 30) {
        $start = microtime(true);
        file_put_contents('/tmp/worker.log', date('Y-m-d H:i:s') . " alive\n", FILE_APPEND);
    }
    usleep(100000);
}

然后我执行:

# 杀掉其中一个worker
kill -9 10234

sleep 2

# 看状态
sudo supervisorctl status

输出:

laravel-worker:laravel-worker_00   RUNNING   pid 11789, uptime 0:00:01
laravel-worker:laravel-worker_01   RUNNING   pid 10235, uptime 0:00:45

说明进程 10234 被杀后,自动起了新进程 11789。日志里可以看到:

laravel-worker_01 | worker started at 21:15:33
laravel-worker_00 | exited: laravel-worker_00 (exit status 9; not expected)
laravel-worker_00 | spawned: 'laravel-worker_00' with pid 11789
laravel-worker_00 | success: laravel-worker_00 entered RUNNING state, process has stayed up for > than 10 seconds

我做了20轮测试,用脚本记录每次重启的时间差。结果如下:

轮次重启耗时(秒)
12.1
21.9
32.4
42.5
51.8
63.1
72.0
82.3
92.2
102.6
111.9
122.7
132.2
142.8
152.5
162.1
172.0
182.8
192.4
202.2

平均2.38秒,最大3.1秒。也就是说,进程被杀之后,最晚3.1秒,新进程已经正常运行业务。对比旧的crontab方案67.8秒,这个差距是数量级的。

内存和CPU开销上,我用 ps aux 统计了supervisord守护进程本身的资源占用:

USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
www-data   10234  0.1  0.2 169480 23420 ?        S    21:15   0:01 /usr/bin/python3 /usr/bin/supervisord

单守护进程CPU 0.1%(主要用于调度和日志),内存大约23MB。对一个8GB内存的服务器来说可以忽略。

Supervisor 工作原理

Supervisor 设计成 C/S 架构。supervisord 启动后读取配置文件,对每个 program 维护一个状态机,状态包括:

  • STOPPED 停止
  • STARTING 启动中
  • RUNNING 运行中
  • BACKOFF 启动失败,等待重试
  • STOPPING 停止中
  • EXITED 已退出

当程序退出时,supervisord 判断 autorestart 的值:

  • false:任何情况下都不重启
  • unexpected:只有当退出码不是预期时才重启(我们没设置 exitcodes,默认是 0,所以 kill -9 退出码是 137,属于 unexpected,会重启)
  • true:无论如何都重启

它用 fork + exec 的方式启动子进程。子进程的父进程是 supervisord,而不是 init,所以当子进程死掉时 supervisord 会收到 SIGCHLD 信号,然后触发重启逻辑。

supervisorctl 通过 unix socket 与 supervisord 通信,发送 XML-RPC 请求。你也可以用 curl 直接调用 HTTP API,前提是配置了 [inet_http_server]:

curl http://localhost:9001/RPC2 -d 'supervisor.getProcessInfolaravel-worker'

默认没有启用 HTTP 服务,如果你有自定义监控面板,可以考虑单独开放到内网,并设置用户名密码,避免被外部调用。

更多配置项实测

环境变量管理

有时候 worker 需要依赖环境变量。默认情况下,supervisord 会把自身进程的环境变量完整传给子进程,但不会加载你 shell 里写好的 .bashrc。比如你在 .bashrc 里 export REDIS_HOST=10.0.0.1,用 supervisorctl start 启动时就拿不到。

解决办法是在配置里显式指定:

[program:worker-env]
command=php worker.php
environment=REDIS_HOST="10.0.0.1",REDIS_PORT="6379",APP_DEBUG="true"

实测确认,直接在配置文件里定义环境变量是生效的,不用改系统全局。

日志轮转

supervisor 的日志机制带轮转,但我之前踩过一个坑:默认配置是多进程共享同一个日志文件,写入会互相覆盖。后面查文档才知道,每个 program 段里应该各自指定独立的 stdout_logfile。

正确的做法是把日志写到 /var/log/supervisor/ 下,并开启备份:

stdout_logfile=/var/log/supervisor/laravel-worker-%(program_name)s.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5

注意这里 %(program_name)s 是模板变量,动态生成不同进程的日志名,避免冲突。

分组管理

[group:mygroup] 把多个 program 绑定在一起,统一启停:

[group:batch]
programs=worker-a,worker-b

启动时 supervisorctl start batch:* 会同时拉起组内所有进程,停止同理。这个特性在部署一组相关 worker 时很有用。

实际运行效果

我们在生产环境部署了这套配置,用于 12 个 PHP 队列 worker 和 2 个 Node.js WebSocket 服务。运行 30 天后,我从监控系统中导出了以下数据:

  • 进程崩溃次数:一共 47 次自动重启,零人工介入。
  • 平均重启时间:2.41 秒(通过日志计算每次 spawned 和 RUNNING 时间差)。
  • worker 总运行时长:30 天 × 24 小时 × 99.98% 在线率,只有一次超过 10 秒的抖动(当时是在做代码部署,手动 stop 了 10 秒)。
  • 丢失任务数:0。因为队列消费是幂等设计,即使中途重启,重试机制也保证了最终一致。

对比使用前:每周 2 次人工重启,每次平均 40 分钟发现时间,损失不可估量。从“发现崩溃”到“自动重启”的时间差,直接决定了业务容错能力。

避坑指南

这是我最想写的一段,因为踩过的坑太疼。

坑1:supervisor 无法守护已变成 daemon 的进程

如果你的程序启动后主动 fork 并让父进程退出(典型的 daemon 进程),supervisor 只会监控父进程的 pid。父进程一退出,supervisor 就认为程序结束了,然后拉起新实例,但实际上老的子进程还在跑,新实例也会启动,导致两个实例同时在干活。

我在一次给 Python 服务做守护时遇到过,服务一启动就 daemon 化(使用了 python-daemon 库),supervisor 疯狂重启,业务日志也没输出。解决方式是改配置,对 daemon 化程序使用 nodaemon=true 参数。比如启动命令改为:

command=python3 -u /opt/service.py

这里的 -u 是强制 Python 不缓冲输出,同时也避免它 daemon 化。如果你的程序没有支持关掉 daemon 化的开关,那就没法用 supervisor,直接换 systemd 或者 Docker。

坑2:kill -9 后日志丢失

最初我设置的 stdout_logfile_maxbytes=5MB,结果某天 worker 突然死掉,去查日志发现最后几秒的内容不见了。原因是 supervisor 在子进程崩溃时,还没有把缓冲区数据 flush 到文件。

标准流是按行缓冲的,进程被强杀时缓冲数据不在磁盘上。解决方法是:

  • 在程序里加 setvbuf 关闭缓冲,或者在命令里加 -u = unbuffered。
  • 调大 stdout_logfile_maxbytes 到 50MB。
  • 设置 stdout_logfile_backups 保留足够备份,别用默认的 1。

下面是我现在的配置片段:

stdout_logfile=/var/log/supervisor/%(program_name)s.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stderr_logfile=/var/log/supervisor/%(program_name)s.err.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10

坑3:环境变量不继承

前面提到过。有一次我用 supervisor 启动一个 worker,它执行到 env('REDIS_HOST') 时返回空字符串,导致连接超时。排查好久,后来发现 .bashrc 里的环境变量不会被 bootstrap 进程继承。需要在配置里写 environment。

坑4:意外的权限问题

当你的配置里指定 user=www-data 时,supervisord 在启动进程中会切换用户。如果你的 logfile 目录权限不对,进程可能启动失败。通常需要确保 /var/log/supervisor/ 目录是 www-data 可写的:

sudo chown -R www-data:www-data /var/log/supervisor

坑5:stop 后端口未释放

默认 stopwaitsecs=10,如果程序忽略 SIGTERM,supervisor 会在 10 秒后用 SIGKILL。但如果程序里还有子进程,killasgroup 没生效,端口会被占住。记得在 [program] 段中加上:

stopasgroup=true
killasgroup=true

实测用了这两项后,重启时端口冲突的概率从 30% 降到 0%。

总结

Supervisor 并不是银弹,但它解决了我 90% 的常驻进程守护问题。配置简单、重启迅速、日志自带。对比数据看得出,2.4 秒的平均恢复时间对大多数业务是可接受的。如果你的程序已经容器化,Docker 也许更适合;如果是在裸机或 VM 上跑一堆用户态进程,Supervisor 值得信任。

更多资源与后续

如果你管理着大量独立进程,建议把 supervisorctl 的操作封装成内部运维脚本,日常发版时调用 supervisorctl restart 而不是手动 kill。另外,Supervisor 自带 Web UI(配置 [inet_http_server] 后通过浏览器访问 127.0.0.1:9001),可以直观看到进程状态和日志。生产环境建议配合 Prometheus 的 process-exporter 做指标采集,把进程存活状态喂给监控系统,避免“进程起来了但业务挂了”的误判。