Moby 仓库中的 Docker Distribution 路线图:Registry 2.0 设计目标与数据分发架构演进
本文基于 vendor/github.com/docker/distribution/ROADMAP.md(Docker Distribution 项目路线图)展开,结合 Moby 仓库中被 vendor 的 distribution Go 包源码与
daemon/internal/distribution的调用证据,梳理 Docker 下一代镜像仓库 Registry 2.0 的项目目标、组件划分、核心设计原则,以及删除(Deletes)、检索(Search)等尚未落地的功能讨论。读完本文,你将理解 Docker 镜像内容分发模型"名称—标签—清单—内容地址"的来龙去脉,以及这套设计如何在 Moby(Docker Engine)的镜像拉取与推送链路中落地。
Distribution 项目(即 github.com/docker/distribution,Docker Registry 2.0 的官方实现)被 Moby 以 vendor 依赖的形式内嵌在 vendor/github.com/docker/distribution 目录中,由 Docker Engine 侧的拉取、推送代码调用。这份 ROADMAP 是一份"活文档",它定义项目的高层目标、识别当前组件,并阐明其与 Docker 平台之间的发布关系。由于文档诞生于 Registry 2.0 的早期阶段,其中既有已经兑现的设计原则(如内容寻址、blob 不可变存储),也保留了至今仍在演进的功能讨论——理解它,是理解现代 OCI 分发生态的一把钥匙。
一、路线图在 Moby 仓库中的实际位置
在 vendor/github.com/docker/distribution 这一 vendor 目录中,Moby 实际引入了该项目的以下组成部分:
- 包级接口定义:根目录下的 blobs.go、manifests.go、registry.go、tags.go 定义了 Descriptor、BlobService、ManifestService、Namespace、Repository 等核心抽象;
- API 规范与路由:registry/api/v2(路由、URL 构建、错误码)与 registry/api/errcode;
- HTTP 客户端:registry/client(含 repository.go、blob_writer.go 等);
- 存储缓存扩展点:registry/storage/cache。
从源码结构看,Moby vendor 的是 distribution 的 Go 包与客户端侧能力,而完整的 Registry 2.0 服务端存储实现并未随 Moby 一起发布——这正是 ROADMAP 所述组件关系的一个缩影:Registry 服务本身独立发行,而 Docker Engine 通过内嵌的 Distribution 包完成对任意 registry 的 pull/push。在 daemon/internal/distribution 目录中可以看到实际消费方,例如 pull_v2.go、push_v2.go 与 manifest.go,它们把 Moby 引擎的镜像层传输过程与 distribution 的数据模型连接起来。
二、Distribution 项目的五大总体目标
ROADMAP 开篇给出了项目的顶层目标,它们也是评估每一项新特性的标尺:
- 用新实现取代既有
docker-registry:作为主要实现替换旧版 Python 编写的 Docker Registry(docker-registry),即交付下一代 Registry 2.0。 - 替换 Docker 引擎中的既有 push/pull 代码:将 Docker Engine 内部原有的镜像推送与拉取逻辑替换为 distribution 包实现——对应今天 Moby 中
daemon/internal/distribution的存在。 - 为 Docker 镜像分发定义强数据模型:建立清晰、可验证的内容模型来承载镜像分发。
- 为 Docker 平台提供灵活的分发工具包(distribution tool kit):以 Go 包形式暴露可复用能力。
- 解锁新的分发模型:通过解耦内容分发与镜像格式,让此前不可能的分发方式成为可能。
从 Moby 源码可以印证目标 3 与 4 的落地形态:包级模型全部收敛在几个顶层 Go 接口中——registry.go 定义 Namespace(命名空间,按名称寻址一组 repository)与 Repository,manifests.go 定义 ManifestService(Exits/Get/Put/Delete)与 Manifest 接口,tags.go 定义标签访问;任何符合这些接口的存储后端都能接入同一套分发协议。
三、组件划分与发布节奏
Distribution 项目的组件通过里程碑(milestones)管理:某个功能或缺陷修复会被挂到对应组件的里程碑上;未挂入里程碑的功能即被视为"当前未排期"。
文档明确的组件只有两个:
3.1 Registry
Registry 是 distribution 仓库的主体部分。Registry 2.0 是下一代 registry 的首个发布,其首要工作是实现新的 registry API,并把重心放在安全与性能上。
3.2 Distribution Package
Distribution 项目的核心是一组组成组件的 Go 包集合。在该文档写作时,其中大部分包构成了 Registry 的实现。文档特别警告:
这个包集合本身被视为**不稳定(unstable)**的。如果你在使用它,请务必 vendor 锁定所依赖的版本。
Moby 仓库正是这一告诫的实践案例:它通过 vendor 目录将 distribution 源码冻结在仓库内,从而让 Docker Engine 的构建不随上游 API 变动而漂移。
四、Registry 的五大设计原则
围绕项目总体目标,Registry v2 的设计遵循以下原则,任何新特性在合入前都应拿这些原则逐一对照。
4.1 数据存储与分发优先(Data Storage and Distribution First)
Registry 的首要职责是提供可靠、一致的镜像数据存储位置。它只应提供拉取镜像数据所需的最小索引,不应更多。这意味着在新增特性与 API 时要非常克制——尤其是那些要求"昂贵、不断增长的索引"的特性。请求应当可以在"恒定时间"(constant time)内被服务。
这一原则在数据模型上的体现是:Registry 面向的是不可变 blob 的存取,而非对内容做深度关系化索引。
4.2 内容寻址(Content Addressability)
Registry API 中使用的所有数据对象都应是内容可寻址的,内容标识符应当安全且可验证。这为构建更高级的内容分发系统提供了安全、可靠的基础。
在 blobs.go 中,这一原则直接落在类型系统上:Descriptor 是描述任意 blob 的通用结构,由 MediaType、Size、Digest 等字段构成,注释明确说明该结构即"wire 协议格式",字段只增不改。任何内容都能通过其 digest 唯一标识并回源校验:
// blobs.go
type Descriptor struct {
MediaType string `json:"mediaType,omitempty"`
Size int64 `json:"size,omitempty"`
Digest digest.Digest `json:"digest,omitempty"`
URLs []string `json:"urls,omitempty"`
...
}
digest 由 github.com/opencontainers/go-digest 提供(如 sha256),客户端与服务端都能以此验证字节流的完整性——这就是"可验证的内容寻址"。
4.3 内容无关(Content Agnostic)
过去,镜像格式的任何变动都会引发 Docker 与 Registry 两侧的大改动。通过把分发与镜像格式解耦,两种格式可以各自演进而无需彼此协调。因此不仅要"把 Registry 从 Docker 中解耦出来",也要"把 Docker 从 Registry 中解耦出来"。
更进一步,新 Registry 应对内容保持无关性:它只提供一个由"名称、标签、清单(manifest)与内容地址"构成的数据模型,让这个模型去承载任意内容。
这一设计的直接成果是分层引用结构(roadmap 中 Deletes 章节有精炼表述):
Manifests 引用 layers;Tags 引用 manifests。(Manifests reference layers. Tags reference manifests.)
manifests.go 的 Manifest.References() 返回构成该清单的一组 Descriptor,可能是层、资源或其他清单——这正是"content agnostic"在接口层的支撑:Registry 只关心引用关系,不关心被引用内容的镜像语义。因此 OCI Image Spec 等新格式得以在不改动 Registry 传输层的前提下演进。
4.4 简单性(Simplicity)
新 Registry 应更接近微服务组件而非其前身:更窄的 API、更少的服务依赖、易于部署。在修改 API 或增加依赖之前,应先探索其他替代方案——如果确实需要某功能,优先考虑以**扩展(extension)或伴生服务(companion service)**的方式提供,而非打进 Registry 核心。
4.5 可扩展性(Extensibility)
Registry 应提供扩展点来追加功能:核心范围保持窄,但保留加功能的能力。搜索、索引、同步、registry 浏览器等都属于这一类功能,除非证明确实无法通过扩展实现,否则不应被加进核心。
五、活跃的功能讨论:被刻意挡在核心之外的特性
以下是文档写作时正在进行的特性讨论。文档明言:如果你没看到自己心仪的功能,可以联系项目组讨论——目标在于让新特性在合入 Registry 前经过严格的设计流程。
5.1 代理到其他 Registry(Proxying)
Registry 已有一种 pull-through 缓存 模式,但受 Docker 客户端限制,当时只允许镜像官方 Docker Hub。这项能力要等到镜像溯源(image provenance)在 distribution 项目中明确并实现后,才能进一步放开。
5.2 元数据存储(Metadata storage)
Registry 元数据当时与 manifest 和层数据一起存放在存储后端。这换来的是简单性与状态维护的可靠性,代价则是一致性与高延迟。文档提出:可变的 registry 元数据操作应被抽象到 API 之后,从而允许 ACID 兼容的存储系统来承载元数据。
5.3 点对点传输(Peer to Peer transfer)
分布式的 P2P 内容传输当时只开启了初步讨论,文档中并未给出结论性方案,仍属探索范畴。
5.4 索引、搜索与发现(Indexing, Search and Discovery)
旧版 registry 曾内置搜索以服务私有仓库,V2 已将其剔除,原因有二:将搜索功能与 registry 解耦能让 registry 更易部署(尤其是不需要搜索的场景),同时进一步把镜像格式从 registry 中解耦。
探索方向是利用 catalog API 与通知系统(notification system) 构建外部索引;较为主流的思路是定义一套通用搜索 API,以 registry(或一组 registry)伴生系统的方式运行,为镜像发现提供动力。该问题有两个难点:
- 如何定义一套能适配不断变化的数据格式的搜索 API;
- 如何与
docker search客户端流程集成。
仓库中与"目录列举"相关的抽象可以在 registry.go 看到雏形:Namespace.Repositories 返回按字典序排序的 repository 目录列表(catalog),RepositoryEnumerator、RepositoryRemover 等接口则保留了外部工具化的可能性。相关 issue 编号为 206。
5.5 删除(Deletes):一个被详细推演的设计难题
文档特别提醒:删除是呼声极高的功能。在提出该需求或参与讨论之前,请完整阅读本小节并理解删除背后的难题。
删除看似简单,实际上存在大量使多数方案变得不理想甚至病态的因素。这段论述是全篇最具技术密度也最值得精读的部分,逐层展开如下。
(1)删除的元原则:删除只应移除被请求删除的数据,绝不能误删其他数据。误删造成的后果比"未删除被请求的数据"更糟。因此默认策略是宁可让数据多保留久一些,只在能确定可安全删除时才删除;当时的 Registry 行为是永久保留数据,以保证数据绝不被错误清除。
(2)底层存储模型:所有 registry 数据都存放在一种文件系统布局中,由"存储驱动"(storage driver)实现——本质上是一个虚拟文件系统(VFS)。系统必须假设该 VFS 层是最终一致的、读后写(read-after-write)一致性差,因为这是所有存储驱动的最大公约数。写入时按"反向依赖顺序"来缓解这一问题,但这也使得更大的事务性操作不安全。
(3)内容寻址的 DAG:在 VFS 之上,是由 blob 构成的内容寻址有向无环图(DAG):
- manifest 引用 layer;
- tag 引用 manifest;
- 同一数据可能被多个 manifest 引用,因此即使在不同仓库中,数据也只存一份;
- 于是系统拥有一组被 tags 与 manifests 引用的 blobs。
想要删除一个 blob,必须确认它不再被任何 manifest 或 tag 引用;删除 manifest 时可以顺带尝试删除其引用的 blob。"判断一个 blob 是否仍被引用"正是问题的核心。相关接口可以在 blobs.go 中找到对应物:BlobDeleter.Delete、BlobEnumerator.Enumerate(遍历全部 blob)、BlobStatter.Stat 构成了实现删除/回收所依赖的最小原语集。
(4)为什么并发让问题变难:概念上,删除一个 manifest 及其资源很简单——枚举所有 manifest、收集被引用的 blob 集合、删除不在集合中的 blob。眼尖的读者会认出这是垃圾回收(garbage collection)问题:与编程语言中的 GC 一样,当视图始终一致时它非常简单,一旦引入并行与不一致的数据视图就极具挑战。
文档用一个并发反例说明:进程甲正在删除 manifest A,扫描后判定其所有 blob 均可删除;与此同时进程乙接收新 manifest B,引用了 A 的部分 blob——由于这些 blob 当时"存在",操作通过;随后进程甲删除了这些 blob,导致本应数据完整的 B 从此无法被服务。这正是一个典型的"检查与使用之间的竞态"。
(5)候选方案:安全删除需要协调操作,文档列举了四类方向:
| 方案 | 思路 | 优点 | 代价 |
|---|---|---|---|
| 引用计数(Reference Counting) | 为每个 blob 维护引用计数 | 直观 | 跨一组 registry 维护一致的计数共识困难(需 Paxos/Raft 类共识协议);为存量 registry 构建初始计数表需一次全量扫描 |
| 锁住全世界的 GC(Lock the World GC) | 停止所有写入,遍历数据存储找出全部 blob 引用,删除所有无引用 blob | 非常简单、准确、有效 | 服务期间需长时间禁用写入;慢且昂贵 |
| 分代 GC(Generational GC) | 类似上者,但不阻断写入:写入导向新存储后端,读请求广播到新旧两端,GC 只在只读区执行 | 写不中断,只读区的数据可安全删除 | 复杂度与协调成本高 |
| 集中式裁决(Centralized Oracle) | 用集中式事务数据库精确获知任意时刻哪些数据被引用 | 单点管理数据,避免协调问题;对多数部署是很好的选择;镜像服务的主要瓶颈通常不是元数据 | 元数据可扩展性受限,成为潜在瓶颈 |
(6)权衡结论:当时项目的取舍是用磁盘空间换取简单性与易部署性——简单与易部署能显著降低开发者介入成本,而那才是当时软件工程中最昂贵的资源。任何删除方案的落地都会剧烈改变这一平衡:用非常廉价的磁盘空间换取一套复杂的部署与运维故事。文档请社区列举其他可行方案,并强调无论采用哪种方案,实现本身都是巨大的工程考量(例如 mark-sweep 看似简单,但协调工作量可能超过构建集中式裁决方案)。相关 issue 编号为 422、461、462。
这一"宁可保留,不可误删"的取向与前述内容寻址理念在工程上形成了自洽闭环:registry 存储的是不可变、可校验的 blob,磁盘成本可控而数据安全性优先。Moby 作为消费者侧并不直接承担该 GC 决策,但理解这份设计推演,有助于判断镜像仓库在什么条件下才能安全启用"untag 后回收层数据"之类的操作。
六、Distribution Package 的稳定性承诺与 vendor 实践
Distribution Package 章节传达了一个关键的工程信号:包不稳定,使用必须 vendor。对下游集成方而言,这一策略意味着 API 可能随版本演进,仓库应锁定具体修订。
Moby 仓库就是该策略的直接样本。在 Moby 中搜索 distribution 包的引用可以看到两个层面的消费:
- 引擎镜像分发:
daemon/internal/distribution中的 push_v2.go(镜像推送)、pull_v2.go(镜像拉取)、repository.go(仓库抽象)以及 transport.go(带 token 认证的 HTTP 传输); - 接口与实现分离:vendor 目录内的包接口保持纯净,客户端认证等能力分布在 registry/client/auth 等子包中。
这印证了 ROADMAP 中"为 Docker 平台提供灵活的分发工具包"的目标:引擎不必自己重新发明 registry 协议,而是复用一套被严格评审过的 Go 包,并在自身仓库内固化版本。
七、项目规划:开源的规划流程
项目采用**开源规划流程(Open-Source Planning Process)**来定义路线图,由 Project Pages 定义每个里程碑的目标并标识当前进度。这是一种向社区开放、按里程碑滚动推进的治理方式,与第一节中"组件经里程碑管理、未排期即视为不实现"的机制配合,构成整个 distribution 的演进节奏。
结语:一份"仍在演化"的设计蓝图
这份 ROADMAP 的价值不在于预测未来,而在于把设计约束固化下来:Registry 2.0 是一个数据存储与分发优先、内容寻址、内容无关、力求简单却保留扩展点的微服务组件;被刻意排除的核心功能(搜索、索引、同步、删除回收)不是被遗忘,而是被设计为"扩展或伴生服务",并针对其中最危险的"删除"给出了完整的安全分析框架。
对阅读 Moby 源码的开发者而言,理解这份路线图能帮你快速建立坐标系:
- 看到
Descriptor/digest 时,知道这是"内容寻址"原则的实现; - 看到 manifest 与 blob 的引用层级时,知道这是"内容无关"模型的落地;
- 看到 pull/push 链路复用 vendor 包时,知道这是"灵活分发工具包"目标的体现。
仓库后续演进可以继续沿 vendor/github.com/docker/distribution/ROADMAP.md 追踪,而数据模型实现细节可在 blobs.go、manifests.go 与引擎侧 daemon/internal/distribution 中交叉印证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00