第 5 章 · 01 容器基础与镜像分层


文档摘要

第 5 章 · 01 容器基础与镜像分层 容器到底是什么?为什么它比虚拟机轻一个数量级?答案藏在两个内核机制和一个聪明的存储模型里:namespaces 隔离视野、cgroups 限制资源、镜像分层让"一个镜像千个容器"成为可能。本节先把容器世界观建起来,再拆开镜像这只"千层蛋糕"。 学习目标 说清容器与虚拟机的本质区别及各自的适用场景 理解镜像 = 只读层集合、容器 = 镜像 + 可写容器层的模型 知道 digest、悬空镜像、多架构镜像的含义与用途 掌握镜像构建流程与缓存失效规则 分清 namespaces(隔离视野)与 cgroups(限制资源)的分工 一、容器 vs 虚拟机 容器是为执行进程提供"可配置隔离 + 资源限制"的环境。

第 5 章 · 01 容器基础与镜像分层

容器到底是什么?为什么它比虚拟机轻一个数量级?答案藏在两个内核机制和一个聪明的存储模型里:namespaces 隔离视野、cgroups 限制资源、镜像分层让"一个镜像千个容器"成为可能。本节先把容器世界观建起来,再拆开镜像这只"千层蛋糕"。

学习目标

  • 说清容器与虚拟机的本质区别及各自的适用场景
  • 理解镜像 = 只读层集合、容器 = 镜像 + 可写容器层的模型
  • 知道 digest、悬空镜像、多架构镜像的含义与用途
  • 掌握镜像构建流程与缓存失效规则
  • 分清 namespaces(隔离视野)与 cgroups(限制资源)的分工

一、容器 vs 虚拟机

容器是为执行进程提供"可配置隔离 + 资源限制"的环境。它的实现不依赖任何专有技术——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 的每个指令背后是一套"临时容器"机制:

  1. 引擎启动一个临时容器(基于前面所有层构建出的中间镜像);
  2. 在临时容器中执行单条指令;
  3. 将结果保存为新镜像层;
  4. 删除临时容器;
  5. 对下一条指令重复以上过程。

缓存机制让重复构建快如闪电:后续构建时,引擎在缓存中查找"相同基础镜像 + 相同指令"构建出的层——找到就跳过该指令、直接链接已有层;找不到就重新构建,且从此处起缓存全部失效。两个细节值得注意:FROM 指令先检查基础镜像是否在本地,不在才拉取;COPY/ADD 指令即使自身没变,被复制文件的 checksum 变了同样失效。因此最佳实践是"把变化最少的指令放在 Dockerfile 前面",让缓存命中率最大化。

四、隔离原理:namespaces 与 cgroups

容器"轻"的秘密在于它只借用了宿主内核的两个能力:

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 的完整生命周期。


发布者: 作者: 灏天文库 转发
评论区 (0)
U