第 5 章 · 01 容器基础与镜像分层 容器到底是什么?为什么它比虚拟机轻一个数量级?答案藏在两个内核机制和一个聪明的存储模型里:namespaces 隔离视野、cgroups 限制资源、镜像分层让"一个镜像千个容器"成为可能。本节先把容器世界观建起来,再拆开镜像这只"千层蛋糕"。 学习目标 说清容器与虚拟机的本质区别及各自的适用场景 理解镜像 = 只读层集合、容器 = 镜像 + 可写容器层的模型 知道 digest、悬空镜像、多架构镜像的含义与用途 掌握镜像构建流程与缓存失效规则 分清 namespaces(隔离视野)与 cgroups(限制资源)的分工 一、容器 vs 虚拟机 容器是为执行进程提供"可配置隔离 + 资源限制"的环境。
容器到底是什么?为什么它比虚拟机轻一个数量级?答案藏在两个内核机制和一个聪明的存储模型里:namespaces 隔离视野、cgroups 限制资源、镜像分层让"一个镜像千个容器"成为可能。本节先把容器世界观建起来,再拆开镜像这只"千层蛋糕"。
容器是为执行进程提供"可配置隔离 + 资源限制"的环境。它的实现不依赖任何专有技术——Linux 内核自带的 namespaces 与 cgroups 就足够了,因此创建容器的方式也不止 Docker 一种(systemd-nspawn、LXC 都是)。
容器与虚拟机共享"打包应用运行环境"的目标,但走的是两条完全不同的路线:
| 维度 | 容器 | 虚拟机 |
|---|---|---|
| 虚拟化层级 | 操作系统级虚拟化 | 硬件级虚拟化 |
| 操作系统 | 不含 Guest OS,共享宿主内核 | 每个 VM 有完整 Guest OS |
| 隔离机制 | namespaces + cgroups | Hypervisor 硬件隔离 |
| 启动时间 | 秒级 | 分钟级(需引导整个 OS) |
| 隔离强度 | 较弱(共享内核) | 强(完全隔离) |
| 可移植性 | 高(镜像即环境) | 相对受限 |
选择的标准也因此清晰:需要完整 OS 能力、强隔离与安全 → 虚拟机;需要轻量、快速部署、同一应用的多个版本/实例共存 → 容器。生产中两者常组合使用——虚拟机提供硬件隔离的底座,容器在其上提供应用级的快速交付。
容器化的完整流程只有四步:编写 Dockerfile(应用 + 运行命令 + 依赖)→ 构建镜像 →(可选)推送到镜像仓库 → 用镜像运行容器。镜像在流程中处于枢纽位置,它也是本节接下来解剖的主角。
镜像(image)包含应用、其依赖以及运行应用所需的操作系统文件。它不是一个大文件,而是一组只读层的松散集合:每一层对应 Dockerfile 的一条指令,每层只是相对于前一层的一组差异(diff),层层堆叠。
这个模型带来三个重要性质:
层是内容寻址的。每层拥有基于内容的哈希 ID,层与镜像不可变——任一层的内容变化,哈希就变化,改动因此极易被识别。
层是共享的。两个镜像如果基于同一个基础镜像构建,它们的底层完全相同。拉取第二个镜像时输出 "already exists",正是层共享的证据:相同的层无需重复下载。这也是为什么多个镜像共存时磁盘开销远小于"各自完整复制"。
容器只加一层可写层。用镜像启动容器时,引擎在只读层之上叠加一个薄薄的可写层——容器层。对运行容器的所有写操作(新建、修改、删除文件)都落在这层:
由此引出两个概念。digest(摘要):tag 是可变的(同一 tag 可指向不同内容的镜像),而 digest 是内容寻址标识符——不可变、可预测,判断"两个镜像是否真的一致"只能靠它。悬空镜像(dangling image):没有任何 tag 挂着的镜像,典型产生方式是用完全相同的名称+tag 重新构建;它仍可通过完整 SHA 引用,通常用一条清理命令批量回收。
镜像还有一个常被忽略的能力:一个镜像可支持多种架构(Linux x64、Windows x64……)。拉取多架构镜像时,客户端向仓库请求 manifest list,检查自己的架构是否被支持,再按对应架构的 manifest 逐层拉取——这是"一次构建、处处运行"的底层保障。
docker image build 的每个指令背后是一套"临时容器"机制:
缓存机制让重复构建快如闪电:后续构建时,引擎在缓存中查找"相同基础镜像 + 相同指令"构建出的层——找到就跳过该指令、直接链接已有层;找不到就重新构建,且从此处起缓存全部失效。两个细节值得注意:FROM 指令先检查基础镜像是否在本地,不在才拉取;COPY/ADD 指令即使自身没变,被复制文件的 checksum 变了同样失效。因此最佳实践是"把变化最少的指令放在 Dockerfile 前面",让缓存命中率最大化。
容器"轻"的秘密在于它只借用了宿主内核的两个能力:
namespaces(命名空间)管"能看到什么"——隔离系统资源视图。Linux 提供七类:PID(进程号)、Mount(挂载点)、Network(路由表/接口/ARP 表)、UTS(主机名)、IPC(进程间通信)、User(用户与组 ID)、Time(时间)。容器里的进程以为自己是 PID 1,看到的网络栈、挂载点都是独立副本。
cgroups(控制组)管"能用多少"——限制一组进程(含子进程)可消耗的资源量:CPU、内存、块 I/O、网络。防止某个容器吃光宿主资源拖垮邻居。
一句话总结:cgroups 限制你能用多少,namespaces 限制你能看到什么。两者合起来构成了 OCI 标准所说的"容器"。
镜像之所以远小于虚拟机磁盘,有两个原因:大多数镜像不含内核——容器共享并使用宿主内核,镜像只需应用 + 依赖 + 少量系统文件;镜像只包含运行特定应用所需的内容,不装用不到的东西。这也预告了第 3 节要讲的优化手段:减少指令数、选更小的基础镜像、构建后清理、多阶段构建。
本节建立了容器世界观:容器 = 共享内核的 OS 级虚拟化;镜像 = 只读层堆叠 + 可写容器层 = 运行实例;层共享与构建缓存让镜像的存储与构建都高效;namespaces 与 cgroups 分别回答"看得到什么"和"用得了多少"。镜像作为"可执行环境的模板",下一步的问题是:模板怎么写?——这就是 Dockerfile 的舞台。
第 5 章第 2 节《镜像构建与容器生命周期》将逐条讲解 Dockerfile 指令,重点拆解 RUN/CMD/ENTRYPOINT 三兄弟的分工与 Exec/Shell 两种写法的坑,最后走一遍容器从 run 到 stop 的完整生命周期。