在 Kubernetes 日常运维中,我们大多数时候停留在:

  • Pod YAML
  • kubectl describe
  • 资源 requests / limits

但真正决定资源是否生效、进程是否被正确隔离、容器是否能被干净回收的,并不是 YAML,而是 Linux 内核里的 cgroup + systemd 对进程的真实管理状态。

本文从一个正在运行的真实容器出发,切入到它对应的 systemd cgroup scope,把四个最容易被忽视、却极其关键的文件一次性讲明白。


一、问题背景:我看到的这些文件,到底是谁的?

在节点上进入如下目录:

/sys/fs/cgroup/…/
└── cri-containerd-xxxxxxx.scope/

并看到:

# ls
cgroup.clone_children
cgroup.procs
notify_on_release
tasks

这不是“某个抽象 cgroup”,而是 一个正在运行中的 Kubernetes 容器,对应的 systemd scope。换句话说:

  • 它对应 一个具体容器
  • 由 containerd + runc 通过 systemd 创建
  • 是 cgroup 树的叶子节点
  • 生命周期 ≈ 容器生命周期

在此基础上,我们再来看这四个文件,语义会变得极其清晰。


二、cgroup.procs:这个容器里有哪些“进程”

# cat cgroup.procs
1438200

内核语义(在 container scope 中)

cgroup.procs 列出了“属于这个容器的进程 PID”

并且:

  • 每个进程只出现一次
  • 写入这个文件 = 把整个进程(含所有线程)移动进该 cgroup

结合当前输出 1438200。这意味着:

  • 当前这个容器 scope 中
  • 只有一个进程仍然存活
  • 它极有可能是:
    • 容器的主进程(容器内 PID 1)
    • 或主进程 fork 出来的唯一存活子进程

无论如何,可以 100% 确认:这个容器当前只运行了一个进程


三、tasks:这个容器里一共跑了多少“线程”

# cat tasks
1438200

内核语义

在 Linux 内核中:

  • task = 线程
  • 一个进程 = 1 个主 task + N 个子 task

因此:tasks 展示的是这个容器 cgroup 中,所有线程的 TID 列表

结合当前输出 1438200,说明:

  • 当前进程是 单线程
  • 或者此刻只有主线程存活

如果这是一个典型的多线程程序(如 Java / 推理引擎),我们很可能会看到:

1438200
1438201
1438202
...

Debug 技巧

  • cgroup.procs 不变,但 tasks 暴涨 --> 单进程多线程模型
  • tasks 持续增长不回落 --> 线程泄漏 / 线程池失控

四、cgroup.clone_children:fork 会不会“破坏容器隔离”

# cat cgroup.clone_children
0

这是最容易被误解、但也是最重要的一个参数。

在 container scope 里的真实含义为:

这个容器里的任何进程 fork 子进程时,子进程必须仍然留在这个 container scope 中

也就是说:

  • fork 不会导致子进程“跑到别的 cgroup”
  • 所有子进程仍然受同一套 CPU / 内存 / OOM 限制

为什么这里即便是叶子节点,也必须是 0?

当前结构是cri-containerd-43049….scope/(没有子目录)

虽然现在没有子 cgroup,在“正常路径”下,clone_children 不会被触发

但 Kubernetes 仍然必须设置:

cgroup.clone_children = 0

这是防止异常、误操作、未来变化破坏容器隔离的“硬安全带”

一旦出现异常情况(例如):

cri-containerd.scope/
└── sub.slice/

如果 clone_children = 1,fork 出来的进程就可能:

  • 进入 sub.slice
  • 脱离当前容器的资源限制
  • 导致 limits / 统计全部失效

这在 Kubernetes 中是绝对不可接受的


五、notify_on_release:容器什么时候会被“彻底回收”

# cat notify_on_release
1

在 container scope 中的真实含义为:

当这个容器里最后一个进程退出时,systemd 会自动销毁这个 scope,并回收对应的 cgroup

在具体实践中我们也看到:

  • 容器退出
  • cri-containerd-xxx.scope 目录消失
  • /sys/fs/cgroup 下不留垃圾

如果这个值是 0,会发生什么?

  • scope 残留
  • 僵尸 cgroup
  • systemd-cgls 树不断膨胀
  • 资源统计逐渐失真

Kubernetes 依赖该机制完成“容器级生命周期回收”


六、把四个文件放在一起,读懂“这个容器现在的真实状态”

我们当前看到的是:

cgroup.procs           1438200
tasks                  1438200
cgroup.clone_children  0
notify_on_release      1

在 cri-containerd-xxxxx….scope 这个真实容器中,可以准确解读为:

这个 Kubernetes 容器当前只运行了一个单线程进程(PID 1438200);
所有 fork 出来的子进程都被强制留在当前容器 cgroup 中;
当该进程退出后,这个容器对应的 systemd scope 会被自动销毁并回收资源。

这是一个健康、标准、完全符合 Kubernetes 设计哲学 的 container scope 状态。


七、总结

在 cri-containerd-xxx.scope 中:

文件含义
cgroup.procs这个容器现在有多少进程
tasks这些进程一共开了多少线程
cgroup.clone_childrenfork 是否可能破坏容器隔离
notify_on_release容器退出后是否能被干净回收

现在,我们已经从“看 Pod/YAML”,升级到了“直接读取内核中的容器真实状态”,而这正是 Kubernetes在 AI 推理服务、大规模高负载场景中 排查 OOM、资源争抢、容器异常退出问题的关键能力。

更多推荐