跳到内容

第 3 卷 / 第 14 章 / 第 2 课

14.2 Namespace、cgroup 与容器隔离 ​

虚拟机向 Guest 提供虚拟硬件并运行独立内核;容器则由 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_NEWTIMEMONOTONIC/BOOTTIME 等时钟,非 REALTIME

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 与 setns 的效果取决于类型。尤其对 PID namespace,它们只改变后续子进程进入的 namespace,调用者本身的 PID namespace 不变。unshare 创建 time 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 还有特殊职责。它应等待并回收已退出的子进程,包括被它收养的孤儿,并按进程树需要处理或转发终止信号。中间 subreaper 也可能先收养孤儿;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 不停申请资源,仍可能拖慢整台 Host。cgroup 负责另一半边界:组织、计量和限制。

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

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

文件含义
cpu.max公平调度类的带宽配额与周期,默认 max 100000
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 是另一套机制。接口是否出现取决于挂载层级和 controller 是否启用;祖先配额仍可限制子组,默认 max 也不表示逃过祖先限制。

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