打开一份镜像,里面装着什么
一句话总结
OCI 镜像由 Image Index → Image Manifest → Image Config 三层 JSON 以及其下方的层 blob 构成,每个组成部分都以自身内容的 SHA-256 作为地址。
为什么需要它
如果不了解一行 docker pull 背后发生了什么,就无法解释:为什么固定 digest 的 pull 会失败,
为什么把 registry 做成冗余部署后 tag 会混乱,或者上传十个镜像后为什么磁盘只增加了一点点。
只要拆开看一次这三层结构,这些问题都会落在同一幅图上。
它如何运作
Image Index(fat manifest)是各平台 manifest 的列表。 在 arm64 Mac 上执行 pull,却意外得到 amd64 镜像的问题,就在这一层完成分流。
Image Manifest 包含一个 config digest、一个顺序有保证的层列表, 以及每一项的 mediaType。常见的 mediaType 如下。
application/vnd.oci.image.index.v1+json
application/vnd.oci.image.manifest.v1+json
application/vnd.oci.image.layer.v1.tar+gzip
Image Config 是运行时真正读取的设置,其中包含 Env、Entrypoint、Cmd、
WorkingDir 等值,以及 rootfs.diff_ids 和 history。
这里会出现最容易混淆的区别。
- manifest 的
layers[].digest是经过 tar+gzip 压缩的 blob 的哈希。 通过网络下载的就是它。 - config 的
rootfs.diff_ids是解压后 tar 的哈希。 在本地堆叠层时使用的是它。
两者数值不同。docker image inspect 的 RootFS.Layers 显示的是 diff_id,
而在 registry 中看到的 digest 属于压缩版本。不理解这一点,就会误以为
“明明是同一个镜像,哈希却不一样”。
内容寻址的收益可以归纳为三点:去重(内容相同只存一次)、 完整性验证(对收到的字节做哈希并与名称比较)、不可变性(哈希相同则内容相同)。
在实际工作中会遇到的情况
digest 与 tag 的区别占据了实务问题的一半。digest 是内容哈希, 因此不存在缓存失效问题,也不会产生实际可行的冲突。tag 是人赋予的名字, 随时可以改为指向另一个 digest。状态就在这里,分布式系统中所有困难的部分也都在这里。 试图把多台 registry 组成集群时,问题最终总会收敛为如何串行化 tag 更新,原因就在这里。
格式转换也必须小心。从 Docker schema 转为 OCI 时,manifest 的字节会改变; 字节改变,digest 也会改变。这样一来,固定 digest 的 pull 与签名验证会同时失效。 “内容明明没变,为什么不能用了”的答案就是这个。
用数字看,层共享非常清楚。如果十个镜像都使用在 ubuntu:22.04 上安装了 node 的基础镜像, 实际存储的是一份基础层 + 十份应用层。因此统一基础镜像,本身就是 registry 容量策略。
使用更少的层,并保持稳定顺序
理解镜像结构后,就能解释为什么有的 Dockerfile 只需几秒构建,另一些却每次都要几分钟。 缓存以层为单位,只要一层改变,后面的所有层都必须重新创建。
于是得到一条顺序原则:不常变化的放前面,经常变化的放后面。 先只复制依赖清单并安装依赖,再复制全部源码之所以是标准做法,原因就在这里。 如果先复制源码,哪怕只改一个字符,也要从安装依赖开始重新执行。
**层的数量本身也有成本。**每层都附带元数据,下载时请求会被拆分, 文件系统需要叠加的层数也会增加。但如果不加区分地合并成一行,整个缓存都会失效, 因此判断标准应该是:把会一起变化的内容放在同一层。
而且,**已删除的文件不会从镜像中消失。**如果在后面的层中删除前一层创建的文件, 叠加后的视图中虽然看不到它,文件仍原样留在前一层并计入大小。 这与前面模块讨论的 secret 问题结构完全相同,从容量角度也会得到相同结论: 要么在同一层创建并删除,要么使用多阶段构建,只复制最终产物。
多阶段构建能最干净地解决这个问题。把构建工具和中间产物留在前一阶段, 最后阶段只复制运行所需的内容。编译器和缓存会被整体排除,镜像可以缩小数倍, 攻击面也会同时缩小——最终镜像中没有的工具,入侵者同样无法使用。
最后还要决定:**基础镜像固定到 digest,还是保留 tag。**固定 digest 可以获得完全可复现性, 但不会自动接收安全更新;使用 tag 会跟随更新,却可能让昨天和今天的构建采用不同内容。 标准做法是先固定版本,再另设自动提出更新建议的机制。
后续实验要做什么
确认 image ID 就是内容哈希,把镜像导出为 tar,亲自打开其中的 manifest 和 config blob。 再用 skopeo 从 registry 工具的视角查看相同信息,并通过比较 digest 证明: 从相同基础镜像构建的两个镜像共享第一层。