14.2 Namespace、cgroup 与容器隔离
虚拟机向 Guest 提供虚拟硬件并运行独立内核;容器则由 Host 内核调度普通进程,通过隔离视图和资源控制来划定边界。
Namespace 提供隔离视图,cgroup 组织、计量和限制资源。image、root filesystem、capability、seccomp、network 与生命周期管理再补齐“容器”这一产品抽象。先保留一句不会误导人的区分:
VM 给工作负载一套虚拟硬件和独立 Guest 内核;Linux 容器给进程一组隔离视图与资源边界。
1. Namespace 改变“看见什么”
Linux 当前常见的 namespace 类型包括:
| 类型 | clone/unshare 标志 | 隔离的主要视图 |
|---|---|---|
| Mount | CLONE_NEWNS | 挂载点 |
| PID | CLONE_NEWPID | 进程编号 |
| Network | CLONE_NEWNET | 网络设备、协议栈、端口和路由 |
| UTS | CLONE_NEWUTS | hostname 与 NIS domain name |
| IPC | CLONE_NEWIPC | System V IPC 与 POSIX 消息队列 |
| User | CLONE_NEWUSER | UID、GID 与 capabilities |
| Cgroup | CLONE_NEWCGROUP | cgroup 路径视图 |
| Time | CLONE_NEWTIME | MONOTONIC/BOOTTIME 等时钟,非 REALTIME |
namespace 是可组合的。只创建 PID namespace,不会自动隔离网络、挂载点或用户身份。一个容器 runtime 必须明确创建哪些 namespace,以及哪些资源有意与 Host 或其他容器共享。
先观察当前进程
/proc/PID/ns 暴露进程所属 namespace 的句柄:
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:
Host PID namespace: container init = 18342
Container PID namespace: container init = 1PID 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:
container eth0
│
veth pair
│
Host bridge / routing / policy
│
physical networknetwork namespace 隔离接口、地址、路由、端口等,但流量最终仍经过 Host 网络路径。防火墙、eBPF、service proxy、NAT 和 CNI 插件可能继续处理数据包。
“容器有独立 IP”不代表存在物理网卡,也不表示网络策略自动安全。共享 Host network namespace 的容器甚至不再拥有独立端口空间。
5. User namespace 改变身份映射
User namespace 允许进程在 namespace 内看到 UID 0,而在父 namespace 中仍映射到普通非特权 UID:
inside user namespace: uid 0
maps to
outside on Host: uid 100000capability 的效力相对于 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.procs | cgroup 中的进程成员 |
cpu.max 的格式是:
$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. 从镜像到进程
一次容器启动可以概括为:
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 流水线。