从 HELM Chart 成功对接容器镜像仓库开始,到近两年 OCI 社区 artifacts 标准化的落地推广,现在 OCI Registry as Storage 已经是事实标准,不同业务场景的数据存储都可以对接 OCI registry 的分发协议。同时,这几年容器镜像的安全供应链需求徒增,以及各大云厂商对容器镜像 lazy-loading 探索力度在加大,OCI 社区在 artifacts 标准的基础上为容器镜像引入 referrers 定义:在保证前后兼容的情况下,每一个容器镜像都可以通过关联的 artifacts 来拓展自身的属性,如下图所示,用户可以通过 referrers 关联特性为 net-monitor:v1 镜像提供了镜像签名和 SBOM 信息。当然,容器镜像 lazy-loading 属性也可以与之相关联,比如阿里云的 overlaybd,蚂蚁金服的 nydus 以及亚马逊的 soci

from-oras-referrers-demo

NOTE: 图片取自 Feynman Zhou 发表的 Azure Container Registry: the first cloud registry to support the OCI Specifications 1.1 文章。

可预见的是,各大云厂商的镜像仓库都会跟进这一标准,安全供应链以及容器镜像加速的增值服务也会因此得到大面积推广。而作为容器镜像的消费端,为了支持这一特性,containerd 社区的想法是引入组合型插件 Image Transfer Service,将镜像分发和打包处理都抽象成数据流的转发,并统一收编在服务端处理。

Pull 需要新的抽象

在 containerd 1.7 版本之前,镜像数据处理的绝大部分操作都集中在客户端。containerd 设计偏向于把服务端做薄,服务端仅做底层资源的 CRUD;而客户端则采用类似 Backends For Frontend 理念来为复杂流程封装接口,如下图所示 Pull 镜像的函数接口。下载镜像涉及到镜像地址解析、密钥管理、并发下载以及解压处理,但该流程仅适用于下载标准格式的镜像,任何调整都容易产生不适。

smart-client-pull

NOTE: containerd CRI 插件属于 containerd 进程的一部分,因此 CRI 插件在使用 Pull 函数接口时,镜像数据在 content/diff/snapshotter service 之间传递不走 grpc 协议,仅是内部函数调用。

让我们来简单回顾下当初 containerd 是如何支持容器镜像 lazy-loading 特性。

由于容器镜像打包采用了不可检索的 tar 格式,因此目前市面上开源的镜像 lazy-loading 方案都涉及到镜像格式的改造,比如阿里云基于块设备的 overlaybd、蚂蚁金服基于文件系统的 nydus,以及 containerd 社区基于可检索 gzip 构建的 stargz。而格式的变化意味着 tar 解压流程不适合这类镜像(上图里的 apply diff 阶段),为了适配这一特性,containerd 在 GA 之后首次修改了 snapshotter 元数据的管理逻辑 Support target snapshot references on prepare:即在解压镜像时,它会告知 snapshotter 插件这是为解压镜像而准备的可写层。

容器镜像的制作过程大多都是将容器的可写层提交成镜像的只读层,因此 containerd snapshotter 实现里并不会区分可写层是为谁准备的。而上述特性可以让 lazy-loading snapshotter 感知到这是在下镜像阶段,它可以通过返回 已存在 错误来告知客户端:该镜像层已经下载并解压过了,请处理下一层吧。在不大改流程的情况下,这也算是支持 lazy-loading 特性。

但这种方案仅支持匿名用户访问的镜像。基于安全的考虑,下镜像过程并不会将用户密钥信息传递给 snapshotter;而 lazy-loading 需要时刻保证数据可访问,否则业务容器在读取 rootfs 时将会出现 IO 阻塞。直到目前为止,containerd 社区依旧在讨论如何管理用户密钥信息: Enable remote snapshotter to receive creds from containerd CRI plugin。除此之外,容器镜像 lazy-loading 特性还要求用户手动替换或者集群管理员通过 webhook 来修改 pod 里的镜像信息。从产品侧的角度看,这种端到端的集成体验极差。

安全供应链相关的特性也面临相同问题:Datadog 开发者提出 Plugin to CRI for image digest verification,他们希望 containerd 能在下载镜像阶段进行镜像校验,而不是在集群的 webhook 里。但目前的 Pull 流程是无法感知镜像的辅助信息,更谈不上校验。考虑到镜像辅助信息的多样性,Pull 函数接口的 Optional 配置方法无法满足插件化需求。

Image Transfer Service

镜像数据的来源可以是镜像仓库、本地镜像 tar.gz 或者是本地解压后的存储等。不同的来源之间可以相互流动,比如下载流程可以理解成 镜像仓库 -> 本地对象存储 (containerd content service) -> 本地解压后的存储 (containerd snapshot service),相反的流转将变成上传镜像;甚至还可以通过简单的数据转发,实现镜像在不同仓库之间迁移。containerd 通过引入 Image Transfer service 来将 Pull/Push 抽象成数据迁移流程,而 transfer service 本身是插件化的,在相同数据流向的情况下,containerd 支持定制化开发。

type Transferer interface {
	Transfer(ctx context.Context, 
	    source interface{},
		destination interface{},
		opts ...Opt
	) error
}

overview-transfer-service

如上图所示,Transfer objects 代表着数据源,数据流向是 Registry 到 Image Store,目前已在 1.7 版本里支持的迁移能力如下表所示。其中数据转发过程可能会和客户端交互,比如推送当前的传输状态,或者客户端提供密钥信息做鉴权等等。这些交互过程都通过 streaming service 来实现: 客户端在发起数据传输时,它会向 streaming service 申请数据交互通道,这些通道将会共享给 Transfer object,并由具体的 Transfer object 实现来决定如何交互。

SourceDestinationDescription
RegistryImage Store“pull”
Image StoreRegistry“push”
Object stream (Archive)Image Store“import”
Image StoreObject stream (Archive)“export”
Image StoreImage Store“tag”

对于 lazy-loading 和安全供应链场景,镜像的数据流向应该如下表所示。在后续的实现里,fetch 操作将会负责拉取镜像相关的 referrers,并在目标数据源中做相应的处理,比如安全供应链的数据源会校验数据的合法性,而 lazy-loading 场景下的关注点将会是 如何通过 referrers 来获得真正的镜像格式,后续的进展可以关注 OCI Referrers API support

SourceDestinationDescription
RegistryContent Store(Object Stream)“fetch”
Content Store(Object Stream)Mount/Snapshot“unpack”
Content Store(Object Stream)Image Store“tag”

除此之外,镜像的密钥管理也将会插件化,只是现在如何同 lazy-loading snapshotter 交互还未确定,后续的进展可以关注 [transfer] credential manager plugin

统一镜像处理逻辑

虽然 containerd CRI 插件使用了 Pull 函数方法来处理镜像下载,但是它有不少优化特性,比如可根据流量来自动取消下载 feature: support image pull progress timeout,通过共享机制来避免重复下载 CRI: improve image pulling performance,或者是优化 lazy-loading snapshotter 集成体验 Export remote snapshotter label handler 等。这些处理在 nerdctl 也有类似的实现,transfer service 的出现可以很好的统一类似的处理逻辑。

需要说明的是,transfer service 出现并不意味着放弃客户端下载的支持,类似 buildkit 这样项目有自身镜像管理需求,transfer service 不一定适合它们,这些项目依旧会使用 containerd 提供的 Pull 函数接口。

把镜像下载到虚拟机里??

有部分厂家会使用机密计算和普通安全容器来提供多住户的 Kubernetes 产品,为了保证容器数据安全,这些场景有在 Guest OS 内下镜像的需求。为了支持这一特性,containerd ttrpc 支持了 streaming 能力 Introduce streaming。配合上 transfer service,containerd 2.0 大概率会把下载请求转发给 Guest OS 内的控制器,如下图所示。这部分转发逻辑也还在设计阶段,预计会在 Sandbox API 定稿后出提供。

pull-image-in-vm

最后

个人一直很喜欢 containerd 插件机制,所以整体下来会比较期待 transfer service 在插件管理上的演进。

这一篇就先这样吧,有问题欢迎私信。

下一篇打算聊一下 containerd 垃圾回收机制。