Linux 容器本质探秘:Cgroup 与 Namespace 深度解析与实战

一、引言

很多人日常 docker run、kubectl apply 用得很熟,却说不清一个本质问题:容器到底是什么?

一句被讲烂的话是"容器不是虚拟机"。但只说到这里等于没说。真正的答案是:

容器 = 一个普通的 Linux 进程 + 一组 Namespace(限制它能"看见"什么)+ 一组 Cgroup(限制它能"用掉"多少资源)+ 一套 rootfs(OverlayFS 提供独立文件系统视图)。

前面《Pod 安全》那篇我们强调过"容器共享宿主机内核、Namespace 只是视图隔离而非权限隔离",本文就把这句话拆开讲透。理解了 Cgroup 与 Namespace,你会发现:

  • 为什么容器里 top 看到的 CPU 和主机不一样;
  • 为什么 kubectl exec 进去后 ps aux 只看到自己这一小撮进程;
  • 为什么 requests/limits 最终会变成宿主机上的 cgroup 限制;
  • 以及 OOM、PID 耗尽、namespace 泄漏这些"诡异问题"的根因。
    本文聚焦两大原语:Namespace(隔离视图) 与 Cgroup(限制资源)。文件系统(OverlayFS)与 seccomp/eBPF 留待后续 Linux 专题。
    二、原理解析
    2.1 Namespace:进程看到的"平行宇宙"
    Namespace 是 Linux 内核提供的一种轻量级隔离机制:让一组进程看到一份"被裁剪过"的系统视图,彼此之间互不可见或受限可见。它只改变"看见什么",不改变"能做什么"------这是整篇文章最重要的一个认知,也是容器安全模型的根基。
    Linux 目前有 8 类 Namespace(按引入顺序):
    Namespace
    隔离对象
    典型作用
    Mount (mnt)
    挂载点
    容器有自己的 /proc、/sys,看不到宿主挂载
    UTS
    主机名 / 域名
    容器可设独立 hostname
    IPC
    System V IPC / POSIX 消息队列
    容器间默认不能互发共享内存消息
    PID
    进程号空间
    容器内 ps 只看到自己的进程,PID 1 是 init
    Network (net)
    网络设备、IP、端口、路由、iptables
    容器有独立网卡、IP、端口表
    User
    用户/组 ID 映射
    容器内 root(uid 0) 可映射到宿主普通 uid
    Cgroup
    cgroup 视图
    容器内只能看到自己所属的 cgroup 子树
    Time
    系统时钟
    可独立调整容器时钟(5.6+)
    关键纠偏:PID Namespace 让容器里的进程以为自己是 PID 1,但这不意味着它真的拥有这台机器。宿主上 ps 仍能看到它,只是换了个全局 PID。所谓"容器逃逸"之所以危险,正是因为 Namespace 只是视图隔离------一旦内核漏洞被利用(如脏牛 Dirty COW),进程就能跳出 Namespace 看到全局。这解释了为什么《Pod 安全》强调 readOnlyRootFilesystem、seccompProfile、非 root 等一系列"权限下限"手段:光靠 Namespace 是不够的。
    内核视角下,每个进程所属的 namespace 都体现在 /proc//ns/:

查看某进程加入了哪些 namespace(每个文件是内核中的一个 ns inode)

ls -l /proc/1/ns/

lrwxrwxrwx ... cgroup -> 'cgroup:4026531835'

lrwxrwxrwx ... pid -> 'pid:4026531836'

lrwxrwxrwx ... net -> 'net:4026531840'

...

User Namespace 是安全加分项:通过 uid_map / gid_map,可以把容器内的 uid 0(root)映射到宿主上一个毫无特权的普通 uid。这就是"容器里是 root,宿主上不是 root"的实现原理------也是 rootless 容器(如 Podman rootless)的基石。

2.2 Cgroup:资源的"配额与记账"

如果说 Namespace 管"看见什么",Cgroup(Control Group)管"能用多少"。它把一组进程组织起来,对 CPU、内存、IO、PID 数量等资源施加上限与统计。

cgroup v1 与 v2 的架构差异

这是运维中最容易踩的坑,务必分清:

  • cgroup v1:每类控制器(cpu、memory、blkio...)各自是一棵独立的层级树,路径分散(如 /sys/fs/cgroup/cpu/...、/sys/fs/cgroup/memory/...),配置割裂。
  • cgroup v2(unified hierarchy):所有控制器挂在一棵统一的树下,路径统一在 /sys/fs/cgroup/。新内核(≥4.5 主线,RHEL 8+/Ubuntu 20.04+ 默认)基本都是 v2。
    查看当前系统用的是哪一版:
    stat -fc %T /sys/fs/cgroup/

cgroup2fs -> 使用的是 cgroup v2

tmpfs -> 使用的是 cgroup v1(混合挂载时也可能显示这个,需进一步看 /sys/fs/cgroup 下结构)

核心控制器一览

控制器

作用

v2 关键接口文件

cpu

CPU 时间配额

cpu.max(配额/周期)、cpu.weight

memory

内存上限与统计

memory.max、memory.current、memory.oom.group

pids

进程/线程数上限

pids.max、pids.current

io

磁盘 IO 限速

io.max、io.weight

freezer

暂停/恢复进程组

cgroup.freeze

devices(v1)

限制访问哪些设备


重要认知:cgroup 限制的是"上限",不是"保底"

这是和 Kubernetes requests/limits 映射时最容易混的点:

  • limits → cgroup 的硬上限(如 memory.max、cpu.max),超出即被 throttle 或 OOM kill;
  • requests → 仅用于调度器决策(能不能放下这个 Pod),不是给进程预留的保底资源。没有 limits 时,进程理论上能用满整机,直到触发宿主 OOM。
    内存 cgroup 超限时,内核会触发 OOM Killer 在该 cgroup 内挑一个进程杀掉,并在 dmesg 留下 Out of memory: Killed process ... 记录------这正是《日常排障》那篇里 OOMKilled 的源头。
    2.3 二者如何协作:一个容器的一生
    当你执行 docker run nginx 时,底层大致发生:
  1. clone() 系统调用带上 CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | ... 标志,创建新 namespace;
  2. 在 cgroup 文件树下为这个容器新建目录,写入 memory.max、cpu.max 等限制;
  3. pivot_root / chroot 切换到镜像的 rootfs(OverlayFS 挂载);
  4. 在隔离 + 受限的环境里 exec 目标进程(如 nginx),它成为该 PID namespace 里的 PID 1。
    所以"容器"没有任何黑魔法,全是标准 Linux 内核能力。unshare、nsenter、cgroupfs 这些工具不过是把上述步骤暴露给了人类。
    三、实战演示
    下面所有命令均可在任意现代 Linux(cgroup v2)上复现。
    场景 1:用 unshare 手工造一个"迷你容器"
    不借助 Docker,仅用内核原语隔离一个 shell,直观感受 namespace:

新建 uts+ipc+pid+net+mount namespace,并把它挂在独立根文件系统(这里复用当前根)

sudo unshare --fork --pid --mount --net --uts --ipc --mount-proc chroot / /bin/bash

进去后验证:

hostname mini-container && hostname # UTS:主机名独立

ps aux # PID:只看到自己,且自己是 PID 1

ip link # Network:只有 lo,看不到宿主网卡

echo $$ # 显示 1

退出(exit)后,这些 namespace 随进程释放。这几十行命令,就是 Docker 的本质。

场景 2:查看运行中容器的 cgroup 限制

进入一个 K8s 容器的 shell(或 Docker 容器),直接读 cgroup v2 接口:

容器内执行(需特权或宿主挂载了 cgroup)

cat /sys/fs/cgroup/memory.max # 内存硬上限,对应 limits.memory

cat /sys/fs/cgroup/memory.current # 当前用量

cat /sys/fs/cgroup/cpu.max # 如 "50000 100000" 表示 0.5 核

cat /sys/fs/cgroup/pids.max # PID 上限

宿主侧也可按容器定位(以 Docker 为例):

找到容器短 ID 后

DID=(dockerinspect−f′.Id′my−nginx∣cut−c1−12)ls/sys/fs/cgroup/system.slice/docker−(docker inspect -f '{{.Id}}' my-nginx | cut -c1-12) ls /sys/fs/cgroup/system.slice/docker-(dockerinspect−f′.Id′my−nginx∣cut−c1−12)ls/sys/fs/cgroup/system.slice/docker−{DID}.scope/

场景 3:用 cgroup v2 手动限制一个进程的资源

用 stress-ng 模拟压力,证明 cgroup 限制真实生效:

1) 建一个 cgroup 并设限制:最多 0.5 核、最多 100MB 内存

sudo mkdir -p /sys/fs/cgroup/demo

echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max

echo "100M" | sudo tee /sys/fs/cgroup/demo/memory.max

2) 把当前 shell 移入该 cgroup

echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs

3) 跑压力测试:请求 4 核 CPU、分配 500MB

stress-ng --cpu 4 --vm 1 --vm-bytes 500M --timeout 20s &

观察:CPU 占用被压在 ~50%,内存超过 100M 时该进程被 OOM kill

场景 4:nsenter 进入运行中容器的 namespace 排障

容器没有 shell 或 kubectl exec 不通时,从宿主直接进入其 namespace(呼应《日常排障》的 kubectl debug):

找到容器内的 1 号进程 PID(宿主视角)

PID=$(docker inspect -f '{{.State.Pid}}' my-nginx)

进入它的 net/mount/pid namespace 执行命令

sudo nsenter -t $PID -n -m -p -- ip addr # 看容器内网络

sudo nsenter -t $PID -n -m -p -- ps aux # 看容器内进程

sudo nsenter -t $PID -n -m -p -- netstat -tlnp # 看容器内监听端口

场景 5:User Namespace 与 root 映射

新建 user + pid namespace,把容器内 uid 0 映射到宿主 uid 1000

unshare --user --pid --fork --mount-proc

--map-root-user bash

id # 容器内显示 uid=0(root)

宿主侧该进程实际以 uid=1000 运行(ps -o uid= -p <宿主PID> 可见)

这解释了 rootless 容器的安全性:容器内是 root,但宿主上只是个普通用户,即便逃逸也拿不到宿主特权。

四、排错实战

4.1 容器频繁 OOM:cgroup 内存 vs 宿主 OOM

现象:Pod 重启,kubectl describe 显示 OOMKilled;或宿主 dmesg 出现 Out of memory: Killed process。

定位:

容器内确认是否被 cgroup 限制杀掉

dmesg -T | grep -i "oom|killed" | tail

或读 memory.events(v2)

cat /sys/fs/cgroup/memory.events # oom_kill 计数 > 0 即发生过

宿主侧看全局 OOM(区别于 cgroup 内 OOM)

grep -i "out of memory" /var/log/kern.log

根因与修复:

  • 多半是 limits.memory 设太小,或应用确有内存泄漏 → 调大 limits、查内存泄漏;
  • 注意 JVM、Go、Node 等运行时的"堆外/缓存"不认容器内存限制,需显式传 -Xmx / GOMEMLIMIT / --max-old-space-size,否则会算着"宿主机总内存"去分配,撞上 cgroup 上限被 OOM;
  • 若宿主整体内存紧张触发全局 OOM,会优先杀 oom_score_adj 高的进程(容器进程通常被调高),需靠调度打散、加节点或加 requests 保底。
    4.2 cgroup v1/v2 混用导致 Kubelet 起不来
    现象:节点 kubelet 启动失败,日志报 failed to find cgroup mount 或 cgroup v2 not supported。
    根因:Kubelet 对 cgroup 驱动有要求(推荐 systemd 驱动 + 统一 v2),而某些老发行版内核仍是 v1,或开启了 systemd.unified_cgroup_hierarchy=0 强制 v1。
    修复:

确认版本

stat -fc %T /sys/fs/cgroup/

若需切到 v2:在内核启动参数加(RHEL/CentOS 在 /etc/default/grub)

systemd.unified_cgroup_hierarchy=1

然后 grub2-mkconfig -o /boot/grub2/grub.cfg && reboot

4.3 PID 耗尽:fork: retry: No child processes

现象:容器里新进程起不来,报 fork: retry: No child processes 或 Resource temporarily unavailable,但内存、CPU 还很空闲。

根因:pids cgroup 达到 pids.max 上限(如某应用疯狂 fork 子进程/线程泄漏)。K8s 默认不限制 PID,需显式配 kubelet --pod-pids-limit 或 Pod 级 annotations: kubernetes.io/pid-limit。

定位:

cat /sys/fs/cgroup/pids.current # 已用

cat /sys/fs/cgroup/pids.max # 上限(max 表示无限制,可能已被宿主总 PID 限)

ps -eLf | wc -l # 总线程数

4.4 Namespace 泄漏:已退出容器残留 netns

现象:ip netns list(或 /var/run/docker/netns)里堆积大量残留条目,或 ss -tan 看到诡异 socket。常见于早期 Docker / 某些 CNI 插件的 bug:容器退出但 net namespace 没清理干净,消耗内核资源。

修复:升级 Docker / CNI 版本;必要时手动清理残留 netns 或重启 docker 服务(生产需谨慎)。

4.5 排错决策树

应用异常

├─ 内存暴涨/被杀 → 读 memory.events、dmesg OOM → 调 limits / 修泄漏 / 配运行时堆上限

├─ 进程起不来、fork 失败 → 查 pids.current vs pids.max → 提 pid 上限 / 查泄漏

├─ CPU 被压死 → 读 cpu.max、cpu.stat(throttled) → 调 cpu limit / 查忙循环

├─ Kubelet/容器运行时起不来 → 查 cgroup 版本与驱动 → 切 v2 / 配 systemd 驱动

└─ 视图"不对"(看不到网卡/进程) → 查 /proc//ns、unshare 参数 → namespace 没建对

五、总结

  • Namespace 管"看见什么":8 类隔离把进程圈进各自的平行宇宙,但只是视图隔离、共享同一内核------这正是容器安全必须叠加"权限下限"(非 root、seccomp、只读根)的根本原因。
  • Cgroup 管"用掉多少":v2 统一层级,靠 memory.max/cpu.max/pids.max 等接口施加硬上限;它限制上限而非保底,OOM 与 PID 耗尽都源于此。
  • 二者 + OverlayFS = 容器:unshare、nsenter、cgroupfs 不过是把内核能力暴露给人,Docker/K8s 只是编排了这些步骤。
相关推荐
万联WANFLOW2 小时前
CVE-2026-98432 等漏洞自查:AI 查出的 Linux、Windows 漏洞,运维怎么确认受不受影响
linux·运维·windows·网络安全
debugdynamo2 小时前
Netcatty:把 SSH 客户端做回工作台
运维·其他·ssh
时空无限2 小时前
ssh 直接免密输入 sudo 密码
运维·ssh
OPEN-F2 小时前
Ubuntu 自动登录后 GNOME 远程桌面连不上?一次登录钥匙环锁定的排查与永久修复
linux·运维·ubuntu
数字新视界2 小时前
机房动环监控系统在提升数据中心能效与安全中的应用探索
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
硅基手札2 小时前
【Linux 内核专栏 19】20 · 嵌入式子系统 — GPIO·pinctrl·clock·regulator
linux·运维·单片机·linux内核·嵌入式开发·调试定位
默|笙3 小时前
【Linux网络】传输层协议TCP(3)滑动窗口、流量控制
linux·网络·tcp/ip
修心光3 小时前
记录一次,jenkins打包异常问题
运维·jenkins
分布式存储与RustFS3 小时前
JuiceFS 接 RustFS 当数据后端:把对象存储挂成本地文件系统
运维·云原生·开源·对象存储·分布式存储·s3·性能基准