本地已知镜像的index、manifest 与 blob,接下来问另一台机器如何得到相同字节。Distribution Specification 约定 registry API;它不等于 Linux 的 mount、容器进程的 exec,也不保证客户端已把层成功解压成可运行的 rootfs。

引用解析和内容获取分开

客户端把名字/tag 交给 registry,商议清单媒体类型,读取 manifest 后根据 descriptor 的 digest 与 size 请求 config 和 layer blob。digest 绑定字节,tag 可重新指向另一 manifest;“我拉到了 tag”不能证明和上一次拉到的内容相同。Blob 下载成功仍需校验摘要、解压层、建立本机快照才能供运行时使用。认证失败应定位到 HTTP 挑战、凭据与仓库权限,缺失 blob 则应定位对应 digest 和 HTTP 返回,而非笼统说“网络不通”。不要将镜像 registry 的身份验证和镜像签名混为一谈。

引用如何落到确定的 manifest

OCI Distribution Specification 把一次 pull 的对象分成 manifest 和其引用的 blobs。客户端向 /v2/<name>/manifests/<reference> 请求,reference 可以是 tag,也可以是 digest;商议的 Accept 指明客户端接受的 manifest 媒体类型,响应的 Content-Type 再表明服务器实际返回了什么。一个 tag 在两次请求之间可以重新指向不同 manifest,因此“执行两次 pull example:lab”并不是复现同一字节的充分条件。要做真正对照,保存当时解析到的 manifest digest、响应字节的 hash、选中的 platform,以及之后取得的 config/layer descriptor,后续再按 digest 请求。HTTP 状态 200 只说明端点返回了响应;按 digest 请求时仍应验证 body 与请求摘要相符,而不是从 Docker-Content-Digest 头里拿到一个字符串就信任全部 payload。

索引和单平台 manifest 也不能混写。如果 reference 解析到多平台 index,客户端先选与节点平台匹配的 manifest 描述符,再去获取那个 manifest 的 config 与 layers。清单媒体类型不受支持时,客户端可能根本没走到层下载;某架构对应 manifest 缺失也不能归因于“磁盘坏了”。记录 HTTP 请求头、返回 Content-Type、manifest/index digest 和平台匹配条件,有助于把拒绝、协商与真正的缺失 blob 区分开。12 研究的是传输合同,不实现兼容所有镜像媒体类型的客户端;本机 oci_layout.py 不发 HTTP 请求,因此不能从其本地 index.json 验证 registry 实际响应。

下载字节与可执行 rootfs 之间的阶段

拿到 manifest 以后,客户端通常根据描述符检索 config 与所选 layers 的 blobs,按 descriptor 的 size 和 digest 校验下载内容。压缩层 blob 的 digest 依其收到的压缩字节计算,解压后的 DiffID 则要同 config 的层序列核对;后续才由实现将变化集解包为本地内容/快照并准备运行所需根文件系统。下载 100% 完成不等于 tar 可安全解开、不等于上层挂载成功,更不等于程序已经 exec。要验证终点,还须保留本机解包后所选镜像、运行时对象 ID、实际启动的 PID 及探针请求结果;若缺运行时环境,仅能断言传输和静态字节检查的结果。

反过来,一次缺失 blob 也有多种前提:manifest 中确实引用该 digest 吗?请求的是同一 repository 的 blobs 路径吗?服务器返回了明确的 404 还是认证失败、代理超时、重定向失败?客户端本地内容存储是否已有这个 digest 并绕过了网络访问?实验要强制在自己 VM 的干净缓存或新实例上做记录;不能删共享镜像仓库的 blob 来求一个 404。当前环境没有专用 registry 与 OCI runtime,正文中任何 HTTP 状态仅是规范给出的预期,不是此次执行所得的日志。

鉴权失败和摘要失败各自回答什么

Registry 要求认证时可能先返回挑战,客户端再根据挑战请求凭据或 token,并携带有权访问该 repository 的请求重试;认证服务的身份、请求范围与 pull/push 的具体权限需要分别记录。HTTP 401 可能是认证尚未完成、凭据错误或挑战流程中的正常一跳,单看一个状态不能断言镜像内容损坏。HTTP 403 或权限拒绝也只说明这次操作未获授权,不表示相应 digest 一定不存在。失败案例在自建 registry 中用两组专门的测试凭据做同一 digest 的 pull,核对挑战、授权范围、最终响应与客户端退出码,不把测试 token 打进博客或构建日志。

摘要校验是另外一条线:即使 registry 正确验证了账号,传输的 blob 字节仍要按描述符验证;即便所有 blob hash 一致,也不代表发布者可信或镜像经过签名。认证回答“请求者是否获准读/写这个 repository”,签名与信任策略回答“这些内容由谁发布并符合什么策略”,内容地址回答“这些字节是否与所给 digest 相等”。三问的证据分别来自 HTTP 授权记录、签名/策略检查与本地摘要核算,不能用一次 docker login 退出 0 一举替代。33 篇讨论签名,当前章节只要求不把凭据获取误作供应链证明。

push 也有明确的数据依赖

准备向自己的 registry 推送时,应先确认被 manifest 引用的 blobs 已存在或成功上传,再上传指向它们的 manifest;若上传某层中途断开,需要按实现与规范支持的上传状态续传或重新开始,而不是写一个指向不存在 blob 的 manifest 就当 push 完成。按 digest 与按 tag 读取是两种不同请求:将新 manifest 推送到同一个测试 tag 可以改变后来按 tag 拉取的结果,先前保留的 digest 仍指原始字节。测试应使用合成镜像,不上传含真实业务凭据的层。完成后记录本次 manifest 字节摘要、config/layer digest、客户端和 registry 版本,清理自己创建的 tag、blob 测试资源及临时容器,删除过程中发生被别的测试引用的对象时停止而不是强制清理整仓。

如果 registry 配置了垃圾回收或删除策略,还要区分“从 tag 列表移除了引用”“manifest 在 repository 中不再能获取”和“底层 blob 的物理字节已回收”。一个测试 tag 看不见不能直接证明旧 blob 被擦除,和 11 的历史层问题类似。只有在专用 registry 中读取自己的残留对象、记录 GC 的真实效果,才能得出清理结论;在共享仓库中不能通过故障注入去验证缺失 blob。数据分发是镜像生产与节点执行之间的一段链路,不应将其操作经验替换成 Docker 命令大全。

正常实验应在专用 VM 自建仅用于教程的 registry:用同一探针镜像按 digest 推送、拉取,记录请求、manifest digest、层 digest、HTTP 状态、容器 ID 以及本地展开成功。反例只对这份自建制品使用错误凭据或删除受控 registry 的某个测试 blob,核对失败发生在鉴权、传输还是解包层,再恢复受控数据。不能篡改共享镜像仓库,更不能从本机发散出线上故障注入。

当前环境没有 Docker/containerd、可控 registry 与 ip 工具;附件提供专用环境中的操作步骤,可取得源码包只有离线镜像布局示例,不能冒充 registry 的 HTTP 实验。真实 push/pull、凭据拒绝、缺 blob 与解包结果均 NOT_RUN。本地可用 10 的 verify 核对字节,但这只是静态完整性检查。

专用 VM 补跑时先锁定 runtime、registry、构建器和节点平台的实际版本,再为 probe-app 构建带完整解释器和入口的镜像。记录一次按 digest 正常 push/pull、一次错误凭据的授权失败和一次仅影响自己 registry 的缺失 blob,分别列出每步输入、HTTP 状态、退出码与生成的对象标识。最后以同一摘要启动探针并核对 HTTP 响应,再删除本实验创建的制品与临时实例。若正常 push/pull 也失败,先报告失败层并保留原始输出,不能把脚本中“预计拉取完成”当实际结果;这个后续验证与本机现有静态 OCI 字节校验仍是两套实验。

列一个正常与边界案例的检查顺序

正常案例先确认专用 registry 健康,再把本次 image 的 manifest 和 blobs 上传,按新生成的 manifest digest 请求一次并从 HTTP 响应字节重新算摘要。客户端用这个不可变引用拉取,核对本地内容存储确实拥有指定 config 与 layer 的 digest、层是否成功解压及探针是否产生请求结果。若复用缓存,网络传输未发生也可能得到正确制品;因此记录“HTTP 真正下载了哪些 blob”“本地已有的是什么”和“最终执行了什么”三列,不把缓存命中错写成 registry 下载成功。最终请求 /health 返回成功也不补足这些内容身份步骤,否则无法证明响应来自这份刚拉取的镜像。

错误凭据案例只改认证条件,保持同一 repository、tag/digest 和客户端网络配置;如果读请求未携带有效授权且最后返回认证错误,再核对是 token 获取被拒还是 registry 最终拒绝。若之前客户端缓存了 token,表面上“错误密码仍能拉取”可能只是本机仍有有效登录状态,必须先在自己 VM 的隔离客户端配置目录下做对照,而不是登出共享宿主其他账户。缺 blob 案例只在自己搭建的 registry 中制造:先保存 manifest 指向的目标 digest,再在受控环境令该 blob 不可获取,记录 GET 请求 URL、HTTP 状态与客户端是否因本地缓存跳过这次请求。没有真实请求就没有“服务端返回 404”的日志,仍写为未验证。

若 GET /v2/<name>/manifests/<reference> 返回 404,按规范首先是该 repository 中的 manifest 引用不可获取;若 manifest 200 而 GET .../blobs/<digest> 返回 404,定位到具体 blob 的下载阶段。若 blob 请求返回 200 却与 descriptor digest 不符,客户端应拒收而不是用“成功下载”覆盖校验失败。若全部 blob 正确而解包失败,应去查层的解压与变更集应用,而不是责怪鉴权。最后若解包与挂载均通过但应用不就绪,调试对象才转为入口进程、监听地址或请求路径。按 URL、digest、响应和本机状态定位,比归纳为同一个“docker pull 失败”更接近可迁移的排障方法。

时间关系也重要:可变 tag 在构建完成与部署发起之间改变指向时,原先记录的 Dockerfile、镜像名和节点启动时解析的 digest 可能不再相同。部署记录应固化执行时选定的 digest,保留当时 platform、manifest/media type 以及运行时本地 ID。未设置私有 registry、无法记录响应头和原始字节的环境只能完成镜像布局的离线实验;把离线脚本输出的 digest 填进一段想象的 HTTPS 响应,会使之后所有结论失去可复核性。当前文章的 HTTP 细节都标记为规范规则和 VM 补跑方案,而不是本次云端请求所得。

运行清理前还需先确定是否只有自己的实验 tag 指向这组对象;删去 tag 不是删除内容存储中的共享 blob。若测试 registry 为其他任务共用,就只删除自己的临时容器与自有标签并报告未回收的内容,不能根据经验去触碰其他任务的镜像缓存。

现象 对照层 判定边界
tag 解析变化 manifest digest 前后 名字不是不可变标识
HTTP 401 challenge 与凭据范围 不等同于镜像摘要不匹配
layer 下载结束、启动失败 digest、解包与 rootfs 下载不等于进程已执行

练习

  1. 先预测把测试 tag 重新指向另一 manifest 后,按 digest 拉取与按 tag 拉取是否还能保证得到同一内容;在专用 registry 对照 digest。
  2. 先预测成功拿到 manifest 但一个 layer blob 返回 404 时,客户端能否启动该镜像;保存 HTTP 错误、blob digest、本地内容存储和清理结果,再说明失败层。

上一篇:11:层与快照;下一篇:13:构建应用镜像。

系列总目录:从进程隔离到运行时与编排。

参考资料