首页 游戏 软件 排行 专题

进程崩了没人拉起:退出码写错,Supervisor 一直空转

来源: 本站整理 日期:2026/11/4 0:47:43

上周处理一台跑着几个后台任务的云服务器,客户说"任务从昨天下午开始就没跑过,页面上的数据一直停在同一条"。我第一反应是磁盘满了或者内存爆了,上去一看都很干净磁盘用了 40%,内存 3G 的机器才吃掉 800M。真正有意思的是 Supervisor 的状态:进程显示 RUNNING,日志里却全是 spawned: 'worker' with pid 21345 exited 这种记录,一行接一行,pid 每次都变。也就是说进程一直在生一直在死,但 Supervisor 的状态栏永远显示运行中。

如果你手上也有跑后台任务、需要进程崩溃自动拉起的机器,选机器时就得考虑稳定性和长期在线能力虫网 BUG NET(简单稳定的 VPS 主机服务),或者EUGameHost(自有硬件与数据中心,异常由自己处理),都可以在配置守护策略前先把底子打好。进程守护逻辑再漂亮,机器本身扛不住也是白搭。

一、退出码不是"成功或失败",而是三类语义

Supervisor 的 autorestart 取值只有三个:unexpected、false、true。关键在于 unexpected 的判定依据不是"进程死了",而是"进程死的方式不在预期内"。Linux 里进程退出时会把一个值交给父进程,这个值分三类:0 表示正常退出,大于 0 表示程序自己报的错(比如 Python 的 sys.exit(1)),大于 128 的部分则是被信号杀死时由 shell 转换出来的(137 是 SIGKILL,139 是 SIGSEGV,143 是 SIGTERM)。

默认的 autorestart=unexpected 会重新拉起的,只有"退出码不在 exitcodes 列表里"这一种情况,而 exitcodes 的默认值是 0,2。也就是说:程序退出码是 0,Supervisor 认为这是预期内退出,不重启;退出码是 2,同样不重启;其他非零值才重启。听起来合理,但坑就埋在这里很多脚本用 shell 包装,set -e 没开,或者中间管道某一步失败,最后 echo $? 被吃掉,脚本以 0 退出。

更要命的是另一种情况:进程被 OOM Killer 干掉时,内核发的是 SIGKILL,Supervisor 那边拿到的退出码是 137,属于"意外退出",会触发重启。但如果你在 [program:xxx] 里写的是 command=/bin/bash /opt/xxx/run.sh,而脚本最后一行是 tail -f /var/log/xxx.log,那 bash 这个壳进程永远不退出,真正的业务子进程早就死了Supervisor 盯着的是壳,壳活着就是 RUNNING。这叫假活进程,是最难查的一种。

Linux 终端中的进程列表与崩溃日志

二、第二个现场:autorestart 写成了 always,但 startsecs 不对

我这次遇到的就是这个组合。配置里写的是 autorestart=always,看上去无论怎么退出都会重启。但 startsecs 没设,默认值是 1,也就是"进程启动 1 秒内不退出就算启动成功"。有个任务初始化要 3 秒,第 3 秒的时候做了一次健康检查,失败就直接 os._exit(1)。Supervisor 认为它在 1 秒窗口内退出了,标记为 BACKOFF/FATAL,重试几次之后放弃,于是就出现日志里 pid 疯长、状态栏却显示正常的情况。

对应的修法有两处:把 startsecs 设成大于真实初始化时间,比如 startsecs=10;同时把 startretries 调大到 10 以上,别让它三次就放弃。另外一定要加 stopwaitsecs,Supervisor 发 SIGTERM 之后如果进程赖着不走,10 秒后会强杀,否则重启的时候新旧两个进程会同时占着端口,日志里又是一堆 Address already in use。

服务器机房中常驻运行的守护进程

三、第三个现场:日志把自己写满,磁盘满了之后进程写不进去卡死

这个现场更隐蔽。我第一次排查时把 Supervisor 的日志翻了个遍,看到一个规律:每次 spawn 的间隔大概是 22 秒,而任务本身应该 5 分钟跑一轮。22 秒这个数字很扎眼,因为 Supervisor 默认 startretries 的重试间隔是按秒递增的,后来一算,正好卡在某个固定的休眠上。再往下查发现是被守护的任务写日志时 stdout 一直往 Supervisor 的 stdout_logfile 里灌,那个文件没有配轮转策略,涨到几个 G 之后把根分区塞满了。磁盘满的那一瞬间,Supervisor 自己写状态日志也写不进去,于是状态更新半死不活,看着像在跑,其实早就失控了。

所以 stdout_logfile 一定要配 stdout_logfile_maxbytes 和 stdout_logfile_backups,让 Supervisor 自己负责轮转,而不是甩给 logrotate。同时 redirect_stderr=true 打开,错误日志和标准输出分家以后排查会轻松很多。这台机器后来加了磁盘用量监控,超过 85% 就告警,问题才算真正闭环。

服务器上的定时健康检查脚本与检查清单

四、现在我检查守护配置的固定顺序

踩完这几个坑之后,我给自己定了一份检查顺序,基本十分钟能定位问题:第一,看 supervisorctl status,如果状态是 RUNNING 但 pid 一直在变,八成是假活或者重启风暴;第二,看 ps -ef | grep 进程名,确认被守护的到底是不是你以为的那个进程,shell 包装是最常见的障眼法;第三,直接查 supervisorctl tail 的日志,找退出码,0 和非 0 的含义完全不同;第四,确认 autorestart、startsecs、startretries、stopwaitsecs 这四个参数是不是跟你的业务实际耗时对得上;第五,检查日志文件大小和磁盘剩余量。

另外还有两个小坑值得提一下。一是 stopsignal,如果你的进程需要优雅关闭时间超过 10 秒,默认的 SIGTERM 会被直接升级成 SIGKILL,数据库连接来不及关就断了,可以显式配成 stopsignal=TERM 并配合 stopwaitsecs=30。二是环境变量,Supervisor 默认不继承 shell 的 PATH,很多命令写相对路径就会"command not found"然后秒退,脚本里 PATH 又没设全,于是又是一轮看不懂的重启。写绝对路径永远是最省事的做法。

进程重启风暴导致服务器负载飙升

回过头看这次故障的根因,其实特别朴素:一个包装脚本把退出码吞成了 0,配合 autorestart=unexpected 的默认语义,Supervisor 判定"这是预期内退出",于是既不重启也不报警。整套守护机制看上去健壮,实际上一直在安静地做无用功。写配置的时候别假设默认值符合你的直觉,把退出码、启动耗时、重试次数这三件事按业务实际算一遍,比装十个监控脚本都管用。

玩家留言 跟帖评论
查看更多评论
相关文章
猜你喜欢
同类下载