15怪兽:底层原理

第 15 关 底层原理
本关的产出:把前十四关一直在用、却从没解释过的东西——namespace、cgroups、veth、iptables、overlayfs——各自在哪一层、干什么,一次看清。
15.1 容器不是一台小机器
先把最容易误导的直觉拆掉:容器不是虚拟机。
- 虚拟机:有自己的内核,硬件是模拟出来的。
- 容器:没有自己的内核,就是宿主机上的一组普通进程,加上内核给它套的几层「视图」和「限额」。
所以「容器」不是一个东西,是几样内核机制合起来的效果:
| 机制 | 管什么 | 一句话 |
|---|---|---|
| namespace | 看得见什么 | 给它一套独立的视图 |
| cgroups | 用得了多少 | 给它记账、限额 |
| overlayfs | 文件系统长什么样 | 把几个目录合成一个根目录 |
| veth + bridge + iptables | 网络怎么通 | 给它一套虚拟网卡和转发规则 |
这一关把这四行展开。第 16 关会亲手拼一个迷你容器。
15.2 namespace:看不见
namespace 的作用是:让一组进程看到一套「自己的」系统资源。
同样是 PID,容器里看是 1、2、3;宿主机上,它们只是几千几万号里的一段。同一台机器,两套视图。
Linux 有几种 namespace,各管一类资源:
| namespace | 隔离什么 | 对应前面哪一关 |
|---|---|---|
pid | 进程号 | 第 8 关的「1 号进程」 |
net | 网卡、路由、端口 | 第 5 关的网络 |
mnt | 挂载点 | 第 4 关的卷 |
uts | 主机名 | --hostname |
ipc | 共享内存、信号量 | — |
user | 用户 / 组 ID 的映射 | 权限隔离(本系列没展开) |
cgroup | cgroup 视图 | — |
第 8 关的「PID 1」就是 pid namespace 的直接结果:容器里第一个进程拿到 1 号,而不是它在宿主机上的真实号。
观察:
docker exec <容器> ps aux只看到容器里的进程——宿主机上另外几百个,它一个都看不见。
在 Linux 宿主机上,能看到每个进程属于哪些 namespace:
lsns关键:多数 namespace 换的是视图,不是权限——它没有「造一台机器」,只是换了一套编号。内核还是同一个。
15.3 cgroups:用不超
namespace 管「看得见什么」,cgroups 管「能用多少」。第 14 关那些参数,底下就是它:
| 你写的 | cgroup 里落成 |
|---|---|
--cpus=1.5 | cpu.max(配额 / 周期) |
--memory=512m | memory.max |
--pids-limit=100 | pids.max |
cgroups 做的事就两件:记账(这一组用了多少)和限额(超过就怎样)。CPU 超了被限流;内存超了触发 OOM killer——就是第 12 关那个 137 和 OOMKilled。
观察(Linux,cgroup v2):
docker run -d --name demo --memory=256m nginxdocker exec demo cat /sys/fs/cgroup/memory.max打出来是 268435456——256 MiB。容器里的 /sys/fs/cgroup 就是它自己那个 cgroup(cgroup namespace 让它看起来像整个根),所以读的是这条容器自己的限额:
- 设了
--memory,就是那个值; - 没设,就是
max(无上限)。
换一个容器读,读到的是它自己的——各是各的。宿主机那边,每个容器在 /sys/fs/cgroup 下各有一个目录。
要分清:cgroups 只管资源,不管隔离。它不会让容器看不见别的进程——那是 namespace 的事。
15.4 网络:一根线、一台交换机、几条规则
第 5 关讲的是「怎么用」,这里讲「底下是什么」。
- veth pair:一对虚拟网卡,天生连在一起——从一头进去的包,从另一头出来。Docker 给每个容器插一根:一头是容器里的
eth0,另一头在宿主机上(名字像veth3f2a...)。 - 网桥(bridge):宿主机上一台虚拟交换机。veth 的另一头插在它上面。默认网络那台叫
docker0,自己建的网各有一台。 - 同一张网 = 同一台交换机:两个容器接在同一台网桥上,二层就通了——这是第 5 关「自定义网络里能互相叫到」的物理基础(名字解析则是内建 DNS 的事)。
- 出网:宿主机做 NAT,把容器发出的包换成宿主机的地址送出去。
- 端口发布
-p:Linux 上落成iptables的 DNAT 规则——「外部打到宿主机 8080 的包,改写成容器 IP<80>80>」。
第 5 关那个 macOS 的现象,现在能对照了:macOS 的内核不是 Linux,没有这些规则。Docker Desktop 的办法是——由它自己的进程在 Mac 上监听端口,再把流量转发进虚拟机。所以 lsof 看到的是 com.docker.backend,而不是内核规则。同一套操作,两个平台两套实现。
观察:这些命令要跑在运行 Docker 引擎的那台 Linux 机器上。
- Linux 宿主机:直接在宿主机上跑(要 root)。
- macOS / Windows:在 Docker Desktop 的虚拟机里,不在你的系统上——你那边没有
docker0,也没有这些iptables规则。
然后在里面:
ip link showiptables -t nat -L DOCKER前者能看到 docker0 和一堆 veth*,后者能看到端口转发的规则。
15.5 存储:overlayfs 把几棵目录树合成一个根
第 3 关说过「容器看到的 / 是拼出来的」。拼法就是 overlayfs。
先说清楚「层」是什么:一层就是一棵目录树——某一步对文件系统的改动,最后留下的那些文件和目录。镜像不是一整块,它是好几个这样的目录(docker history 一条条列着),每条构建指令各留下自己那一棵叠在一块的。比如 RUN apt-get install -y nginx 那一层里,就是 /usr/sbin/nginx、/etc/nginx/... 和它依赖的那些库。
overlayfs 把这些目录按顺序挂成一个,再在最上面加一棵可写的:
- lower:镜像的各个目录树,只读;
- upper:容器自己的目录树,可写;
- merged:挂出来的视图——容器的
/就是它。
merged 不是第三份拷贝,只是一个挂载点:读的时候把 lower 和 upper 合着看,写落在 upper。
读:从最上面往下找,找到哪棵算哪棵;上面的同名路径盖住下面的。 写:copy-up——文件只在下面的只读树里时,先复制到 upper 再改,所以第一次改大文件会慢一下。 删:在 upper 里放一个 whiteout 标记,把下面那个盖住——实物还在。
实物在哪:全在跑引擎那台机器的磁盘上(注意这些对容器内不可见,只有引擎机器能看到)——
# 每个diff各自是一棵树/var/lib/docker/overlay2/<层id>/diff # 镜像的每一层各占一个,只读/var/lib/docker/overlay2/<容器层id>/diff # 容器的可写层,只有一个/var/lib/docker/overlay2/<容器层id>/merged # 合并后的挂载点macOS / Windows 上,这块磁盘就是 Docker 的虚拟机——第 4 关说卷「在虚拟机里」,说的正是它落在这块磁盘上。
第 9 关那条「清理必须和安装同一条 RUN」,机制就在这儿:跨层删除只是盖标记,实物仍然留在下面那层;一条构建命令在同一层里建了又删,才能真正删除文件。
第 4 关的卷:卷不在这套合成里。它是一个 bind mount,直接挂在容器某个路径上——读写不经过 overlayfs 的 copy-up,数据直接落在磁盘上。
关于「一条指令一层」:第 3 关那个模型是经典构建器的。现在的 BuildKit 产出的快照结构不完全一样,但「层能复用、上层盖住下层、删除只是标记」这几条不变——那个简写仍然是对的起点。
15.6 串起来:一次 docker run 发生了什么
把上面的东西按顺序走一遍:
- 找到镜像(本地没有就拉)。
- 用 overlayfs 把镜像各层加一个新的可写层,拼成根目录。
- 建一组 namespace(pid、net、mnt、uts、ipc……)。
- 建一个 cgroup,把第 14 关那些限额写进去。
- 建一对 veth,一头放进容器(
eth0),一头插上网桥。 - 把卷用 bind mount 挂到指定路径。
- 在 namespace 里 exec 入口程序——它就是容器里的 1 号进程(第 8 关)。
所谓「启动容器」,就是做上面这一串,然后让一个进程跑起来。没有开机,没有内核。
15.7 边界:这些机制不是安全边界
第 13 关说过:容器共享宿主机内核。这一关能看清为什么这句话要紧——
- namespace 换的是视图,不是权限(
usernamespace 是例外:它连「你是谁」一起换,所以能当权限边界用); - cgroups 限的是用量,不是行为;
- 一旦内核有漏洞,这些「视图」就可能被看穿、被绕过。
真正加锁的是另外几样,同样是内核机制:capabilities、seccomp(第 13 关)、user namespace。它们和这一关讲的是同一层东西——都是内核在管,不是 Docker 在管。
这一关的产出:知道容器是「一组被内核缩小了视图和配额的普通进程」,也能把前面每一关的现象对上它的机制。下一关不再看,亲手拼一个——用 unshare、chroot、mount 把 namespace 和文件系统搭起来。
本关命令速查
资源与网络的 Docker 侧参数,见 命令速查表 · 资源限制、命令速查表 · 网络。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





