深入理解 Linux Namespace 与 Cgroup:容器技术的内核基石

引言

在云原生时代,容器技术已经成为软件发布的基础设施。无论是 Docker 还是 Kubernetes,其底层都依赖于 Linux 内核的两大核心机制:Namespace(命名空间)和 Cgroup(控制组)。Namespace 负责隔离——让进程看到独立的系统视图;Cgroup 负责限制——让进程使用可控的资源配额。

本文将从操作系统内核的角度,深入解析这两大机制的设计哲学、API 演进以及在生产容器运行时中的工程实践。

一、Namespace:隔离的艺术

1.1 什么是 Namespace

Namespace 是 Linux 内核用来隔离全局资源的一种机制。它可以让一组进程看到独立于其他进程的系统资源视图——包括进程 ID、网络栈、文件系统挂载点、主机名等。

在没有 Namespace 的世界中,所有进程共享同一套系统资源视图。进程 1 总是 init/systemd,所有机器上的 hostname 返回相同的根文件系统。Namespace 打破了这个假设。

1.2 Namespace 的八大类型

截至 Linux 6.x 内核,共有 8 种 Namespace 类型:

Namespace 系统调用 Flag 隔离内容 内核版本
Mount CLONE_NEWNS 文件系统挂载点 2.4.19
UTS CLONE_NEWUTS 主机名与域名 2.6.19
IPC CLONE_NEWIPC System V IPC、POSIX 消息队列 2.6.19
PID CLONE_NEWPID 进程 ID 编号空间 2.6.24
Network CLONE_NEWNET 网络协议栈、路由表、防火墙规则 2.6.29
User CLONE_NEWUSER 用户和组 ID 映射 3.8
Cgroup CLONE_NEWCGROUP Cgroup 根目录视图 4.6
Time CLONE_NEWTIME 系统时钟(启动/单调时间) 5.6

1.3 核心系统调用

Namespace 的创建涉及三个关键系统调用:

clone() — 创建新进程时指定 Namespace 标志:

pid_t pid = clone(child_func, child_stack + STACK_SIZE,
                  CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWNET | SIGCHLD,
                  NULL);

unshare() — 让当前进程脱离某个 Namespace,创建新的:

# 创建一个独立的 PID 命名空间运行 shell
sudo unshare --pid --fork --mount-proc /bin/bash

setns() — 让当前进程加入某个已存在的 Namespace:

int fd = open("/proc/1234/ns/pid", O_RDONLY);
setns(fd, CLONE_NEWPID);

Docker 的 docker exec 命令就是通过 setns() 让调试工具加入目标容器的 Namespace 实现的。

1.4 PID Namespace 深度解析

PID Namespace 是最复杂的 Namespace 之一。关键特性:

  • 每个 PID Namespace 的进程编号独立,从 1 开始
  • PID Namespace 是嵌套的,可以有 15 层深度
  • 父 Namespace 可以看到子 Namespace 的进程,反之不行
  • PID 1 有特殊职责:收养孤儿进程、转发信号
# 示例:创建嵌套的 PID Namespace
# 在 PID Namespace 中,"init" 进程就是新创建进程
sudo unshare --pid --fork mount -t proc none /proc
ps aux  # 只看到一个 PID 1 的 shell

1.5 User Namespace:安全的基石

User Namespace 实现了 UID/GID 映射,这是容器安全的核心:

容器内 UID 0 (root)
    ↓ 映射到
容器外 UID 100000 (普通用户)
# 查看映射关系
cat /proc/self/uid_map
# 输出: 0 100000 65536

这意味着容器内的 root 进程在宿主机上只是一个普通用户进程,极大降低了安全攻击面。

1.6 Network Namespace:网络隔离的魔法

Network Namespace 复制了整个网络协议栈:

# 创建网络 Namespace
ip netns add mynet

# 在 Namespace 中回环接口默认是 down 的
ip netns exec mynet ip link show lo
# 输出: LOOPBACK, DOWN

# 启动回环接口
ip netns exec mynet ip link set lo up

Docker 每创建一个容器,就创建一个新的 Network Namespace,通过 veth pair 将容器与宿主机网桥(docker0)连接。

二、Cgroup:资源管控的引擎

2.1 Cgroup 的设计哲学

控制组(Control Groups)是 Linux 内核的一个功能,用来限制、记录和隔离进程组所使用的物理资源(CPU、内存、磁盘 I/O 等)。

Cgroup 的核心数据结构是 hierarchy(层级树)。每个 subsystem(控制器)挂载到一个 hierarchy 上,每个进程可以属于任意数量的不同 hierarchy 中的 cgroup。

2.2 Cgroup v1 vs v2

特性 Cgroup v1 Cgroup v2
发布时间 2008 (2.6.24) 2016 (4.5)
设计模型 每个控制器独立 hierarchy 统一 hierarchy
进程规则 同一 hierarchy 下只能属于一个 cgroup 同样约束,但更严格
线程模式 不支持 支持 per-thread 资源控制
默认状态 Ubuntu 20.04 默认 Fedora/RHEL 8+ 默认
压力停滞信息 无 memory.pressure PSI (Pressure Stall Information)

Docker 20.10+ 和 Kubernetes 1.20+ 已经开始支持 Cgroup v2。Ubuntu 22.04+ 和 Debian 11+ 默认启用 v2。

2.3 核心控制器

cpu 控制器 — 控制 CPU 使用比例:

# 创建 cgroup
mkdir /sys/fs/cgroup/cpu/myapp

# 限制 CPU:每 100ms 周期内最多使用 50ms
echo 50000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
echo 10 > /sys/fs/cgroup/cpu/myapp/cfs_period_us  # 默认值(可选)

# 将进程加入 cgroup
echo $$ > /sys/fs/cgroup/cpu/myapp/cgroup.procs

memory 控制器 — 控制内存使用:

# 限制内存上限为 512MB
echo 536870912 > /sys/fs/cgroup/memory/myapp/memory.limit_in_bytes

# 限制物理内存 + Swap
echo 536870912 > /sys/fs/cgroup/memory/myapp/memory.memsw.limit_in_bytes

# 设置软限制(低优先级,尽力而为)
echo 268435456 > /sys/fs/cgroup/memory/myapp/memory.soft_limit_in_bytes

io 控制器 — 控制磁盘 I/O:

# 限制读带宽 10MB/s (device 8:0 = /dev/sda)
echo "8:0 10485760" > /sys/fs/cgroup/io/myapp/io.max

pids 控制器 — 限制进程数:

# 限制最多 100 个进程
echo 100 > /sys/fs/cgroup/pids/myapp/pids.max

2.4 Cgroup v2 的改进语法

Cgroup v2 提供了更简洁的接口:

# Cgroup v2 启用的判断
$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# CPU 限制(权重模式)
echo "max 50000 100000" > /sys/fs/cgroup/myapp/cpu.max

# 内存限制
echo "536870912" > /sys/fs/cgroup/myapp/memory.high
echo "536870912" > /sys/fs/cgroup/myapp/memory.max

# IO 限制
echo "8:0 rbps=10485760" > /sys/fs/cgroup/myapp/io.max

三、Namespace + Cgroup = 容器

3.1 容器本质公式

容器 = Namespace (隔离) + Cgroup (资源限制) + Rootfs (文件系统)

这不是一个比喻——而是对容器技术的精确描述:

  • Namespace 决定了容器内进程"能看到什么"
  • Cgroup 决定了容器内进程"能用多少"
  • Rootfs + OverlayFS 决定了容器内进程"能访问哪些文件"

3.2 容器运行时的 Namespace 和 Cgroup 对照表

以 runc 兼容运行时为例,Docker/K8s 容器启动时:

runtime-spec.linux.namespaces:
  - type: pid       → CLONE_NEWPID
  - type: network   → CLONE_NEWNET
  - type: mount     → CLONE_NEWNS
  - type: ipc       → CLONE_NEWIPC
  - type: uts       → CLONE_NEWUTS
  - type: cgroup    → CLONE_NEWCGROUP  (K8s 1.19+)
  - type: user      → CLONE_NEWUSER   (可选)

runtime-spec.linux.resources:
  cpu.shares: 1024
  memory.limit: 536870912
  pids.limit: 1024

3.3 容器中的 Cgroup 层级

Docker 在 Cgroup 中的目录结构:

/sys/fs/cgroup/
└── memory
    └── docker
        └── a1b2c3d4e5f6...  (容器ID)
            ├── memory.limit_in_bytes
            ├── memory.usage_in_bytes
            ├── memory.stat
            └── cgroup.procs

Kubernetes Pod 的 Cgroup 结构:

/sys/fs/cgroup/
└── kubepods
    └── besteffort  / burstable / guaranteed
        └── pod<POD_UID>
            ├── crio-<CONTAINER_ID>.scope  (容器)
            └── ...

3.4 特殊的 Namespace 共享策略

Kubernetes 通过 Namespace 共享实现 Pod 内容器协作:

  • Network Namespace 共享:同一 Pod 内所有容器共享网络栈,localhost 互通
  • IPC Namespace 共享:同一 Pod 内容器可共享内存和信号量
  • PID Namespace 共享(v1.25+ alpha):同一 Pod 内容器可看到彼此的进程,便于信号传递和调试

四、工程实践深度解析

4.1 Docker run 参数的底层映射

docker run -it \
  --name=my-container \
  --cpus=2 \                            # cpu.max: "200000 100000"
  --memory=512m \                       # memory.max: 536870912
  --pids-limit=100 \                    # pids.max: 100
  --uts=host \                          # 共享 UTS Namespace
  --net=bridge \                        # 新建 Network Namespace
  --pid=container:other \               # 共享目标容器的 PID NS
  --userns=host \                       # 共享 User Namespace
  --cgroupns=private \                  # 新建 Cgroup Namespace
  ubuntu:22.04 /bin/bash

4.2 常见问题与排查

问题 1:容器内看不到自己的进程列表

# 症状:mount 了宿主机的 /proc
# 解决:确保正确挂载独立的 procfs
mount -t proc none /proc

问题 2:Cgroup 内存限制导致 OOM

# 查看 OOM 事件计数
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.oom_control

# 分析 OOM 时的内存使用
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.stat

问题 3:Network Namespace 中 DNS 解析失败

# 检查 Namespace 中的 resolv.conf
ip netns exec mynet cat /etc/resolv.conf

# 手动配置
mkdir -p /etc/netns/mynet
echo "nameserver 8.8.8.8" > /etc/netns/mynet/resolv.conf

4.3 生产级最佳实践

  1. 永远不要共享 User Namespace:除非你完全理解 uid_map/gid_map 的含义
  2. Cgroup v2 优先:统一 hierarchy 简化管理,PSI 提供更精准的 OOM 预警
  3. 不要过度隔离:按业务场景选择合适的 Namespace 共享策略
  4. 资源限制是硬上限:memory.max 触发后 OOM Killer 不会留情
  5. 使用 cgroup namespace:防止容器内进程看到宿主机的 cgroup 拓扑信息

五、下一代容器技术展望

5.1 eBPF 的深度融合

随着 eBPF 的成熟,Namespace 和 Cgroup 与 eBPF 的结合催生了新一代可观测性和安全性方案:

  • Tetragon (Cilium):基于 eBPF 的运行时安全监控,可跟踪容器内进程跨 Namespace 行为
  • Falco:系统调用审计,结合 Namespace 信息过滤
  • Pixie:零侵染获取容器内观测数据

5.2 WebAssembly 与 Micro-VM

Firecracker / Kata Containers / gVisor 等方案在 Namespace+Cgroup 基础上增加了用户态内核拦截层,提供更强的隔离保证。

5.3 Kubernetes Sidecar-less 革命

Istio Ambient Mesh 去掉 Sidecar,改为基于 eBPF 的 ztunnel 共享代理。这是 Namespace 与 eBPF 联动的经典案例——每个节点一个 ztunnel 实例,通过 eBPF 将流量劫持到该实例处理。

总结

Namespace 和 Cgroup 是 Linux 容器技术的两大支柱:

  • Namespace 负责"让进程以为自己独占了系统"——视觉隔离
  • Cgroup 负责"让进程只能用分配到的资源"——资源管控

理解了这两者,你就理解了 Docker、containerd、CRI-O 等容器运行时 90% 的内核交互逻辑。对于 SRE 和平台工程师而言,这不仅是理论知识,更是排查生产故障、优化容器性能的必备技能。

推荐资源

  • 《Linux Container Internals》— Jessica McKellar
  • Linux 内核文档:Documentation/admin-guide/cgroup-v2.rst
  • man 手册:man 7 namespaces, man 7 cgroup
  • 在线实验:Play with Docker

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.355226s