KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · 层 — keel 龙骨
类型:衍生概念(服务于「镜像」) · 应用课对应:02 章 · 层、缓存与指令顺序
类型:衍生概念(服务于「镜像」) · 应用课对应:02 章 · 层、缓存与指令顺序
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。
一、它为什么会出现
两个现象,都指向同一件事:
① 改了一行业务代码,构建却重装了一遍依赖 —— 60 秒变成 5 分钟
② 两张来自不同仓库的镜像,磁盘上只占了一份空间
它面对的需求是:镜像要在网络上来回搬,一台机器上还要同时存很多张,而这些镜像长得很像(大多数机器上都有一张 Node 或 Python 基础镜像)。
问题在于:如果每张镜像都存一整份文件系统,那么传输量和磁盘占用都会随镜像数量线性上涨。
答案不是"压缩得更好",而是只存变化的部分。
二、它是什么
一句话:层是镜像里"相对上一层改了哪些文件"的一次记录——新增了什么、改了什么、删了什么。它的名字,就是这段内容算出来的摘要。
由此有两个直接后果,都是日常要用的判断:
- 层的身份是内容,不是名字。 两张镜像里只要有一层内容相同,它就是同一个对象,磁盘上只存一份。
- 一层不能脱离它的上一层单独解释。 它记的是"改动",不说清"改的是谁",这段改动就没有意义。所以层之间有先后顺序,顺序也参与身份计算。
它不是这两样东西——混淆会带来误判:
- 不是"一次命令执行的记录"。 一次执行可能不产层:
ENV、CMD、EXPOSE只改配置,落在 config 里而不是层里。所以"历史条数"和"层数"通常对不上。 - 不是可以回退的快照。 层里没有父层、没有时间、没有执行过的命令,只有内容。所以它只能被复用和叠加,不能被"回到"。
三、它长什么样
一张镜像的层清单,就是一组带摘要与体积的条目(以下取自公开镜像的元数据,本机用不依赖 daemon 的方式读到):
L1 sha256:e2de96513ba9eb53b4… 3849738 B ← 基础系统
L2 sha256:f7f2d304681aaa935c… 55585204 B ← 装出来的运行时
L3 sha256:e554276b05e6306c5a… 1262001 B
L4 sha256:d39db1cf9caa4f49c5… 447 B ← 最后一层只是几行启动脚本
看它有两个命令,各自的用处不同:
docker history <镜像> # 逐条历史 + 每条的 SIZE:SIZE 不为 0 的那几行才落在层上
docker system df -v # 每个镜像的 SHARED SIZE / UNIQUE SIZE:共享了多少、独占多少
本机 Docker daemon 没有运行,上面两条命令的执行结果没有实测。这里给的是"该看哪一列、判据是什么",不是"我跑出来是这样"。
四、它怎么工作
一次改动变成一层,是四步:
① 执行 一条会改文件系统的指令跑完
② 差分 把执行前后的文件系统差异打成一份 tar(新增 / 修改 / 删除都在里面)
③ 命名 对这份 tar 算摘要——摘要就是这一层的名字
④ 存放 按摘要存进本地库;下次要的层已经在库里,就直接复用
这四步带来三处可复用的地方。它们不是三个功能,而是同一个机制在三个场景下的表现:
| 场景 | 复用的判据 |
|---|---|
| 磁盘 | 这个摘要已经存在 → 不存第二份(跨镜像共享) |
| 构建 | 这一层的输入没变 → 不重做(缓存命中) |
| 传输 | 本地已有这个摘要 → 不重传(pull 只取缺的那几层) |
五、它会带出哪些概念
- 缓存:命中判据就是"这一层的摘要是不是已经在库里";而"上一层变了、这一层也要重做",源头就是本节第一条性质(一层不能脱离上一层解释)。→ 应用课 02 章
- 构建上下文:
COPY的内容变一次,这一层的摘要就变一次——所以上下文里混进易变文件会持续销毁缓存。→ 03 页 - 容器:运行时新加的那个可写层,是层的一种特殊形态——不共享、不可复用,随容器一起消失。→ 02 页
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
| "每条会改动文件系统的指令都产出一层" | 判据其实是"有没有产生非空的文件系统差异";只改 config 的指令(ENV、CMD)不产层 |
| "缓存键 = 父层的缓存键 + 本指令的指纹" | 因为一层只记"相对上一层改了什么",所以"上一层是谁"必须参与这一层的身份计算 |
| "清理必须与产生写在同一层,否则体积不降" | 删除在层里不是"减掉字节",而是新增一次遮蔽记录;下面那一层的字节还在原地 |
这一页改变了你的哪个判断:看到"两张镜像共享了 N MB"这类报告时,你知道要去比对层摘要——摘要相同才叫共享,名字像不算(都叫 alpine 并不保证共用同一层)。
自测题
docker history打印出 9 行,但镜像只有 4 层。这 5 行的差别在哪?- 为什么把
rm -rf拆到单独一行RUN里,镜像不会变小?