镜像是容器的基础,如今有很多用户在实践使用 Harbor 作为镜像存储与分发方案,本文介绍了 Harbor 在支持镜像加速方面的能力,以及 Nydus 这种改进的镜像格式,用于解决镜像在网络,存储,端到端可信方面的问题。使用 Harbor 结合按需加载,P2P 分发与预热技术加速镜像拉取,可以将容器启动时间从分钟级降低到秒级。
OCI 镜像格式
OCI 容器镜像格式是由 tar/tar.gz/tar.zstd
等格式的多层数据组成的,在容器创建时 Containerd 等组件需要将所有层分别下载并且解压到目录下,再用 OverlayFS 这类文件系统按目录顺序堆叠起来,提供给容器一个只读的 RootFS 文件系统。这样的分层设计可以在多个镜像间复用数据,做到增量构建,但同时它也有很多局限。
数据全量下载
下载并解压镜像层是一个非常耗时的过程,有篇 研究[1]表明,镜像拉取操作所需的时间占容器启动时间的 76%,但只有约 6.4% 的数据在运行时被容器读取,这意味着大量可能用不到的数据会占用网络带宽而拖慢启动速度。
元数据更新成本高
镜像中文件的元数据和数据是以 tar 格式放在同一层的,因此文件名、权限位等元数据的改动,会导致整个镜像层的 Hash 变化。例如一个 10GB 的层,重命名了其中一个文件,即使文件数据部分没有更新,也会导致整个镜像层在构建阶段重新压缩、上传、存储以及在运行时重新下载。
不被察觉的冗余数据
在 Dockerfile 指令中删除文件时,根据 OCI Image Spec[2],会在 Upper 层生成一个 Whiteout 特殊空文件表示删除,而修改(甚至只修改元数据)文件,会在 Upper 层生成完整的新文件。这两种情况下,旧版本文件还存在于 Lower 层,在运行时,用不到的旧版本数据依然会被下载和解压,镜像越来越复杂时这样的问题很难察觉,导致镜像体积很难优化。
数据去重效率低
镜像只能以层的粒度,以 Hash 比较的方式在存储和下载中去重,但在文件内、层内、层与层之间、镜像与镜像间等多个维度上都可能存在重复数据,镜像越复杂,问题越明显。
本地镜像数据不可信
镜像下载后可以通过 Hash 校验一致性,但在容器启动后,访问镜像数据过程中不会再次校验数据完整性,无法知道 RootFS 中的文件是否被篡改,这导致镜像在运行时是不可信的。
重构 OCI 镜像格式
要解决上述数据全量下载的问题,一种思路是按需加载,即容器启动时只拉取需要读取的部分数据,而不是整个镜像层。但传统镜像层都是先用 tar 格式打包,再使用 gzip/zstd
等压缩算法压缩整个 tar 数据,无法直接做到随机访问的解压。
这里简单介绍一下 Nydus 镜像加速框架[3],Nydus 基于高性能只读文件系统格式 EROFS[4] 扩展出了 RAFS(Registry Acceleration File System) 文件系统格式,它将 gzip/zstd 镜像层,转换为 RAFS 数据和元数据两大部分。
镜像构建
RAFS 文件数据部分,按指定 Size(如 1MB)大小切割成小块数据(Chunk),例如一个 10.5MB 的文件会切割成 11 个小的 Chunk,将每个 Chunk 块使用
Zstd/LZ4
等方式单独压缩,然后聚合在一起打包到一个文件(Blob)中,在打包过程中可以根据 Chunk Hash 或外部 Chunk 字典做去重,以降低镜像的增量尺寸;RAFS 文件元数据部分,全部存放在一个元数据(Metadata)文件中,除了文件元信息外,它还记录了文件有哪些 Chunk,Chunk 的 Hash 值,以及在上述 Blob 中的偏移位置(Offset);
另外,Nydus 镜像层始终比 OCI v1 镜像多了一层 Overlayed Metadata,这层 Metadata 是所有层的元数据堆叠,它通常很小,例如几十 GB 的镜像,Overlayed Metadata 通常有 10MB。
镜像运行
Nydus 支持 FUSE 用户空间和内核态文件系统两种方式来实现按需加载,以 FUSE 实现为例,镜像拉取过程如下:
容器创建阶段,由 Containerd 配合 Nydus Snapshotter 组件先拉取镜像的 Overlayed Metadata 层,这层很小速度较快;
Nydus Snapshotter 组件启动 Nydus Daemon(Nydusd)进程,创建 FUSE 挂载点作为容器 RootFS 目录,此时容器就可以启动了;
容器启动后读取数据时,Nydusd 处理容器 RootFS 发起的 FUSE read 请求,根据 Metadata 层信息,从远端镜像中心拉取部分 Chunk 数据,然后解压,校验,缓存到本地并返回给容器。
Nydus 通过重构镜像格式,可以解决上述传统 OCI 镜像的诸多问题,且能够做到 OCI 生态的兼容,另外 Nydus 也支持了内核态的 EROFS + FSCache[5] 按需加载方案,有更好的性能和更低的开销。下图为容器端到端的启动时间测试,传统 OCI 镜像随着镜像尺寸增大耗时明显增高,Nydus 镜像可以保持平稳。
引入 P2P 的按需加载
Harbor 集成的 Distribution[6] 支持 HTTP Range[7] GET 读取镜像层的部分小块数据以支持按需加载,很多业务容器在启动阶段就会读取大量镜像数据,或者在服务期间读取大量数据,这会发起大量小块 HTTP IO 请求,另外容器流量到来时再做按需加载会对在线业务有性能影响。为了解决这些问题,Nydus 支持按需时后台线程预取数据,以及小块 IO 请求的合并,可以降低 HTTP 请求量。
在大规模部署场景下,尤其是例如 AI 模型镜像,大量 HTTP 请求还是会给单点镜像中心带来压力,集群请求延迟增高。Dragonfly[8] 是基于 P2P 的文件分发和镜像加速框架,能够提高大规模文件传输的性能,最大限度的利用节点之间的闲置带宽,减少镜像中心回源流量,它也支持在首次 Range GET 请求后在节点间预热整个 Blob 数据。从来自蚂蚁集团大规模部署实践来看,Nydus 与 Dragonfly 架构支撑了每日百万级别的加速镜像容器创建,还没有遇到业务报告因为按需加载而导致的性能抖动。
Harbor 镜像转换服务
Harbor 可以很好地支持镜像加速格式,并且也支持了 P2P 预热[9],在用户 Push 镜像后触发 P2P 服务为集群预热镜像数据,提高部署时镜像数据分发速度。加速镜像格式和普通镜像格式不同,构建或转换步骤是必须的,可以使用 Buildkit[10] 从 Dockerfile 直接构建加速镜像,也可以使用 Nerdctl[11] 或 Nydusify[12] 转换工具。为了让 Harbor 进一步支持用户透明地使用加速镜像,Harbor 的子项目 Acceld[13] 诞生了,Acceld 为 Harbor 提供了自动转换加速镜像的能力。
Acceld 是一个通过 Harbor Webhook 扩展的服务,它是一个通用的加速镜像转换框架。当用户推送镜像时,Harbor 向该服务发送 Webhook 请求,通过其集成的 Nydus、eStargz 等转换驱动完成镜像的转换。
Acceld 服务缓存
在用户 Push 镜像后,Acceld 创建加速镜像转换任务,它需要先拉取原始镜像数据,转换完成后将加速镜像 Push 回 Harbor,Acceld 支持本地与远端两种缓存:
本地缓存:用于缓存拉取后的源镜像层,避免相同源镜像层的重复拉取;
远端缓存:用于缓存转换后的镜像层,避免相同镜像层的重复转换/推送。远端缓存实际上也是一种镜像,它使用类似 LRU 的方式在远端记录最近转换过的 layers,下次转换时根据转换过的缓存记录就可以跳过重复层的转换,以提高转换速度。
下表是 wordpress 镜像前后两次转换的性能数据,第二次是基于 wordpress 镜像添加增量层(几 MB 数据)构建的新镜像,没有本地缓存,只命中远端缓存的情况:
Acceld 垃圾回收
作为常驻服务,Acceld 有自己的本地镜像缓存垃圾回收,它使用 Containerd 的租约[14]管理机制(但并不依赖 Containerd 服务),能够按照 LFRU 的(先 LFU 再 LRU)顺序清理缓存,优先删除不常用的 Upper 镜像层数据。
Acceld 可扩展性
Acceld 提供了一个内置的可扩展 Driver 方法,允许支持其他类型的镜像格式转换,框架会处理 Pull,Push 及 Cache 操作,格式提供商只要实现一个 Drvier 接口[15]。
OCI Reference Type
OCI 镜像设计原则是 Immutable 的,也就是当 Tag & Push 一个镜像后,理论上不应该再次修改镜像内容后覆盖(虽然可以这么做)。Cosign[16]/Notation[17] 项目可以为镜像签名并验证镜像完整性,在之前这类附件只能作为单独的 Tag Push 到镜像中心,这会污染镜像列表,也需要跟踪它和原始镜像的关联关系,在 Harbor 中是特殊处理了这类附件的,在 UI 展示中将签名标记为一个图标,并处理了诸如镜像复制等操作,让签名始终跟随着镜像。
对于以上类似用例,在 OCI Image Spec v1.1 中,新增了 Reference Type 的支持,在附件 Manifest 上使用 subject[18] 字段记录原始镜像 Manifest 的 Digest,能够让构件 Manifest 间产生关联。另外可以使用 OCI Distribution Spec v1.1 中定义的 Referrers API[19] 发现镜像关联了哪些附件 Manifest,这两个特性都已被 Harbor 支持。
这对于镜像加速非常有用,在很多情况下加速镜像是由普通镜像转换而来,或者用户需要同时保留两种版本镜像,用于非按需场景的 Fallback,或镜像加速的方案过渡。Harbor v2.9 版本中也为 Nydus 更好地支持了 Reference Type:
原始镜像关联的 Nydus 镜像,在 Harbor UI 上会被级联展示并显示 Nydus 镜像图标,这让镜像列表更加干净,且关联关系一目了然;
删除原始镜像时,Harbor 会自动删除关联的 Nydus 镜像,这样用户不再需要考虑如何同时 GC 两种镜像。
在使用 Nydus Snapshotter[20] 运行普通镜像时,它可以使用 Harbor 提供的 Referrers API[21] 自动发现是否有关联的 Nydus 镜像,如果有则直接运行,实现用户无感的镜像加速。
无需转换的加速镜像格式
上面提到的按需加载方案需要重新制作镜像格式,这给镜像的转换和存储都带来了额外开销。实际上 Gzip 也是可以随机访问压缩的,但需要构建一个解压寻址索引,参考 zran[22] 和 SOCI[23] 实现,Nydus 也支持了 zran 索引格式,它的特点是:
分析 gzip 镜像层生成解压寻址索引,而不是转换整个镜像层格式,这比完整转换一个镜像要快很多;
生成的索引很小,不会生成额外格式的数据,可以节省镜像中心大量存储,不用存储两份数据。
以 node:19.0
镜像为例,可以看到 Zran 索引镜像要小很多,转换耗时也比 Nydus Native 镜像更低,由于 Zran 有部分读放大,运行时按需加载的数据量要偏大,启动耗时会高一点,但这相比普通 OCI 镜像依然有优势,足以应对大多数场景。
Acceld 也实现了 Nydus Zran 镜像格式的转换[24],通过配合本地与远端缓存,有更好的转换速度。
镜像加速性能收益
我们引用了 Dragonfly 文档[25]中的测试数据,在单机上测试了不同语言镜像运行版本命令的容器启动时间,例如 Python 镜像运行启动命令为 python -V
:
OCI v1:使用 Containerd 直接拉取 OCI 普通镜像并启动容器的耗时;
Nydus Cold Boot:使用 Containerd 拉取 Nydus 镜像,没有命中任何缓存并且启动容器的耗时;
Nydus & Dragonfly Cold Boot:使用 Containerd 通过 Nydus + Dragonfly P2P 拉取镜像,没有命中任何缓存并且启动容器的耗时。
测试表明使用 Nydus 与 Dragonfly P2P 后,相对于 OCI 普通镜像能够有效减少镜像下载时间,有缓存(Dragonfly Peer Cache & Nydus Cache)的情况下效果更好,这在大规模集群情况下更加明显,以下是字节跳动批量扩容 Nginx Pod 的测试数据,可以看到 OCI 普通镜像启动耗时随着并发量越大扩容越慢,但结合 Nydus 与 Dragonfly 在性能上无明显影响:
Harbor 镜像加速工作流
基于上述基础,一个完整的镜像加速工作流如下:
用户使用 Docker/Buildkit 等构建工具 Push OCI 普通镜像(Step 1);
Harbor Acceld 帮助自动转换为加速镜像 & 使用 P2P 服务预热镜像,参考 Acceld & Harbor Preheat(Step 2, 3, 4);
运行时 Snapshotter 运行加速镜像,并通过 P2P 加速按需拉取,参考 Nydus Integration[26](Step 5, 6);
当然这不是唯一的镜像加速工作流解决方案,它的目标是做到自动化与用户无感,使用这一基础设施能够让用户镜像无缝切换到加速版本,作为一种实践参考。
参与社区
Harbor:https://goharbor.io/
Harbor Acceld:https://github.com/goharbor/acceleration-service
Dragonfly:https://d7y.io/
Nydus:https://nydus.dev/
参考链接
[1]研究:https://www.usenix.org/node/194431
[2]OCI Image Spec:https://github.com/opencontainers/image-spec/blob/main/layer.md#representing-changes
[3]Nydus 镜像加速框架:https://nydus.dev/
[4]EROFS:https://docs.kernel.org/filesystems/erofs.html
[5]EROFS + FSCache:https://github.com/dragonflyoss/image-service/blob/master/docs/nydus-fscache.md
[6]Distribution:https://github.com/distribution/distribution
[7]Range:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
[8]Dragonfly:https://d7y.io/
[9]P2P 预热:https://goharbor.io/docs/2.9.0/administration/p2p-preheat/
[10]Buildkit:https://github.com/moby/buildkit
[11]Nerdctl:https://github.com/containerd/nerdctl/blob/main/docs/nydus.md
[12]Nydusify:https://github.com/dragonflyoss/image-service/blob/master/docs/nydusify.md
[13]Acceld:https://github.com/goharbor/acceleration-service
[14]租约:https://github.com/containerd/containerd/blob/main/docs/garbage-collection.md
[15]Driver 接口:https://github.com/goharbor/acceleration-service/blob/main/docs/development.md#driver
[16]Cosign:https://github.com/sigstore/cosign
[17]Notation:https://github.com/notaryproject/notation
[18]subjece:https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest
[19]Referrers API :https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
[20]Nydus Snapshotter :https://github.com/containerd/nydus-snapshotter
[21]自动发现:https://github.com/containerd/nydus-snapshotter/blob/1e090da3b512feaa933ee46a5db2019ac32aa0ed/misc/snapshotter/config.toml#L109C8-L109C8
[22]zran:https://github.com/madler/zlib/blob/master/examples/zran.c
[23]SOCI:https://github.com/awslabs/soci-snapshotter
[24]转换:https://github.com/goharbor/acceleration-service/blob/f902238337fe09d671861d2f2e6eb71e64c20e1a/misc/config/config.nydus.ref.yaml#L56
[25]文档:https://d7y.io/docs/next/setup/integration/nydus/#performance-testing
[26]Nydus Integration:https://github.com/dragonflyoss/image-service/blob/master/docs/containerd-env-setup.md
Nydus Star 一下✨:
https://github.com/dragonflyoss/image-service
参考链接
Nydus 加速镜像一致性校验增强
Nerdctl 原生支持 Nydus 加速镜像
Dragonfly 发布 v2.1.0 版本!
Dragonfly 中 P2P 传输协议优化