跳到内容

14.2 Namespace、cgroup 与容器隔离

地心叠屋里有两种“分身”。虚拟机让住户以为自己拥有一套硬件和内核;容器没有复制地基,它仍让 Host 内核调度普通进程,只是改变这些进程能看见什么、能用多少资源。

Namespace 提供隔离视图,cgroup 组织、计量和限制资源。image、root filesystem、capability、seccomp、network 与生命周期管理再补齐“容器”这一产品抽象。先保留一句不会误导人的区分:

VM 给工作负载一套虚拟硬件和独立 Guest 内核;Linux 容器给进程一组隔离视图与资源边界。

1. Namespace 改变“看见什么”

Linux 当前常见的 namespace 类型包括:

类型clone/unshare 标志隔离的主要视图
MountCLONE_NEWNS挂载点
PIDCLONE_NEWPID进程编号
NetworkCLONE_NEWNET网络设备、协议栈、端口和路由
UTSCLONE_NEWUTShostname 与 NIS domain name
IPCCLONE_NEWIPCSystem V IPC 与 POSIX 消息队列
UserCLONE_NEWUSERUID、GID 与 capabilities
CgroupCLONE_NEWCGROUPcgroup 路径视图
TimeCLONE_NEWTIME部分系统时钟

namespace 是可组合的。只创建 PID namespace,不会自动隔离网络、挂载点或用户身份。一个容器 runtime 必须明确创建哪些 namespace,以及哪些资源有意与 Host 或其他容器共享。

先观察当前进程

/proc/PID/ns 暴露进程所属 namespace 的句柄:

bash
ls -l /proc/self/ns
readlink /proc/self/ns/pid
readlink /proc/1/ns/pid

若两个进程对应类型的链接显示同一个 namespace 标识,它们属于同一个 namespace 实例。打开这些特殊文件得到的文件描述符还能保持 namespace 存活,并供 setns 加入。

clone 可以在创建子进程时一并创建 namespace,unshare 让调用进程进入新的 namespace,setns 则加入已有 namespace。权限判断与 user namespace 紧密相关,不能假设任意普通进程都能创建所有类型。

2. PID namespace 不只是换编号

同一个进程在嵌套 PID namespace 中可以有不同 PID。内层看到的 PID 1,在父 namespace 里可能是 18342:

text
Host PID namespace:       container init = 18342
Container PID namespace:  container init = 1

PID 1 还有特殊职责。它应回收孤儿子进程,并正确转发或处理终止信号。业务程序若直接成为容器 PID 1,却从不 wait 子进程,容器里会积累 zombie;若忽略信号语义,停止和滚动发布也会变得迟缓。

PID namespace 只改变编号与可见关系。它不会阻止同一 Host 内核漏洞跨越边界,也不会自动限制可创建进程数;进程数限制应使用 pids controller 等机制。

3. Mount namespace 与根文件系统

Mount namespace 给一组进程独立的挂载点视图。容器 runtime 通常还会:

  • 准备镜像展开后的 root filesystem;
  • 设置挂载传播属性;
  • 挂载 /proc、必要设备与临时文件系统;
  • 使用 pivot_root 或等价机制切换根;
  • 按配置加入只读层、volume 和 bind mount;
  • 避免把 Host 敏感路径意外暴露进去。

chroot 只改变路径解析的根,不是完整安全边界。没有 mount/user namespace、能力削减和其他防护时,特权进程仍可能影响更广的系统。

容器镜像的分层文件系统通常让多个容器共享只读层,并为每个容器增加可写层。copy-on-write 节省空间,却意味着数据库等写密集负载通常应把持久数据放在语义清楚的 volume 中,而不是依赖短生命周期可写层。

4. Network namespace 形成独立协议栈视图

新的 network namespace 开始时可以只有 loopback。runtime 常用 veth pair 把它连接到 Host:

text
container eth0

    veth pair

Host bridge / routing / policy

physical network

network namespace 隔离接口、地址、路由、端口等,但流量最终仍经过 Host 网络路径。防火墙、eBPF、service proxy、NAT 和 CNI 插件可能继续处理数据包。

“容器有独立 IP”不代表存在物理网卡,也不表示网络策略自动安全。共享 Host network namespace 的容器甚至不再拥有独立端口空间。

5. User namespace 改变身份映射

User namespace 允许进程在 namespace 内看到 UID 0,而在父 namespace 中仍映射到普通非特权 UID:

text
inside user namespace:  uid 0
             maps to
outside on Host:        uid 100000

capability 的效力相对于 user namespace 判断。“容器内 root”因此不一定等于 Host root,但配置错误的映射、暴露设备或内核漏洞仍可能造成越界。

rootless 容器利用 user namespace 降低 runtime 与容器进程的 Host 权限,是重要的纵深防御。它也会受到端口、设备、文件所有权和某些挂载操作的限制。

6. cgroup 防止一层住户占满整台引擎

Namespace 让住户看到不同门牌,却不会限制他们消耗 CPU 和 memory。若某层 workload 不停申请资源,仍可能拖慢整座叠屋。cgroup 负责另一半边界:组织、计量和限制。

cgroup 把进程组织成层级,并由 controller 提供资源控制与统计。cgroup namespace 只虚拟化进程看到的 cgroup 路径,不负责实际资源限制;两者不要混淆。

现代 Linux 常使用 cgroup v2 统一层级。几个关键接口是:

文件含义
cpu.max带宽配额与周期,默认 max
memory.high内存压力阈值,触发节流和强回收
memory.max主要硬上限,无法回收时可触发该 cgroup 内 OOM
pids.max任务数量上限
io.max按设备限制带宽或 IOPS
cgroup.procscgroup 中的进程成员

cpu.max 的格式是:

text
$MAX $PERIOD

例如 200000 100000 表示这个 cgroup 在每 100000 微秒周期内最多获得 200000 微秒 CPU 时间。它允许并行消耗这份预算,不是把任务固定到两颗指定核心。CPU affinity 是另一套机制。

memory.high 适合让工作负载在硬失败前先承受回收压力;memory.max 是主要硬边界。当使用量达到 memory.max 且无法降低时,内核可在该 cgroup 内触发 OOM。短暂越界和某些特殊分配仍有例外,监控不应只检查进程是否被立即杀死。

7. 资源上限不等于性能保证

给容器设置“2 CPU、1 GiB”仍有许多未说明事项:

  • CPU 是 quota、weight 还是 affinity?
  • 同一 Host 的其他任务是否争用共享缓存、内存带宽和 I/O?
  • 内存上限是否包含 page cache 与 socket memory?
  • 是否允许 swap?
  • I/O 限制作用于哪个真实设备?
  • NUMA 放置与 CPU 集合是否匹配?

CPU quota 可以防止一个租户长期占满整机,但在短周期突发下也可能造成 throttling 和尾延迟尖峰。memory limit 能限制用量,却不能承诺申请一定成功。SLO 需要同时观察 cgroup 指标、Host 压力和应用队列。

8. 容器安全是多层约束

namespace 不是完整安全沙箱。更稳妥的配置会组合:

  • 非 root 用户或 rootless 模式;
  • 删除不需要的 Linux capabilities;
  • no_new_privs
  • seccomp 限制系统调用面;
  • SELinux、AppArmor 等 LSM 策略;
  • 只读根文件系统和最小化挂载;
  • 不暴露 Host PID/network namespace;
  • 不挂载容器 runtime socket;
  • 镜像来源、签名、扫描与最小依赖;
  • 及时修复共享 Host 内核。

把 Docker 或其他 runtime 的管理 socket 挂进容器,通常等于授予它创建高权限工作负载的能力。此时即使容器进程本身权限看似有限,管理接口也扩大了边界。

安全选择不能只比较“容器逃逸”和“VM 逃逸”哪个更难。威胁模型还包括控制面、镜像供应链、虚拟设备、共享硬件与凭据。

9. 从镜像到进程

一次容器启动可以概括为:

text
image manifest + layers + runtime config

准备 rootfs 与挂载

创建 namespace / cgroup

设置 UID、capabilities、seccomp、LSM

执行容器入口进程

镜像不是运行中的容器,容器也不是长期保存数据的天然边界。镜像描述只读内容与配置;运行时再添加可写层、网络、volume、secret 和资源限制。

高层编排系统还要处理重启、探针、滚动发布、服务发现和调度。namespace 与 cgroup 提供机制,不替应用决定健康状态和数据恢复。

10. 常见误解

“容器里看到 PID 1,就是一台独立机器。” 这只是 PID namespace 的视图;内核仍由 Host 共享。

“namespace 负责 CPU 和内存上限。” 上限主要由 cgroup controller 提供。

“cgroup namespace 会创建新的资源配额。” 它改变的是 cgroup 路径视图。

“容器镜像包含内核。” 普通 Linux 容器镜像提供用户空间,系统调用由 Host 内核实现。

“设置 memory.max 后不会影响 Host。” 容器仍共享内核与物理内存系统,回收和 I/O 争用可能波及整机。

“rootless 就不需要其他防护。” 它降低 Host 权限,但不消除内核、配置和供应链风险。

11. 小结

Linux 容器是多种内核机制的组合:

  • namespace 分别隔离 PID、挂载、网络、身份等视图;
  • cgroup 组织、计量并控制 CPU、内存、I/O 与任务数;
  • rootfs 与镜像提供用户空间文件;
  • capabilities、seccomp 和 LSM 缩小共享内核的攻击面;
  • runtime 和编排层管理创建、停止、网络与持久数据。

下一节通过最小 KVM 实验回到虚拟机边界。若只关心系统原理,可以直接前往第15章:CPU 流水线

参考

Built with VitePress | Software Systems Atlas