worker 残留进程树清理

SKILL运维和基础设施
运维和基础设施 未标注权限 许可证不明确
安装这个技能

排查与安全清理 Linux 宿主上批处理 worker 的残留进程树(父进程卡在管道读、子进程被 init 收养、任务早已失败但进程活数天、内存与句柄被占)。适用于 Python 包装器 + ffmpeg / 浏览器 / ML 推理子进程这一类 worker 体系。当用户报告「任务失败了进程还挂着」「kill 了父进程子进程还在」「worker 越跑越多」「内存被吃光」或要求清理孤儿进程时使用。

一句话选型结论

按资料字段整理

worker 残留进程树清理是运维和基础设施类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类运维和基础设施 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_617dd917/worker-orphan-process-cleanup-moran
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

运维和基础设施
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_617dd917仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验2026-11-06上游最后核验时间 · 数据更新 2026-11-06
正文体量20.5 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
查服务与日志

按现象定位服务状态与日志线索。

写部署与巡检脚本

把重复的部署、巡检写成脚本。

故障排查

按步骤排查网络、依赖与配置。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在运维和基础设施场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
worker 残留进程树清理当前 未标注权限 未标注 未标注 未标注 有
Azure 部署 调用外部服务 GitHub 仓库 664.2 万 Codex、Claude、Cursor 有
Azure diagnostics 问题管理 调用外部服务 GitHub 仓库 657.6 万 Codex、Claude、Cursor 有
Azure aigateway 部署 调用外部服务 GitHub 仓库 655.3 万 Codex、Claude、Cursor 有
Azure AI 搜索 调用外部服务 GitHub 仓库 651.4 万 Codex、Claude、Cursor 有
Azure Cloud migrate 部署 调用外部服务 GitHub 仓库 650.7 万 Codex、Claude、Cursor 有

SKILL.md 全文

20.5 KB
模型实际读到的说明书原文,来自技能仓库

worker 残留进程树清理

批处理 worker 最常见的「幽灵故障」:任务在调度器里早就失败了,宿主上的进程却还活着——父进程卡在管道读、子进程被 init 收养,内存、句柄、端口被一点点吃光,机器越跑越慢,重启一次好两天,然后又犯。

这类残留的清理不是 kill -9 一把梭:要能证明它确实是残留(而不是正在跑的任务),要按进程组整组清(否则子进程会被收养继续活),还要分清「清理」和「修复」——如果清理完又冒出来,那是代码里派发/超时/回收的问题。

> 适用对象:Python 包装器 + 子进程(ffmpeg 转码、Playwright/Puppeteer 浏览器、ML 推理、命令行工具)这一整类 worker 体系;命令以 Linux 宿主为准。

目录

  • 何时使用
  • 参数与占位符
  • 症状索引
  • 现场采集脚本
  • 心智模型:进程组、孤儿与 init 收养
  • 第一步:先发现
  • 判定:残留三条件
  • 清理:整组、有序
  • 验证与复位
  • 平台差异
  • 错误处理约定
  • 失败模式与恢复
  • 完整走查(端到端样例)
  • 不适用场景
  • 反模式与常见问题

何时使用

自动触发:用户描述与下表场景相关时直接启用,不需要点名本技能。

用户会这么说 实际场景
「任务早就失败了,进程还挂在那儿,内存被吃光了」 残留进程树 → 第一步:先发现
「转码 worker 越跑越多,机器越来越卡」 重复拉起 / 孤儿堆积 → 心智模型
「kill 了父进程,子进程还在跑」 只杀父进程的经典坑 → 清理:整组、有序
「ps 里一堆 python xxx_worker,不知道哪些能动」 判定三条件
「清理完过两天又冒出来」 清理不是修复 → 反模式

手动调用:对话里说「用 worker 残留进程树清理处理」,或直接用 @ 引用本技能(平台的完整 slug 可能带作者后缀,不必写全名)。

不适合:Kubernetes 等编排层管理的 Pod、systemd 托管的服务、Windows 上的进程——见「不适用场景」。

参数与占位符

动手前先替换下表占位符;未替换就跑等于用错环境。

占位符 含义 从哪里取
<pid> 进程 id ps -eo pid,pgid,etime,rss,args 第一列
<pgid> 进程组 id 同上第二列(同一作业共享)
<jobId> 作业 id 调度器作业列表,或命令行里的 --job 参数
<作业根目录> 作业数据根 部署清单里的作业挂载点

症状索引

症状 看哪节 判据
内存持续下降,free -m 里 available 越来越少 第一步:先发现 按 RSS 排序找最占内存的 worker 树
同一作业在宿主机上有两套进程树 判定:残留三条件 PGID 不同但命令行里的作业 id 相同
进程 CPU 0%,etime 却是几天 判定:残留三条件 卡在管道读、等一个永远不会来的就绪信号
kill <pid> 之后进程还在 清理:整组、有序 子进程被 init 收养,需要按进程组杀
清理完任务还是「运行中」 验证与复位 调度器读的是启动回执文件,不是进程

现场采集脚本

python scripts/orphan_scan.py --json 一条命令按 ps -eo pid,ppid,pgid,pcpu,stat,etime,rss,args 采全进程,按进程组聚合,用 --pattern 找 worker:孤儿(PPID=1,通常被 init/subreaper 收养,容器内 PID 1 可能是应用本身)、僵尸(STAT 含 Z)、同作业多套进程树、长空闲(≥1 天且瞬时 CPU≈0)。CPU 默认读 /proc/<pid>/stat 的 utime+stime 间隔采样(--cpu-sample-ms,默认 1000ms;采样不可用时回退 ps 的累计值并注明),--max-processes(默认 500)限制分析数量并在 raw.truncated 记录。脚本内没有任何 kill,只打印 PID/PGID 与建议命令。

findings 的 code 对应本节
E-ENV-06 「先发现」「判定:残留三条件」(孤儿 / 重复进程树 / 长空闲)
E-ENV-07 僵尸进程:KILL 无效,需父进程回收
E-ENV-01 / E-ENV-02 清理阶段的进程组范围(脚本给 pgid,命令由人执行)

--json 的结构(真实输出节选,raw 已省略):

{
  "skill": "worker-orphan-process-cleanup",
  "collected_at": "2026-11-04T09:12:33+08:00",
  "target": "local:build-host",
  "findings": [
    {"code": "E-ENV-06", "severity": "high",
     "evidence": "pid=24813 pgid=24800 etime=2-05:12:33 瞬时CPU=0.0% args=transcode_worker.py --job 8842",
     "action": "确认作业 8842 已终结后整组清:kill -TERM -- -24800"}
  ],
  "raw": {"matched": 3, "truncated": false}
}

心智模型:进程组、孤儿与 init 收养

做法:把每个作业看成一棵以进程组为边界的树,而不是一堆零散的 PID。

为什么:worker 通常是三层——调度侧拉的 bash -c → Python 包装器 → 真正的干活的子进程(ffmpeg、浏览器、命令行工具)。包装器被杀后,子进程会被 PID 1(init)收养,命令行里不再有父进程信息,ps 看到的只是「一个孤零零的 ffmpeg」。按 PID 逐个杀既找不到全,也会漏掉后来 fork 的孙子进程。进程组(PGID)才是这棵树的锚点:同一作业启动的所有进程共享一个 PGID,kill -- -<pgid> 一次带走整组。

验证:清理前用 ps -o pid,pgid,args -p <pid1>,<pid2> 看这几个 PID 的 PGID 是否相同;相同才说明它们属于同一个作业的同一棵树。

三种典型残留形态:

  1. 卡在管道读:包装器阻塞在 pipe_read 等子进程的就绪标记,CPU 恒等于 0%,子进程随之长存(可挂数天)。
  2. 父进程已死、子进程被收养:调度器超时杀了 bash -c,Python 和它的子进程变成孤儿,继续跑。
  3. 重复拉起:派发重试没做「启动自认领」,同一作业被拉起两套树,资源翻倍。

第一步:先发现

做法:先只读地看清有哪些树、各自活了多久、占多少内存。

# 按 worker 特征过滤;重点看 PID / PGID / 存活时长(etime) / 内存(RSS)
ps -eo pid,pgid,etime,rss,args | grep -E "<你的 worker 特征>" | grep -v grep

# 按内存倒序,看谁在吃内存
ps -eo pid,pgid,etime,rss,args --sort=-rss | head -n 15

为什么:etime 是最强的信号——一个「正常该几分钟跑完」的任务活了两天,基本可以断定是残留;反过来只看 CPU 会漏掉「卡在 IO 上但仍在跑」的慢任务。

验证:把你怀疑的 PID 的 PGID 记下来,ps -eo pid,pgid,args | grep <pgid> 应该能看到同一组里的全部成员。

判读要点:

  • etime 格式:05:12 是 5 分 12 秒,2-05:12:33 是 2 天 5 小时,位数越多活得越久。
  • RSS 单位是 KB:3245678 ≈ 3.1 GB。
  • STAT 里的 Z 是僵尸(已死未回收),另一种问题,见 FAQ。

判定:残留三条件

做法:动手之前,三条同时成立才算残留:

  1. 作业状态已终结——调度器/数据库里该作业是「成功 / 失败 / 取消」。「没有记录」不等于已终结:记录可能已被清理、或调度器换了存储;这种情况要交叉验证三处——进程启动时间与作业提交时间是否吻合、作业目录里的日志最后一行停在哪个阶段、命令行里的 job id 能否在调度器里查到(含历史表);
  2. CPU ≈ 0%(瞬时)——读 /proc/<pid>/stat 的 utime+stime(字段 14/15),间隔 1 秒再读一次求差,delta_ticks / (秒数 × SC_CLK_TCK) × 100;不要用 ps -o %cpu——它是进程生命周期的平均值,一个跑了 3 天、刚忙起来的进程平均下来也可能只有 2%,会被误判成空闲(脚本的做法就是采样,采样不可用才回退并注明);
  3. etime 以天计,远超该任务的正常耗时。

为什么:只凭「看着像死的」就杀,会误杀正在跑的长任务——尤其是那些安静等外部回调的作业(CPU 本来就接近 0)。第 1 条把「任务已结束」和「进程还在」这两件事对上,才能确认是残留。

验证:三条都记录在清理记录里(作业 id、状态、CPU、etime、RSS),事后有人问「为什么杀这个进程」能直接给证据。

清理:整组、有序

做法(顺序执行,别跳步):

# 0) 预检:确认树成员与进程组归属,确认没有别的进程共用这些进程组
ps -o pid,pgid,args -p <pid1>,<pid2>,<pid3>
pgrep -g <pgid1>; pgrep -g <pgid2>

# 1) 先 TERM 包装器,给它机会走收尾逻辑(写 result、释放资源),等 5 秒
kill -TERM <包装器 pid>; sleep 5

# 2) 按进程组整组 KILL(此时组内只剩 worker 与它的子进程)
kill -KILL -- -<pgid1> -<pgid2> 2>/dev/null

# 3) 验证:全部退出(预期每个 PID 都输出 "No such process")
for p in <pid1> <pid2>; do kill -0 "$p" 2>&1; done

为什么:

  • 先 TERM 再 KILL:TERM 让包装器有机会把结果落盘、把资源还回去;直接 KILL 会让「结果文件」停在半截。
  • kill -- -<pgid> 而不是 kill <pid>:负号表示「整个进程组」。少了这个负号,子进程会被 init 收养继续活——这正是「kill 了但进程还在」的原因。
  • 预检 pgrep -g:确认组里没有别的作业混进来。共享进程组(比如同一 shell 里手动起的任务)会被整组误杀。
  • 例外:如果确认某个旧版 worker 没有收尾逻辑(TERM 之下会二次卡死),可以跳过第 1 步直接整组 KILL——但要在清理记录里写明原因。

验证:第 3 步每个 PID 都应返回 No such process;还有活着的,说明你漏了某个 PGID 或漏了被收养的子进程。

验证与复位

做法:清理完做四件事——看内存、看资源池、清启动回执、跑一个正常任务。

# 内存是否回落(清理前后各跑一次对比)
free -m

# 启动回执:调度器靠它判断「这个作业还在跑」,不清会导致误判
ls -l <作业根目录>/<jobId>/started.pid && rm -f <作业根目录>/<jobId>/started.pid

# 资源池是否能再次分配:提交一个最小任务,确认能正常完成

为什么:进程死掉不等于系统复位——调度器可能还认为作业在跑(读的是启动回执),资源池的配额可能还挂着。只清进程不复位,下一次派发会被自己的残留挡住。

验证:free -m 的 available 明显回升;提交的最小任务在正常时长内完成;作业列表里该作业不再显示「运行中」。

平台差异

做法:这套命令按 Linux 宿主写,换平台要重新核对语义。

为什么:

  • Linux:pgrep -g <pgid> 按进程组过滤,kill -- -<pgid> 整组发送——本技能默认前提。
  • macOS:pgrep -g 的 -g 是「按真实 GID(用户组)」过滤,语义完全不同,命令不能照搬;整组 kill 的写法也需要适配。
  • 容器:容器内 ps 只看得到本 PID namespace 的进程;要清宿主上的残留,得在宿主机上执行,或 nsenter 进去。
  • Windows:没有 POSIX 进程组语义(对应的是 Job Object),本技能的命令全部不适用。

验证:换平台后先跑一次只读的 ps/pgrep 组合,确认过滤结果符合预期,再谈清理。

错误处理约定

本技能把出错分成四类(输入错误 / 环境错误 / 证据不足 / 结论冲突),处置与重试策略不同,不要混着来;四类判据表与统一输出格式见 references/error-handling.md 第一、二节。

本技能常见错误码:

代码 现象 处置
E-ENV-01 kill <pid> 后进程还在 改用 kill -KILL -- -<pgid> 整组清
E-ENV-02 整组 kill 后还有零星进程 对漏网进程单独按 PID 清,并在记录里注明
E-ENV-03 清理完调度器仍显示「运行中」 删掉回执,让调度器按终态回收
E-ENV-04 内存没回落 补清漏网进程;确认不是别的作业
E-CNF-05 误杀了别的任务的进程 立即停止批量清理,从调度器重新派发受影响作业
E-ENV-06 发现孤儿 / 重复进程树 / 长空闲残留 先确认作业终态,再整组清(kill -TERM -- -<pgid>),别只杀父进程
E-ENV-07 僵尸进程(STAT 含 Z) kill 无效:需父进程 wait() 回收,父进程已死则等 init 收养或重启容器

输入校验清单、重试与退出语义见 references/error-handling.md。

失败模式与恢复

失败现象 常见原因 检查方法 恢复动作 可否重试
kill <pid> 后进程还在 只杀了父进程,子进程被 init 收养 ps -eo pid,ppid,pgid,args 看父进程是不是 1 改用 kill -KILL -- -<pgid> 整组清 可重试(换整组)
整组 kill 后还有零星进程 有子进程换了进程组(setsid) pgrep -f "<worker 特征>" 全量再扫一遍 对漏网进程单独按 PID 清,并在记录里注明 可重试
清理完调度器仍显示「运行中」 启动回执文件没删 ls -l <作业根目录>/<jobId>/started.pid 删掉回执,让调度器按终态回收 可重试
内存没回落 清理不彻底,或有别的作业在吃内存 free -m + ps --sort=-rss 前 10 名 补清漏网进程;确认不是别的作业 可重试(先定位)
误杀了别的任务的进程 共享了进程组,或 pgrep -f 匹配过宽 回看预检输出 pgrep -g <pgid> 是否有陌生进程 立即停止批量清理,从调度器重新派发受影响作业 不可重试(已中断)
kill -0 报 Operation not permitted 进程属主不是当前用户 ps -o user= -p <pid> 换对应属主身份(或 root)执行 可重试(换用户)

失败时优先中止:清理是破坏性操作。一旦预检发现「组里有不认识的进程」或「作业状态其实是运行中」,立刻停下,把现场(ps 快照、作业状态)留全再决定——误杀正在跑的任务比残留更难收拾。

完整走查(端到端样例)

场景:批处理平台上一批视频转码任务早已失败,但 Python 包装器与 ffmpeg 子进程活了两天,吃掉 3 GB 内存。

输入:用户原话「转码任务昨天就失败了,进程还挂着,内存快满了,帮我清一下」。

ps -eo pid,pgid,etime,rss,args | grep -E "transcode_worker|ffmpeg" | grep -v grep
ps -p 24813,24820 -o pid,pgid,%cpu,etime,rss,args

中间输出(示例):

24813  24800  2-05:12:33  3245678  python3 /app/workers/transcode_worker.py --job 8842
24820  24800  2-05:12:31    98211  ffmpeg -i /data/in/8842.mp4 -c:v libx264 /data/out/8842.mp4

  PID   PGID  %CPU     ELAPSED    RSS
24813  24800   0.0  2-05:12:33  3245678

(作业表里 job 8842 的状态是 failed,结束于两天前。)

定位与修复:作业已终结 + CPU 0.0% + etime 两天,三条齐全 = 残留;两个 PID 同属 PGID 24800,说明是同一棵树 →

  1. 预检整组:pgrep -g 24800 确认只有这两个进程;
  2. kill -TERM 24813; sleep 5——给它机会写 result、释放临时文件;
  3. kill -KILL -- -24800,整组带走(ffmpeg 是子进程,必须靠负号才能一并清掉)。

修复后复测:

$ for p in 24813 24820; do kill -0 "$p" 2>&1; done
kill: (24813): No such process
kill: (24820): No such process

$ free -m
               total        used        free      available
Mem:           15884        4210        9871        11342

$ rm -f /data/jobs/8842/started.pid && echo "回执已清"
回执已清

结论:这一轮验证了「判定三条件」和「整组清理」——如果只 kill 24813,24820(ffmpeg)会被 init 收养继续跑,内存不会回落,看起来就像「清理没生效」。

不适用场景

  • Kubernetes / 编排层管理的 Pod:进程由 kubelet 管理,残留应通过删除 Pod(必要时 --force)解决,直接 kill 宿主进程只会被重建。
  • systemd 托管的服务:用 systemctl stop <unit>,它会带走 cgroup 里的全部进程;手 kill 反而可能触发自动重启。
  • Windows 宿主:没有 POSIX 进程组语义,需要 Job Object 或 taskkill /T。
  • 需要保留现场取证:先 gcore / 抓 ps 快照 / 保留日志,再考虑清理。
  • 没有作业状态可查的环境:判定三条件缺第一条,无法区分「残留」与「正在跑的长任务」,不要贸然动手。

反模式与常见问题

反模式(每条都踩过)

  • 只杀父进程:子进程被 init 收养后继续跑,看起来像「杀不死」。必须整组。
  • 不查作业状态就动手:正在跑的长任务 CPU 也接近 0,误杀等于人为制造故障。
  • kill -9 一把梭:跳过 TERM 会让结果文件停在半截、临时文件不清理,下次派发直接读到脏数据。
  • 删作业目录:result.json、worker.log 是失败原因的唯一证据,清进程不要清证据。
  • 用 pgrep -f 做唯一判据:命令行匹配太宽会误杀同名脚本;它只适合做「补漏」,前置判断要靠 PGID + 作业状态。
  • 清完不验证:内存没回落、回执没删,等于没清干净——过两天还会「复发」。

常见问题(FAQ)

Q:会不会误杀别人的进程?
会,前提是你跳过了预检。先 pgrep -g <pgid> 看清组内成员,再动手;发现陌生进程就停下。

Q:僵尸进程(STAT 为 Z)要不要清?
僵尸已经死了,杀不掉也不需要杀——它占的只是一个表项(STAT 为 Z),kill -9 对它无效。回收必须由父进程完成:父进程还在就修它(wait())或重启它;父进程已经死了,通常由 PID 1(init / subreaper)收养后回收——但容器内 PID 1 可能就是应用本身,要先看 /proc/1/cgroup 或容器 ID 再下结论。注意:本技能的整组清理会带走父进程,但僵尸不会立即消失——要等 init 或新的父进程回收,所以「清完 ps 里还有 Z」是预期现象,先确认它没有继续跑。

Q:子进程已经被 init 收养,还能按组找回来吗?
能。收养改变的是 PPID,不改 PGID;ps -eo pid,ppid,pgid,args 里 PGID 仍然指向原作业的进程组。

Q:能不能写个定时任务自动清理?
可以,但只能自动「发现 + 告警」,清理动作建议留人工确认——判定三条件里第一条依赖作业状态,脚本判断错的代价是误杀生产任务。

Q:清理完又冒出来,怎么办?
这是「清理不是修复」:说明派发重试在重复拉起,或超时回收没覆盖该场景。回代码排查(启动自认领、硬超时强杀后代、调度器终态回收),不要加到清理清单里了事。

延伸阅读

references/error-handling.md

常见问题

5 问
答案由本站依据条目字段整理
worker 残留进程树清理怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_617dd917/worker-orphan-process-cleanup-moran。安装方式为未标注。装完刷新技能列表或重开客户端。

worker 残留进程树清理支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

worker 残留进程树清理权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

worker 残留进程树清理收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

worker 残留进程树清理用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 2026-11-06。