[2026 Remastered · 09] 为什么 supervisorctl stop 杀不死子进程?top 线程参数勘误与进程组防逃逸实战(Debian 13 真机版)
🛸 站长重制导读(Anti Gravi's Remaster Note)
本文是 「来时路 · 2026 Debian 13 真机重制企划(9/12)」 的第 9 篇,对应本站 2020 年 7 月发布的 《使用supervisor管理进程》(
CID: 45)。2020 年的原稿用 Supervisor 守护了一个
top.sh性能采集脚本,但那篇笔记里留下了两个非常典型的运维深坑:一是把top -p与top -Hp的作用彻底写反了(且将大写-H误写成了小写-h);二是用 Supervisor 托管 Shell 包装脚本时漏配了stopasgroup=true与killasgroup=true,导致每次执行supervisorctl stop时只杀死了外层bash父进程,里面的业务子进程全部变成PPID=1的孤儿进程在后台无限叠加!今天我们在 Proxmox VE 的 Debian 13 (
Supervisor 4.2.5+procps-ng 4.0.4) 真机上亲手还原这两个现场,并梳理 2026 年 Supervisor vs 原生systemd的选型边界。
一、 2020 原稿 (CID: 45) 三大硬伤复盘
我们先对照 2020 年原稿里的 top.sh 与 Supervisor 配置:
| 维度 | 2020 原稿 (CID: 45) 写法 |
真实执行后果 | 2026 Remastered (Debian 13) 根治方案 |
|---|---|---|---|
1. top -p 与 -Hp 概念颠倒 + 大小写错误 |
原稿注释写:top -b -n 1 -p $pid 是“查看所有线程”,top -b -n 1 -hp $pid 是“查看进程本身” |
完全写反了! 在 top 中,-p <pid> 才是查看单个进程汇总;而大写 -H -p <pid>(--threads-show)才是展开显示该进程下的所有内核线程(LWP)!更致命的是,原稿写成了小写 -hp——在现代 procps-ng 4.0.4 下,小写 -h 代表 --help,命令直接打印帮助菜单并退出,根本采不到一行数据! |
单进程汇总用 top -b -n 1 -p "$PID";展开全部子线程必须用大写 top -b -n 1 -H -p "$PID" |
| 2. 子进程孤儿化逃逸 | 用 Supervisor 启动 /bin/bash top.sh,配置中未声明 stopasgroup 与 killasgroup |
当你在 top.sh 里调用 sleep、PHP Worker 或 Java 子进程时,执行 supervisorctl stop/restart 默认只向父进程 bash 发送信号;子进程立刻脱离管控被 PID 1 (systemd) 收养为孤儿进程,在后台继续占着端口或跑满 CPU! |
在 Supervisor 配置中强制开启 stopasgroup=true 与 killasgroup=true(或在包装脚本末尾使用 exec 原地替换进程镜像) |
| 3. 缺失日志轮转与健康判定窗口 | 仅配了最简 command 与 autostart=true |
未设 startsecs(进程秒退也被当成启动成功)与 stdout_logfile_maxbytes,长时间运行后标准输出日志撑爆磁盘 |
显式声明 startsecs=3、stopwaitsecs=10、stdout_logfile_maxbytes=20MB 与 stdout_logfile_backups=5 |
二、 PVE Debian 13 真机复现一:top -hp 翻车现场与 -p vs -H -p 真相
我们在 Debian 13 (10.0.0.140) 真机上启动一个包含 1 个主线程 + 3 个工作子线程(共 4 个线程) 的 Python 进程(PID: 12287),分别测试原稿的小写 -hp、单进程 -p 与大写线程展开 -H -p:
=== [1. DEBIAN 13 SUPERVISOR & PROCPS-NG VERSIONS] ===
4.2.5
top from procps-ng 4.0.4
=== [2. REPRODUCE 2020 CID:45 'top -p' vs 'top -Hp' (AND '-h' HELP TRAP)] ===
--- [A] 2020 Legacy lowercase: top -b -n 1 -hp 12287 (Triggers -h help and exits!) ---
Usage:
top [options]
Options:
-b, --batch-mode run in non-interactive batch mode
-H, --threads-show show tasks plus all their threads
-p, --pid =PIDLIST monitor only the tasks in PIDLIST
-h, --help display this help text, then exit
--- [B] Process Summary Mode: top -b -n 1 -p 12287 (1 process row, Threads=4) ---
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12287 codex 20 0 235820 9072 5820 S 0.0 0.9 0:00.00 python3
--- [C] Thread Expansion Mode: top -b -n 1 -H -p 12287 (4 individual thread rows!) ---
Threads: 4 total, 0 running, 4 sleeping, 0 stopped, 0 zombie
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12287 codex 20 0 235820 9072 5820 S 0.0 0.9 0:00.00 python3
12289 codex 20 0 235820 9072 5820 S 0.0 0.9 0:00.00 python3
12290 codex 20 0 235820 9072 5820 S 0.0 0.9 0:00.00 python3
12291 codex 20 0 235820 9072 5820 S 0.0 0.9 0:00.00 python3
真机输出一目了然:
-p 12287:只输出主进程12287一行汇总;-H -p 12287:完整展开显示12287、12289、12290、12291四个独立线程 ID(LWP),是排查 Java/Go/C++ 某个具体线程死循环打满单核 CPU 时的必备利器!
三、 PVE Debian 13 真机复现二:为什么 supervisorctl stop 杀不死子进程?
接下来我们在 Debian 13 真机上复现运维中最常踩的 Supervisor 孤儿进程泄漏事故。
假设我们的包装脚本 /tmp/sup-lab/worker_wrapper.sh 启动了一个子进程 sleep 300。我们先按 2020 原稿不加 stopasgroup 的写法启动 legacy_leak,然后执行 supervisorctl stop legacy_leak:
=== [3. REPRODUCE SUPERVISOR ORPHAN CHILD PROCESS LEAK vs 'stopasgroup=true'] ===
--- [1] Before stop (legacy_leak WITHOUT stopasgroup): ---
root 12404 12247 0 12:36 ? 00:00:00 bash /tmp/sup-lab/worker_wrapper.sh
root 12405 12404 0 12:36 ? 00:00:00 sleep 300
legacy_leak: stopped
--- [2] After supervisorctl stop legacy_leak (Notice sleep 300 is STILL ALIVE with PPID=1!): ---
root 12405 1 0 12:36 ? 00:00:00 sleep 300
[!WARNING]
真机抓拍铁证:
在停止前,12404 (bash)的父进程是12247 (supervisord),而12405 (sleep 300)的父进程是12404 (bash)。
当执行supervisorctl stop legacy_leak后,Supervisor 兴高采烈地告诉你legacy_leak: stopped,但用ps -ef一查——子进程12405根本没死!它的PPID变成了1,彻底脱离了 Supervisor 的掌控! 如果这是一个监听:8080端口的业务进程,当你再执行supervisorctl start时,新进程就会因为端口仍被孤儿旧进程霸占而疯狂报错Address already in use!
现在,我们加上 stopasgroup=true 与 killasgroup=true(通知 Supervisor 向整个 Linux Process Group 发送信号),再次在真机上执行 stop:
--- [3] Before stop (safe_worker WITH stopasgroup=true & killasgroup=true): ---
root 12433 12247 0 12:36 ? 00:00:00 bash /tmp/sup-lab/worker_wrapper.sh
root 12434 12433 0 12:36 ? 00:00:00 sleep 300
safe_worker: stopped
--- [4] After supervisorctl stop safe_worker (Zero orphans remaining!): ---
Orphan count: 0 (All child processes cleanly terminated!)
父进程 12433 与子进程 12434 被整组连根拔除,零孤儿残留!
四、 2026 生产级 Supervisor 标准模板 vs 原生 systemd 选型
1. 2026 生产级 Supervisor 配置模板 (/etc/supervisor/conf.d/app.conf)
当你在单容器内管理多进程、或需要利用 numprocs 快速拉起 8 个 Laravel/Swoole 队列消费者 Worker 时,请务必使用下面这份防逃逸、带日志轮转的标准模板:
[program:queue_worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /opt/app/artisan queue:work --sleep=3 --tries=3
directory=/opt/app
user=www-data
numprocs=4
autostart=true
autorestart=true
startsecs=3
startretries=3
stopwaitsecs=10
; 核心防线:停止或强杀时必须作用于整个进程组,严防子进程变成 PPID=1 孤儿!
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/log/supervisor/queue_worker.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5
2. 为什么在 2026 年的宿主机上,我们更推荐直接使用 systemd?
在 Debian 12/13 宿主机层面,除非你需要 numprocs 批量克隆队列 Worker,否则原生 systemd Service 相比 Python 编写的 supervisord 拥有三大降维优势:
- 基于内核 Cgroup v2 的天然进程树围栏:
systemd默认KillMode=control-group,无论你的脚本派生了多少层子孙进程,哪怕故意双fork()脱离会话,也永远逃不出内核 Cgroup v2 控制组,systemctl stop100% 斩草除根; - 零额外运行时开销:无需额外常驻一个吃几十 MB 内存的 Python
supervisord守护进程; - 自带硬件级资源隔离:只需在
.service中写一行MemoryMax=512M与CPUQuota=80%,即可由内核直接限制单服务资源上限。