containerd 开发报告深度解读:2017 年 5 月的镜像推送、Go 客户端与测试体系建设
【免费下载链接】containerdAn open and reliable container runtime项目地址: https://gitcode.com/GitHub_Trending/co/containerd
本文基于 docs/historical/reports/2017-05-26.md 这篇历史开发报告展开。它以第一视角记录了 containerd 在 2017 年 5 月冲刺"功能完备"(feature complete)的关键节点:
dist工具新增镜像推送能力、首个 Go 客户端初具雏形、集成测试体系开始落地,以及命名空间(namespace)与事件(events)两大待办功能。阅读本文,你将掌握这段时期内 containerd 镜像推送命令的设计思路与用法、客户端"拉取镜像 → 生成 OCI spec → 创建容器 → 运行任务"的完整编程范式,并能结合当前仓库源码理解这些早期设计如何演化为今天 client、pkg/oci、core/events 等核心包。
项目状态:功能完备冲刺与 API 稳定性规划
报告开篇给出了 2017 年 5 月 26 日的项目全景:containerd 整体已非常接近功能完备,最后的几项能力(如命名空间支持)正在收尾。报告提到当时已有多个下游消费者基于 containerd 做实现,包括 Kubernetes 的 CRI、Docker 执行引擎、Swarmkit 与 Linuxkit。
从研发节奏上看,团队希望在当月月底达成功能完备,将 6 月留给API 质量打磨与客户端需求确认——具体工作包括错误码(error codes)整理,以及重新审视 protobuf 定义(protos),确保产出一套能在 containerd 1.x 整个生命周期内稳定支持的 API。这一决策对后来的架构影响深远:今天的 api 目录下仍然以.proto定义 + 生成的.pb.go形式组织服务接口(如 api/services/containers、api/services/content),正是当年"API 先行、长期稳定"路线的延续。
Image push:向 registry 推送镜像与对象
报告的核心技术内容之一,是给dist工具新增了push与push-object两个命令:
push:将 manifest 及其全部关联对象(层、config 等)推送到 registry;push-object:将 content store 中的单个 blob 推送到 registry。
这两个命令在当时位于dist工具下,而在当前仓库中,它们的后继实现分别落在 cmd/ctr/commands/images/push.go(ctr images push)与 cmd/ctr/commands/content/content.go(ctr content push-object)。
push 命令的关键语义
报告明确了几个在当时很实用的设计点:
- 无需预先打 tag:
push命令不要求先给镜像打 tag,只需指定远程名称与本地名称。若两者相同,则只传一个参数即可。当前实现中local == ""时会回退为local = ref(见 cmd/ctr/commands/images/push.go#L114-L116),正是这一语义的延续。 - 远程名称可省略对象标识符:即可以不写 Docker 风格的 tag 或 digest,此时可用 manifest digest 作为对象标识符重新拉取 manifest。
- 只推送已存在的镜像:push 不会凭空创建镜像 manifest;创建新镜像需要另行计算层 diff、生成 config 并组装 manifest。当前 push.go 的
Description依然保留了同样的约束:"The image manifest must exist before push."
推送示例与输出解读
报告给出了向本地 registry 推送 ubuntu 镜像的完整示例。dist image list先展示本地镜像(REF、TYPE、DIGEST、SIZE 四列),随后dist push localhost:5000/ubuntu docker.io/library/ubuntu:latest依次推送 manifest、各 layer 与 config,并实时输出进度条、总耗时与吞吐:
$ dist image list REF TYPE DIGEST SIZE docker.io/library/ubuntu:latest application/vnd.docker.distribution.manifest.v2+json sha256:382452f82a8bbd34443b2c727650af46aced0f94a44463c62a9848133ecb1aa8 44.7 MiB $ dist push localhost:5000/ubuntu docker.io/library/ubuntu:latest manifest-sha256:382452f82a8bbd34443b2c727650af46aced0f94a44463c62a9848133ecb1aa8: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:cf9722e506aada1109f5c00a9ba542a81c9e109606c01c81f5991b1f93de7b66: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:b6f892c0043b37bd1834a4a1b7d68fe6421c6acbc7e7e63a4527e1d379f92c1b: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:55010f332b047687e081a9639fac04918552c144bc2da4edb3422ce8efcc1fb1: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:3deef3fcbd3072b45771bd0d192d4e5ff2b7310b99ea92bce062e01097953505: done |++++++++++++++++++++++++++++++++++++++| config-sha256:ebcd9d4fca80e9e8afc525d8a38e7c56825dfb4a220ed77156f9fb13b14d4ab7: done |++++++++++++++++++++++++++++++++++++++| layer-sha256:2955fb827c947b782af190a759805d229cfebc75978dba2d01b4a59e6a333845: done |++++++++++++++++++++++++++++++++++++++| elapsed: 6.5 s total: 44.7 M (6.9 MiB/s)值得说明的是,这里的时序数据显示为当年示例环境下的单次运行结果,实际耗时取决于网络与 registry 性能,不应理解为固定指标。示例本身清晰展示了推送流程的两阶段:先 resolve 出 manifest 描述符,再按依赖顺序上传所有关联 blob。
当前ctr push的演进
如今ctr images push的命令定义保留了当年核心语义,并增加了更丰富的控制项(见 cmd/ctr/commands/images/push.go#L47-L79):
--manifest:按 digest 指定要推送的 manifest;--platform:仅推送特定平台的内容;--max-concurrent-uploaded-layers:限制并发上传的层数;--local:绕过 transfer service,直接从本地客户端推送(此时才支持--skip-verify、--tlscacert等 registry 标志);--allow-non-distributable-blobs:允许推送标记为不可分发的 blob。
非--local路径默认走 transfer service 的client.Transfer调用(见 push.go#L137),而push-object则要求<remote> <object> <type>三个参数,其中对象以 digest 形式给出(见 cmd/ctr/commands/content/content.go#L516-L534)。
Client:让 containerd 用起来"不像远程守护进程"
报告用较大篇幅阐述了客户端的设计动机:由于会有多个下游消费者使用同一套 API,团队希望减少使用者的重复代码,并提供一个"感觉不到是在和远程守护进程打交道"的稳定客户端 API。容器对多数人来说已经足够复杂,containerd 客户端要做的就是把底层系统细节(文件系统操作、tar 解包、rootfs 搭建)全部接管,让构建平台变得毫不费力。
这一设计哲学在今天的 client 包中依然清晰可见:New通过 gRPC 连接 containerd 实例(默认超时 10 秒,见 client/client.go#L104-L113),所有请求都经由context携带命名空间等元数据,上层使用者无需关心 socket 与协议细节。
创建客户端
client, err := containerd.New(address, containerd.WithNamespace("docker")) if err != nil { return err } defer client.Close()这里WithNamespace("docker")将后续所有操作限定在docker命名空间内。在当时这是新引入的命名空间 API 的客户端入口;当前仓库中,命名空间可以通过 client_opts.go 的WithDefaultNamespace(ns string)在客户端级别设置,也可以借助 pkg/namespaces 提供的WithNamespace(ctx, namespace)在 context 级别更细粒度地控制(见 pkg/namespaces/context.go)。
从 DockerHub 拉取并解包镜像
// pull && unpack the image to your snapshot ( overlayfs default ) of choice image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack) if err != nil { return err }WithPullUnpack指示在拉取完成后将镜像解包到快照器(默认 overlayfs)中。当前实现中,该选项等价于设置RemoteContext.Unpack = true(见 client/client_opts.go#L161-L167);client/pull.go 的Pull方法会据此解析快照器名称、查询其能力,并创建解包器(unpacker)把各层 diff 应用到快照(见 client/pull.go#L92-L119)。拉取期间客户端还会自动创建 lease(c.WithLease)以保护下载内容不被 GC 回收。
生成 OCI 运行时规范
// generate the spec based on the image that we pulled spec, err := containerd.GenerateSpec(containerd.WithImageConfig(ctx, image)) if err != nil { return err }在 2017 年 5 月,GenerateSpec与WithImageConfig还是客户端包的一部分;如今它们已经下沉到 pkg/oci/spec.go 与 pkg/oci/spec_opts.go。GenerateSpec会先生成一份默认 spec,再依次应用传入的SpecOpts(见 pkg/oci/spec.go#L67-L82);WithImageConfig则读取镜像的 config 对象,将其中声明的环境变量、工作目录、Entrypoint/CMD 等合并进 OCI spec(见 pkg/oci/spec_opts.go#L365-L368)。这一步是把"镜像"翻译成"运行时配置"的关键桥梁。
基于镜像创建容器
// create the container with a persistent ReadWrite layer based on the image and spec container, err := client.NewContainer(ctx, "redis", spec, containerd.WithNewRootFS("redis-rootfs", image)) if err != nil { return err } defer container.Delete(ctx)WithNewRootFS为容器分配一个基于镜像的可读写(read-write)根文件系统快照。当前仓库中,这一职责由WithNewSnapshot(id, image, ...)承担:它根据镜像的 diff IDs 计算 chain ID 作为父快照,随后调用快照器的Prepare创建可写层,并把SnapshotKey与镜像名写回容器元数据(见 client/container_opts.go#L240-L282)。与之对应的WithNewSnapshotView则创建只读视图(View)。容器创建本身在 client/client.go 的NewContainer中完成,id 在命名空间内必须唯一(见 client/client.go#L338-L377)。
运行容器
// use the current process's stdio task, err := container.NewTask(ctx, containerd.Stdio) if err != nil { return err } defer task.Delete(ctx) pid := task.Pid() // start the redis process if err := task.Start(ctx); err != nil { return err } task.Kill(ctx, syscall.SIGTERM) status, err := task.Wait(ctx) os.Exit(status)这段代码展示了任务生命周期管理的核心范式:NewTask创建任务实例(使用当前进程的 stdio,并携带CreateTaskRequest中的 stdin/stdout/stderr 配置,见 client/container.go#L226-L246),随后Start启动、Kill发信号、Wait等待退出状态。报告强调,这套 API 让用户可以放心使用defer与常规控制流,无需做复杂的文件系统操作、处理 tar 文件或自行搭建 rootfs;API 按生命周期切分,用户可以直接观察运行中容器的各阶段状态,而不必注册 hooks 和回调。
Testing:标准 Go testing 驱动的集成测试
报告指出,随着首个客户端合入,团队同步启动了集成测试工作:基于 Travis CI 运行完整的集成测试,且编写测试只需标准 Go testing 包,无需引入其他测试框架。此外,客户端被特意设计为可 mock——既便于 containerd 自身测试打桩,也便于客户端用户为自己的代码做模拟测试。
这一传统延续至今:仓库的 integration/client 目录集中存放了大量面向真实 containerd 实例的集成测试(如container_test.go、image_test.go、snapshot_test.go),而 core 各包也都配套了单元测试(例如 core/events/exchange、pkg/namespaces 均有对应_test.go文件)。client包通过Service接口抽象各类 gRPC 服务,使得测试中可以注入 mock 服务实现。
What's Next:事件与命名空间的收官
报告末尾列出了发布前的最后两块拼图:
- Events(事件):被视为功能完备前最后一个大特性;
- Namespaces(命名空间):当时已进入实现阶段,初始服务 spec 已设计完成,预期数周内完成。
从今天仓库的视角回看,这两块都已经完整落地:
- 事件系统:
ctr events命令可以直接订阅并展示 containerd 事件(见 cmd/ctr/commands/events/events.go);事件以Envelope形式封装时间戳、命名空间、主题与typeurl.Any载荷(见 core/events/events.go#L26-L32),底层由Publisher与Subscriber接口抽象,core/events/exchange/exchange.go 提供了进程内实现。 - 命名空间:cmd/ctr/commands/namespaces/namespaces.go 提供了
ctr namespace create、ctr namespace list等管理命令;API 层有 api/services/namespaces 的 gRPC 服务定义;pkg/namespaces 则负责 context 与命名空间的绑定、环境变量回退等辅助逻辑(见 pkg/namespaces/context.go)。此外,守护进程侧的事件发布命令也支持--namespace标志(见 cmd/containerd/command/publish.go)。
结语:从 2017 年 5 月看 containerd 的架构基因
这份 2017 年 5 月的开发报告虽然篇幅不长,却浓缩了 containerd 的几条核心架构基因,且全部在今天的主干代码中仍然成立:
- 镜像内容与推送:以 content store 为中心管理 blob,manifest 与对象分离、按需推送的设计,演化为今天的 cmd/ctr/commands/images/push.go 与 transfer service 体系;
- 客户端体验:"减少重复代码、隐藏底层系统细节、可 mock 可测试"的目标,成就了今天 API 稳定、被广泛集成的 client 包;
- 功能完备的边界:事件与命名空间从"待办"变为"默认能力",构成了多租户隔离与可观测性的基础。
对读者而言,这份报告既是理解 containerd 早期演进的一手史料,也是对照当前源码(pkg/oci、client、core/events、pkg/namespaces)观察"设计如何延续与落地"的最佳入口。
【免费下载链接】containerdAn open and reliable container runtime项目地址: https://gitcode.com/GitHub_Trending/co/containerd
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考