Linux 命名空间与 Cgroups 深度实战:容器技术的两大基石

容器技术的兴起源于对资源隔离与限制的两大需求。Linux 内核提供的 Namespace(命名空间)和 Cgroups(控制组)正是支撑现代容器运行时(Docker、containerd、Kubernetes等)的核心技术。本文将从源码级别和实战角度,深入解析这两大机制的工作原理与工程应用。

一、Namespace 命名空间:构建隔离的视图

1.1 Namespace 概述

Namespace 是 Linux 内核提供的一种资源隔离机制,它允许不同进程组看到不同的系统视图。你可以将其理解为"进程的视觉分隔"——每个 Namespace 中的进程只能看到属于该 Namespace 的资源,而无法感知其他 Namespace 的存在。

Linux 内核一共提供了 8 种 Namespace:

Namespace隔离资源系统调用标志内核版本
Mount (mnt)文件系统挂载点CLONE_NEWNS2.4.19
PID进程 ID 编号空间CLONE_NEWPID2.6.24
Network (net)网络设备、协议栈、路由表CLONE_NEWNET2.6.29
UTS主机名和域名CLONE_NEWUTS2.6.19
IPCSystem V IPC、POSIX 消息队列CLONE_NEWIPC2.6.19
User用户和组 IDCLONE_NEWUSER3.8
CgroupCgroup 根目录CLONE_NEWCGROUP4.6
Time系统时钟(BOOT/MONOTONIC)CLONE_NEWTIME5.6

1.2 核心系统调用

Namespace 的创建和管理依赖三个核心系统调用:

// clone: 创建新进程同时创建新 Namespace
int clone(int (*fn)(void *), void *stack, int flags, void *arg);

// unshare: 将当前进程从共享的 Namespace 中分离
int unshare(int flags);

// setns: 将当前进程加入到已存在的 Namespace
int setns(int fd, int nstype);

其中 flags 参数使用位掩码组合多个 Namespace 类型,例如 CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWNET。

1.3 Mount Namespace:文件系统的隔离

Mount Namespace 是 Linux 最早实现的 Namespace 类型(始于 2002 年)。它实现了文件系统挂载点的隔离,使得不同 Namespace 中的进程可以看到完全不同的文件系统树。

关键特性:

  • 传播类型控制:通过 MS_SHARED/MS_PRIVATE/MS_SLAVE/MS_UNBINDABLE 控制挂载事件的跨 Namespace 传播
  • 容器镜像基础:Docker/OCI 容器通过 Mount Namespace 实现根文件系统的独立挂载,实现不同容器运行不同发行版
  • OverlayFS 配合:现代容器运行时利用 Mount Namespace + OverlayFS 构建写时复制(CoW)的分层镜像,充分利用缓存减少磁盘占用

1.4 PID Namespace:进程编号的虚拟空间

PID Namespace 实现了进程 ID 的隔离,使得每个 Namespace 内部都有独立的 PID 编号空间。最典型的新 Namespace 中第一个进程会被分配 PID 1,它扮演着类似宿主机上 init 进程的角色。

# 在新的 PID Namespace 中执行 shell
# 看不到宿主机上的其他进程,自己成为 PID 1
$ sudo unshare --pid --fork --mount-proc /bin/bash
$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4532  2268 pts/0    S    10:00   0:00 /bin/bash

PID Namespace 支持嵌套,层数上限为 32 层。这在容器运行容器(Docker-in-Docker)场景中极为关键。

1.5 Network Namespace:网络世界的平行宇宙

Network Namespace 实现了网络协议栈的完全隔离。当创建新的 Network Namespace 时,其中仅包含环回接口(lo),所有物理/虚拟网络接口都属于初始的根 Namespace。

# 创建 Network Namespace
sudo ip netns add blue
sudo ip netns add green

# 查看当前 Network Namespace 列表
$ sudo ip netns list
green
blue (id: 0)

# 在 blue namespace 中执行命令
$ sudo ip netns exec blue ip link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

一个完整的 Network Namespace 包含:

  • 独立的网络设备(eth0、veth pair 端点)/li> li>独立的路由表和路由规则
  • 独立的 iptables/netfilter 规则链
  • 独立的 socket、协议栈状态(TCP/UDP 缓冲区等)/li> li>独立的 /proc/net 和 /sys/class/net 视图

跨 Namespace 通信则依赖 veth pair(虚拟以太网对),一端的网络包会直接从另一端转发,类似一根虚拟网线连接两个平行的网络世界。

1.6 其他 Namespace 类型

UTS Namespace:隔离主机名(hostname)和域名(NIS domain name),使不同容器可以设置不同的 hostname,这在容器编排系统中是基本需求。

IPC Namespace:隔离 System V IPC 对象(信号量、消息队列、共享内存段)和 POSIX 消息队列。不同容器中的进程无法通过 IPC 通信。

User Namespace:实现 UID/GID 的映射与隔离,普通用户在容器内以 root(UID 0)运行时会映射到宿主机非特权用户,极大提升了安全性。这也是 Rootless Docker 的技术基础。

Cgroup Namespace:隔离进程看到的 cgroup 路径视图,让容器内的进程认为自己从 / 开始就是 cgroup 根。

Time Namespace:Linux 5.6 引入,允许不同 Namespace 看到不同的系统时间,适用于需要模拟特定时间点的测试场景。

二、Cgroups 控制组:资源的精细调度

2.1 Cgroups 历史沿革

Cgroups(Control Groups)最早由 Google 工程师 Paul Menage 和 Rohit Seth 在 2006 年提出,最初名为"Process Containers"。2007 年正式合并入 Linux 2.6.24 内核,后经过 Tejun Heo 彻底重写,推出 cgroup v2(自 4.5 起可在生产环境使用)。

cgroup v1 存在几个显著缺陷:

  • 控制器之间缺乏统一的一致性视图
  • 进程只能属于一个层级树,限制了配置灵活性
  • 与线程模式的冲突,导致行为不一致
  • 在容器热迁移、资源审计时存在不可靠性

cgroup v2 从根本上解决了这些问题。

2.2 cgroup v2 设计哲学

v1(碎片化):          v2(统一):
├─ cpu,cpuacct          ├─ /sys/fs/cgroup/
├─ memory               │   ├─ system.slice/
├─ blkio        →       │   │   ├─ cpu.max
├─ devices    统一到    │   │   ├─ memory.max
├─ pids                 │   │   └── io.max
└─ hugetlb              │   └─ user.slice/
                         │       ├─ cpu.max
                         │       └─ memory.high

cgroup v2 的核心设计:

  • 统一层级树:所有控制器共享单一的 cgroup 树
  • 内部节点无进程:进程只能存在于叶子节点(叶子节点约束),v1 中同一组织单元进程分散在多棵树的问题得到解决
  • "no internal processes" rule
  • 渐进式资源控制:提供 threshold 通知机制,如 memory.high 在超额时触发召回而非直接 OOM
  • 线程模式完善:对进程和线程级别的资源记账更加可控

2.3 CPU 控制器实战

v2 中 CPU 资源分配主要通过 cpu.max 实现,格式为 $MAX $PERIOD,表示在 $PERIOD 微秒内可以使用 $MAX 微秒的 CPU 时间。

# 创建 cgroup
sudo mkdir -p /sys/fs/cgroup/mycontainer

# 限制为 1 核心的 50%(0.5 CPU)
echo "50000 100000" | sudo tee /sys/fs/cgroup/mycontainer/cpu.max

# 启用 CPU 控制器
echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# 将进程加入 cgroup
echo $$ | sudo tee /sys/fs/cgroup/mycontainer/cgroup.procs

# 压力测试
$ stress --cpu 4 --timeout 10s

cpu.weight:在竞争 CPU 时分配相对权重(默认 100),范围 1-10000。权重越高,在竞争中获得的时间片越多。

# 竞争时分配 3:1 的权重比
echo "300" | sudo tee /sys/fs/cgroup/container-a/cpu.weight
echo "100" | sudo tee /sys/fs/cgroup/container-b/cpu.weight

RT(实时)进程限制:通过 cpu.max 和 cpu.rstat 可限制实时进程的调度预算。

2.4 Memory 控制器实战

v2 内存控制器提供完整的内存用量管控能力:

文件作用类型
memory.max硬限制,超出触发 OOM绝对值
memory.high软限制,超额触发回收压力绝对值
memory.low内存保护优先级绝对值
memory.min保证的最低可用内存绝对值
memory.swap.maxSwap 使用限制绝对值
memory.peak观测历史峰值只读
# 创建内存限制组,硬限制 512MB,高水位 400MB
echo "536870912" | sudo tee /sys/fs/cgroup/mycontainer/memory.max
echo "419430400" | sudo tee /sys/fs/cgroup/mycontainer/memory.high
echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# 将进程加入并检查
echo $$ | sudo tee /sys/fs/cgroup/mycontainer/cgroup.procs
cat /sys/fs/cgroup/mycontainer/memory.current   # 查看当前用量
cat /sys/fs/cgroup/mycontainer/memory.stat      # 查看统计详情

memory.max vs memory.high 的区别:

  • memory.high:达到此值时内核会对该 cgroup 施加页面回收压力,试图将内存压低到 high 以下。如果成功,进程可能观测到短暂的分配延迟或页面错误(page fault),但不会导致进程死亡。
  • memory.max:硬限制。当 cgroup 内存使用达到 max 且回收失败时,OOM killer 会终止该 cgroup 内的一个或多个进程。对于容器化进程,这是最后的安全阀。

2.5 I/O 控制器(io.max/io.low)实战

v2 的 I/O 控制器名为 io(不同于 v1 的 blkio),比 v1 更加精细:

# 限制 /dev/sda 读取带宽 10MB/s、写入带宽 5MB/s
echo "10737418240 5368709120" | sudo tee /sys/fs/cgroup/mycontainer/io.max

# 查看 I/O 统计
$ cat /sys/fs/cgroup/mycontainer/io.stat
8:0 rbytes=1234567 wbytes=8910112 rios=234 wios=456 dbytes=0 dios=0

io.weight:类似 CPU 权重,对 I/O 带宽进行相对分配。io.low 则提供最小带宽保障(区别于 max 的硬性上限)。

2.6 Pids 控制器

pids.max 限制 cgroup 内可以创建的进程/线程总数,防止 fork 炸弹攻击耗尽系统 PID:

echo "100" | sudo tee /sys/fs/cgroup/mycontainer/pids.max
echo "+pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# 检查当前使用量
cat /sys/fs/cgroup/mycontainer/pids.current

三、从零理解容器:Namespace + Cgroups 的协同

容器的本质可以概括为:Namespace(隔离视图)+ Cgroups(资源限制)+ Rootfs(文件系统)+ Security(Capabilities/Seccomp/SELinux)。

以下是使用 system-call 手动构建一个极简容器的伪代码逻辑,帮助理解两者如何协同工作:

// clone 出新的 PID/Mount/Network/UTS/IPC Namespace
cmd := exec.Command("/proc/self/exe", "child")
cmd.SysProcAttr = &syscall.SysProcAttr{
    Cloneflags: syscall.CLONE_NEWPID |
                syscall.CLONE_NEWNS |
                syscall.CLONE_NEWNET |
                syscall.CLONE_NEWUTS |
                syscall.CLONE_NEWIPC,
   UidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65534, Size: 1}},
    GidMappings: []syscall.SysProcIDMap{{ContainerID: 0, HostID: 65534, Size: 1}},
}
cmd.Run()

// 在子进程(容器 namespace 内部)中:
// 1. 设置 hostname(UTS)
syscall.Sethostname([]byte("my-container"))
// 2. 设置 rootfs(Mount)
syscall.PivotRoot(newRoot, oldRoot)
// 3. Mount proc(PID)
syscall.Mount("proc", "/proc", "proc", 0, "")
// 4. 配置网络(Network)—— 创建 veth pair 的一端放入 namespace

然后在宿主机侧:

// 使用 cgroup 限制容器内所有进程的资源使用
containerCgroup := "/sys/fs/cgroup/containers/my-container"
os.MkdirAll(containerCgroup, 0755)

ioutil.WriteFile(containerCgroup+"/cpu.max", []byte("200000 100000"), 0644)
ioutil.WriteFile(containerCgroup+"/memory.max", []byte("134217728"), 0644)
ioutil.WriteFile(containerCgroup+"/pids.max", []byte("64"), 0644)

// 将容器 PID 1 进程加入 cgroup
ioutil.WriteFile(containerCgroup+"/cgroup.procs", []byte(strconv.Itoa(containerPid)), 0644)

这就是 docker run --cpus=2 --memory=128m --pids-limit=64 在系统调用层面实际做的事情。

四、cgroup 文件系统与 systemd 集成

在基于 systemd 的现代 Linux 发行版中,cgroup v2 深度集成。systemd 将 cgroup 层级单元化为 .service、.slice。你可以通过 systemd 工具直接查看每个服务的资源使用:

$ systemd-cgtop
Control Group                       Tasks   %CPU   Memory  Input/s Output/s
/user.slice/user-1000.slice          386   12.5   4.1G        -        -
/system.slice/docker.service          12    2.3   512M        -        -
/system.slice/kubelet.service         8    0.8   256M        -        -

为 systemd 服务设置资源限制变得简单:

[Service]
# 0.5 CPU
CPUQuota=50%
# 硬限制 512MB
MemoryMax=512M
# 软限制/高水位 400MB
MemoryHigh=400M
# 进程数限制
TasksMax=256
# I/O 权重
IOWeight=200

这实际上会在 /sys/fs/cgroup/system.slice/<service>.service/ 下创建对应的 cgroup 控制文件。

五、生产环境最佳实践与陷阱

5.1 内存限制与 OOM 处理

设置 memory.max 时务必考虑以下因素:

  • 应用 JVM/Node.js 等运行时需要预留堆外内存:JVM 的 MaxHeapSize 只是堆内存,还需考虑 MetaSpace、线程栈、JIT Code Cache、GC 开销等。建议 memory.max = MaxHeapSize * 1.3。
  • 利用 memory.high 做软限制:设置 memory.high 为 memory.max 的 80-90%,在内存达到 OOM 之前提前触发页回收,给应用一个缓冲期。(Kubernetes 的 requests.memory 和 limits.memory 正是对应 memory.high 和 memory.max 的语义)
  • Swap 使用策略:memory.swap.max = memory.max 表示不启用 Swap;设为 0 表示 cgroup 总可用内存为 memory.max 的匿名内存 + cache,但不允许匿名内存换出。在生产环境中,重要的数据库/缓存服务应考虑 Swap 禁用。

5.2 CPU 限制与 throttling 监控

Kubernetes 的 Pod 处于 Burstable(即设了 requests 和 limits)时,可能频繁触发 CPU throttling。监控 container_cpu_cfs_throttled_periods_total 指标至关重要:

alert: ContainerCpuThrottled
expr: rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m]) > 0.5
for: 10m
labels:
  severity: warning

解决 throttling 的常见方法是增加 CPU limit 或改为 Guaranteed 类型(limits = requests)。

5.3 PID Namespace 与 init 进程设计

在 PID Namespace 内,PID 1 进程必须承担特殊责任:

  • 收割僵尸进程:init 进程必须 wait() 回收孤儿僵尸进程,否则 PID 资源会耗尽
  • 信号转发:容器 stop 等场景依赖 PID 1 正确转发信号给子进程
  • tini/dumb-init 等 init 工具:像 ENTRYPOINT 中使用 ["/usr/bin/dumb-init", "--"] 可以让普通 init 工具正确处理信号和进程回收

5.4 cgroup v1 到 v2 迁移注意事项

许多老旧工具/监控系统仍假定 cgroup v1 路径,切换到 v2 时常见问题包括:

  • cAdvisor 在 v1 中通过读取 cpu.stat、memory.usage_in_bytes 获取指标,v2 中文件名和格式均不同
  • Kubernetes 的 cadvisor + node-exporter 组合在 v2 支持上需要较新版本
  • 一些 Java 应用的 cgroup 自动检测逻辑(如 -XX:MaxRAMPercentage)需要版本 >= JDK 10u191 / JDK 11u13

六、Namespace 与 Cgroups 内核实现亮点

6.1 Namespace 的 nsproxy

Linux 通过 nsproxy 结构体(kernel/nsproxy.c)管理进程所属的 Namespace 集合:

struct nsproxy {
    atomic_t count;
    struct uts_namespace *uts_ns;       // UTS
    struct ipc_namespace *ipc_ns;       // IPC
    struct mnt_namespace *mnt_ns;       // Mount
    struct pid_namespace *pid_ns;       // PID
    struct net         *net_ns;         // Network
    struct cgroup_namespace *cgroup_ns; // Cgroup
    struct time_namespace *time_ns;     // Time
};

// task_struct 中引用 nsproxy
struct task_struct {
    ...
    struct nsproxy *nsproxy;
};

每个进程的 nsproxy 在 fork/>clone 时根据新创建的 Namespace 进行替换,实现进程在多个 Namespace 间的"视觉切换"。

6.2 cgroup 的 v2 统一层级

cgroup v2 的内核实现中,通过 cgroup 结构体统一管理所有控制器状态。每个 cgroup 内嵌一个 cgroup_subsys_state 数组:

struct cgroup {
    struct cgroup_subsys_state self; // cgroup 自身的 CSS
    unsigned long flags;
    // ...各种控制器特定状态
};

struct cgroup_subsys_state {
    struct cgroup *cgroup;
    struct cgroup_subsys *ss; // 指向具体子系统
};

// 控制器通过 css 获取自身状态
struct mem_cgroup {
    struct cgroup_subsys_state css;
    struct page_counter memory;
    // ...
};

v2 中控制器必须声明支持的统一统一层级语义,内核保证"no internal processes",因此同一个层级中各控制器的叶子节点进程集合是一致的。

七、总结与展望

Namespace 和 Cgroups 这两大机制共同构成了 Linux 容器化的技术底座。理解它们有助于:

  • 排查容器逃逸问题:熟悉各类 Namespace 的边界,当出现信息泄露时能快速定位是哪个隔离失效
  • 精准诊断性能瓶颈:区分是 Namespace 切换的开销还是 Cgroup throttling
  • 合理配置资源限制:避免 memory.max 设置不合理导致频繁 OOM,或不合理 CPU limit 导致 throughput 下降
  • 设计云原生基础设施:在构建 PaaS/K8s 运行时、监控方案时进行更深层次的选型判断

随着 eBPF、Landlock 等新技术融入内核,Namespace 与 Cgroups 的技术栈正在进一步扩展。例如 Cilium 通过 eBPF 在 Network Namespace 之上构建了零信任网络策略,而 runc 也在探索基于 cgroup v2 的 rootless 容器安全加固方案。理解这些基石,是掌握云原生运维的必经之路。

参考资源

  • Linux 内核源码:kernel/nsproxy.c、kernel/cgroup/
  • man pages:clone(2)、unshare(2)、cgroups(7)
  • Kernel documentation:Documentation/admin-guide/cgroup-v2.rst
  • The Linux Programming Interface(Michael Kerrisk)
  • systemd.service(5) 中的资源控制文档
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ .skip-link { position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } .skip-link:focus { top: 0; outline: 3px solid #0056b3; }