LabHub
学习 学习路径 课程

容器内部原理

七种命名空间与容器的真身

在 LabHub 中继续学习

一句话总结

内核源码中不存在 struct container 之类的东西。容器只是人们为这样一种状态起的 名字:把命名空间 + cgroup + 安全层 + rootfs组合到一个进程上。其中, 命名空间只负责回答一个问题——这个进程能够看到什么。

概念图: 命名空间 + cgroup + 安全层 + rootfs · 展示某些资源清单的另一个版本 · 发布到主机的那一刻 · 编号相同,就属于同一个命名空间。

为什么需要它

“创建容器”这种说法,让很多人以为内核某处真的有一个叫作容器的盒子。 然而在主机上运行 ps,会原样看到容器里的进程,内核版本也与主机完全相同。 如果真有盒子,就不该如此。实际情况恰恰相反:进程从一开始就普通地存在于主机上, 内核只是向这个进程展示某些资源清单的另一个版本。这不是隔绝,而是限制视野。

这种区别在实际工作中会立刻显现:容器内的 nproc 显示 32,实际上却只能用 0.5 个核; 两个容器各自监听 80 端口;容器内删除的文件仍留在主机上。要预测这些现象, 都必须知道“什么被分隔了,什么没有被分隔”。

它如何运作

命名空间共有 7 类。

类型 被分隔的内容
pid 进程号空间。成为 1 号进程后,还要负责处理信号和回收僵尸进程
net 接口、路由表、iptables 规则、套接字以及端口号空间的全部内容
mnt 挂载表。镜像 rootfs 之所以显示为 / 的原因
uts hostname
ipc System V IPC、POSIX 消息队列
user UID/GID 映射。rootless 容器的核心
cgroup 让自己的 cgroup 位置看起来像根目录

重要的一点是,net 连端口号空间也会完整分隔。因此两个容器各自监听 80 端口也不会冲突。 端口冲突并不发生在容器之间,而发生在发布到主机的那一刻

每个命名空间都有 inode 编号,对 /proc/<pid>/ns/<종류> 执行 readlink 就会显示该编号。 **编号相同,就属于同一个命名空间。**实际测量会看到如下结果。

호스트   pid:[4026531836] net:[4026531840] user:[4026531837] time:[4026531834]
alpine   pid:[4026532500] net:[4026532562] user:[4026531837] time:[4026531834]

只有 user 和 time 相同。这是因为 Docker 默认不使用用户命名空间。 因此容器内的 root 与主机 root 同为 UID 0,而这正是下一门课程将讨论的安全问题之根源。

在实际工作中会遇到的情况

Kubernetes Pod 原样采用了这种结构。Pod 会先启动一个约 1MB、名为 pause 的容器, 由它持有 net/ipc/uts 命名空间。应用容器不会新建这些命名空间,而是加入其中, 仅让 mnt 和 cgroup 按容器分别拥有。因此同一 Pod 内的容器通过 localhost 通信并共享端口, 文件系统却各不相同。

在 Docker 中也能直接构造这种结构。--network container:<이름> 这条命令的含义正是 “加入别人的 net 命名空间”。只用 Docker 复现 sidecar 模式时,就会使用它。

命名空间何时消失

命名空间不是对象,而是只在仍有引用时存在的东西。其中最后一个进程结束, 并且再也没有任何东西持有它时,内核便会回收它。理解这种性质后, 容器运维中一些看似奇怪的现象就能得到解释。

**Pod 已消失,网络设置却仍存在。**这表示某处仍持有该命名空间。 持有方式有三种:在其中运行的进程、保持打开 /proc/<PID>/ns/<종류> 的文件描述符, 以及挂在该路径上的绑定挂载。最后一种正是 ip netns 使用的方式, 因此即使所有进程都已结束,只要名字仍然存在,命名空间也会继续存在。

**容器已经删除,名字却一直保留。**原因相同;在实际工作中,通常是清理流程没有卸载该绑定挂载。

人们也会反过来利用这种性质:提前创建一个没有任何进程的命名空间, 之后再让进程加入。前面看到的 pause 容器做的正是这件事。即使应用容器死掉后重新启动, **网络命名空间仍保持存活,所以 IP 不会改变。**Pod IP 能经受容器重启,原因就在这里。

还要明确一点:命名空间不会划分资源。它只决定能看到什么,能用多少则由 cgroup 决定。 因此,仅靠命名空间无法阻止一个容器耗尽 CPU。容器之所以看起来相互隔离, 是因为两者同时生效;只应用其中一个,另一半仍会原样泄漏。

后续实验要做什么

亲自比较主机与容器的命名空间 inode,确认谁是 PID 1,并逐一检查 hostname、 文件树和接口列表分别因哪个命名空间而不同。最后让两个容器共享 net 命名空间, 亲手复现 Pod 的结构。