15怪兽:底层原理

2259 字
11 分钟
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 的映射权限隔离(本系列没展开)
cgroupcgroup 视图—

第 8 关的「PID 1」就是 pid namespace 的直接结果:容器里第一个进程拿到 1 号,而不是它在宿主机上的真实号。

观察:

docker exec <容器> ps aux

只看到容器里的进程——宿主机上另外几百个,它一个都看不见。

在 Linux 宿主机上,能看到每个进程属于哪些 namespace:

lsns

关键:多数 namespace 换的是视图,不是权限——它没有「造一台机器」,只是换了一套编号。内核还是同一个。

15.3 cgroups:用不超#

namespace 管「看得见什么」,cgroups 管「能用多少」。第 14 关那些参数,底下就是它:

你写的cgroup 里落成
--cpus=1.5cpu.max(配额 / 周期)
--memory=512mmemory.max
--pids-limit=100pids.max

cgroups 做的事就两件:记账(这一组用了多少)和限额(超过就怎样)。CPU 超了被限流;内存超了触发 OOM killer——就是第 12 关那个 137 和 OOMKilled。

观察(Linux,cgroup v2):

docker run -d --name demo --memory=256m nginx
docker 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 关讲的是「怎么用」,这里讲「底下是什么」。

  1. veth pair:一对虚拟网卡,天生连在一起——从一头进去的包,从另一头出来。Docker 给每个容器插一根:一头是容器里的 eth0,另一头在宿主机上(名字像 veth3f2a...)。
  2. 网桥(bridge):宿主机上一台虚拟交换机。veth 的另一头插在它上面。默认网络那台叫 docker0,自己建的网各有一台。
  3. 同一张网 = 同一台交换机:两个容器接在同一台网桥上,二层就通了——这是第 5 关「自定义网络里能互相叫到」的物理基础(名字解析则是内建 DNS 的事)。
  4. 出网:宿主机做 NAT,把容器发出的包换成宿主机的地址送出去。
  5. 端口发布 -p:Linux 上落成 iptables 的 DNAT 规则——「外部打到宿主机 8080 的包,改写成容器 IP<80>」。

第 5 关那个 macOS 的现象,现在能对照了:macOS 的内核不是 Linux,没有这些规则。Docker Desktop 的办法是——由它自己的进程在 Mac 上监听端口,再把流量转发进虚拟机。所以 lsof 看到的是 com.docker.backend,而不是内核规则。同一套操作,两个平台两套实现。

观察:这些命令要跑在运行 Docker 引擎的那台 Linux 机器上。

  • Linux 宿主机:直接在宿主机上跑(要 root)。
  • macOS / Windows:在 Docker Desktop 的虚拟机里,不在你的系统上——你那边没有 docker0,也没有这些 iptables 规则。

然后在里面:

ip link show
iptables -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 发生了什么#

把上面的东西按顺序走一遍:

  1. 找到镜像(本地没有就拉)。
  2. 用 overlayfs 把镜像各层加一个新的可写层,拼成根目录。
  3. 建一组 namespace(pid、net、mnt、uts、ipc……)。
  4. 建一个 cgroup,把第 14 关那些限额写进去。
  5. 建一对 veth,一头放进容器(eth0),一头插上网桥。
  6. 把卷用 bind mount 挂到指定路径。
  7. 在 namespace 里 exec 入口程序——它就是容器里的 1 号进程(第 8 关)。

所谓「启动容器」,就是做上面这一串,然后让一个进程跑起来。没有开机,没有内核。

15.7 边界:这些机制不是安全边界#

第 13 关说过:容器共享宿主机内核。这一关能看清为什么这句话要紧——

  • namespace 换的是视图,不是权限(user namespace 是例外:它连「你是谁」一起换,所以能当权限边界用);
  • cgroups 限的是用量,不是行为;
  • 一旦内核有漏洞,这些「视图」就可能被看穿、被绕过。

真正加锁的是另外几样,同样是内核机制:capabilities、seccomp(第 13 关)、user namespace。它们和这一关讲的是同一层东西——都是内核在管,不是 Docker 在管。

这一关的产出:知道容器是「一组被内核缩小了视图和配额的普通进程」,也能把前面每一关的现象对上它的机制。下一关不再看,亲手拼一个——用 unshare、chroot、mount 把 namespace 和文件系统搭起来。

本关命令速查#

资源与网络的 Docker 侧参数,见 命令速查表 · 资源限制、命令速查表 · 网络。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
15怪兽:底层原理
https://hecloud.top/posts/tech/docker/15底层原理/
作者
贺小云
发布于
2026-10-31
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
贺小云
热爱技术与折腾的博客 Dancing with uncertainty. 与不确定性共舞
公告
出于成本考虑,贺云已全面转型Serverless架构
分类
标签
碎碎念
站点统计
文章
95
分类
5
标签
44
总字数
110,484
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Netlify CI
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0