zsh — anti@lateresonance:~ (微重力交互终端 v1.0)
按 `~` 或 `Esc` 展开/收起
🛸 欢迎接入 Late Resonance(迟到的共鸣 · 微重力前哨站) 交互式终端。输入 help 查看可用指令(支持 `ls`, `cat`, `status`, `speedtest`, `anti`, `jelly`)。
anti@lateresonance:~ %

[2026 Remastered · 09] 为什么 supervisorctl stop 杀不死子进程?top 线程参数勘误与进程组防逃逸实战(Debian 13 真机版)

📅 发布于: 2026-11-04 05:32:51 · 🔄 更新于: 2026-11-04 05:32:51 · 📖 5220 字 (约 18 分钟阅读)

🛸 站长重制导读(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 拥有三大降维优势:

  1. 基于内核 Cgroup v2 的天然进程树围栏:systemd 默认 KillMode=control-group,无论你的脚本派生了多少层子孙进程,哪怕故意双 fork() 脱离会话,也永远逃不出内核 Cgroup v2 控制组,systemctl stop 100% 斩草除根;
  2. 零额外运行时开销:无需额外常驻一个吃几十 MB 内存的 Python supervisord 守护进程;
  3. 自带硬件级资源隔离:只需在 .service 中写一行 MemoryMax=512M 与 CPUQuota=80%,即可由内核直接限制单服务资源上限。
🛸 安缇 (Anti Gravi) · 云端伴读舱 微重力巡航中

*(小水母 Gravi-Jelly 在半空轻轻闪烁着荧光青触角,帮你盯死每一行配置与 1C512M 内存水位 🪼✨)*

前哨站内存: -- MB | 系统已用: -- / -- MB
守望灯塔: Zero-G Anchor · 在线值守
微重力前哨站 · 内存: --MB · 负载: --