本文档描述了针对容器镜像gzip层(即OCI镜像规范中的application/vnd.oci.image.layer.v1.tar+gzip和Docker镜像规范中的application/vnd.docker.image.rootfs.diff.tar.gzip)的扩展方案,用于实现延迟拉取功能。该扩展方案命名为eStargz。
eStargz 是一款向后兼容的扩展程序,这意味着镜像可推送至与扩展无关的注册表,并在与扩展无关的运行时环境中运行。
本扩展基于谷歌CRFS项目提出的stargz(可寻址tar.gz的缩写,最初在Go社区讨论),并在此基础上扩展了分块级验证与运行时性能优化功能。
其标记规范遵循OCI镜像规范。
延迟拉取是一种拉取容器镜像的技术,旨在实现更快的冷启动。这使得容器无需等待整个镜像层内容在本地可用即可启动。相反,在容器运行期间,会按需获取层中的必要文件(或大文件的块)。
为实现这一点,运行时需要独立地获取和提取层中的每个文件。然而,没有eStargz扩展名的层不允许这样做,原因如下:
即便是获取单个文件条目,也需要提取整个层的二进制大对象。
没有为每个文件提供摘要,因此无法独立验证。
eStargz解决了这些问题,并支持延迟拉取。
此外,它支持文件的预取功能。这一功能可用于缓解因按需拉取每个文件而导致的运行时性能缺陷。
此扩展具有向后兼容性,因此eStargz格式的镜像可以推送到注册表,甚至可以在不支持eStargz的运行时环境中运行。

eStargz是一种gzip压缩的tar文件归档,包含文件和一个名为TOC(将在后面章节中描述)的元数据组件。在eStargz格式的二进制大对象中,每个非空的常规文件和每个元数据组件都必须单独用gzip进行压缩。这种结构继承自stargz。
因此,gzip头部必须位于以下位置。
二进制大对象的顶部
除目录表(TOC)外,每个非空常规文件tar条目的有效负载顶部
目录表(TOC)tar 头的顶部部
页脚的顶部(将在后面章节中描述)
eStargz二进制大对象中的大型常规文件可以拆分为多个较小的gzip成员。在本文档中,每个拆分后的成员称为“块”。因此,gzip头部可能位于以下位置。
非空常规文件条目的有效负载内的任意位置
采用eStargz格式的二进制大对象是这些gzip成员的拼接,它仍是一个有效的gzip二进制大对象。
eStargz包含一个名为TOC的常规文件,该文件记录了eStargz中所有文件条目的元数据(例如名称、文件类型、所有者、偏移量等),但不包括TOC自身的元数据。容器运行时可以使用TOC来挂载容器的文件系统,而无需下载整个层的内容。
TOC必须是一个JSON文件,作为最后一个tar条目包含在内,且文件名必须为 stargz.index.json。
以下字段包含构成TOC的主要属性。
version int
此必填属性包含目录(TOC)的版本。该值必须为1。
entries array of objects
此属性必须包含一个由所有 tar 条目和 blob 中的块组成的 TOCEntry 数组,但不包括 stargz.index.json。
TOCEntry 包含 eStargz 中文件或数据块的元数据。若文件 TOCEntry 中的元数据与对应的 tar 条目存在差异,则应优先遵循 TOCEntry 的记录。
以下字段包含构成 TOCEntry 的主要属性。除 chunkDigest 之外的属性均从 stargz 继承而来。
name string
此必填属性包含tar条目的名称。它必须是存储在tar文件中的完整路径。
type string
此必填属性包含tar条目的类型。它必须是以下类型之一。
dir: 目录
reg: 普通文件
symlink: 符号链接
hardlink: 硬链接
char: 字符设备
block: 块设备
fifo: fifo
chunk: 常规文件数据块如上文所述,常规文件可划分为多个数据块。每个数据块都必须创建TOCEntry条目。该文件首个数据块的TOCEntry类型必须为reg。第二个及之后的数据块TOCEntry类型必须为chunk。chunk类型的TOCEntry必须设置offset、chunkOffset和chunkSize属性。
size uint64
此可选属性包含常规文件的未压缩大小。非空的reg文件必须设置此属性。
modtime string
此可选属性包含tar条目的修改时间。空值表示零或未知。否则,该值采用UTC RFC3339格式。
linkName string
此可选属性包含链接目标。符号链接(symlink)和硬链接(hardlink)必须设置此属性。
mode int64
此必填属性包含权限和模式位。
uid uint
此必填属性包含此文件所有者的用户ID。
gid uint
此必填属性包含此文件所有者的组ID。
userName string
此可选属性包含所有者的用户名。
groupName string
此可选属性包含所有者的组名。
devMajor int
此可选属性包含设备文件的主设备号。字符文件和块文件必须设置此属性。
devMinor int
此可选属性包含设备文件的次设备号。字符文件和块文件必须设置此属性。
xattrs string-bytes map
此可选属性包含tar条目的扩展属性。
digest string
此可选属性包含常规文件内容的摘要。
offset int64
此可选属性包含常规文件或数据块在 blob 中的 gzip 标头的偏移量。非空的 reg 和 chunk 的 TOC 条目必须设置此属性。
chunkOffset int64
此可选属性包含此数据块在解压后的常规文件有效负载中的偏移量。块类型的目录条目(TOCEntries)必须设置此属性。
chunkSize int64
此可选属性包含此数据块的解压大小。reg文件中的最后一个chunk,或者未分块的reg文件,必须将此属性设置为零。其他reg和chunk必须设置此属性。
chunkDigest string
此可选属性包含此数据块的摘要。非空的reg和chunk的目录条目(TOCEntries)必须设置此属性。此属性可用于验证数据块的数据。
innerOffset int64
此可选属性表示“reg”或“chunk”条目的有效负载在流中的未压缩偏移量,该偏移量从offset字段开始计算。
innerOffset 允许在一个从 offset 开始的 gzip 流中放入多个“reg”或“chunk”有效负载。此字段支持以下结构。

此字段的应用场景是 ctr-remote 的 --estargz-min-chunk-size 标志。该标志的数值代表单个 gzip 流中必须写入的数据最小字节数。若该值 >0,则可将多个文件和数据块写入单个 gzip 流。预期结果为 gzip 头部更小且生成的二进制数据块体积更小。
在二进制大对象(blob)的末尾,必须附加一个页脚。该页脚必须是一个空的gzip成员,其额外字段(Extra field)包含二进制大对象中目录(TOC)的偏移量。此页脚必须为以下51字节(在 gzip 中,1字节=8位)。
- 10 bytes gzip 头部
- 2 bytes XLEN (额外字段的长度) = 26 (4 bytes 头部 + 16 hex digits + len("STARGZ"))
- 2 bytes 额外: SI1 = 'S', SI2 = 'G'
- 2 bytes 额外: LEN = 22 (16 hex digits + len("STARGZ"))
- 22 bytes 额外: subfield = fmt.Sprintf("%016xSTARGZ", offsetOfTOC)
- 5 bytes flate 头部: BFINAL = 1(last block), BTYPE = 0(non-compressed block), LEN = 0
- 8 bytes gzip 头部
(End of eStargz)运行时可以先读取并解析页脚以获取目录(TOC)的偏移量。
每个文件的元数据都记录在目录表(TOC)中,因此运行时只需使用文件元数据即可,无需提取归档的其他部分。若运行时需要获取普通文件的内容,可从目录表中获取该内容的大小和偏移量,从而在不扫描整个二进制数据块的情况下提取指定范围。结合OCI分发规范支持的HTTP范围请求,运行时可从注册表中选择性下载文件条目。
eStargz 的设计旨在兼容 gzip 压缩层。为此,eStargz 的尾部结构与 stargz 不兼容。eStargz 在尾部添加了 SI1、SI2 和 LEN 字段,使其符合 RFC1952 中额外字段的定义。目录(TOC)、条目(TOCEntry)以及 gzip 头的位置仍与 stargz 兼容。

懒加载会因按需获取每个文件而导致运行时性能开销。eStargz 通过支持预加载重要文件(称为Prioritized 文件)来缓解此问题。
eStargz 将 Prioritized 文件的信息编码到文件条目的顺序中,其中包含若干 Landmark 文件条目。
eStargz中的文件条目分为以下几组:
A. prioritized 文件
B. 非 prioritized 文件
如果没有文件属于A组,则归档文件中必须包含一个名为“no-prefetch landmark”的标记(landmark)文件。
如果一个或多个文件属于 A 组,那么 eStargz 必须包含与这些组对应的两个分离区域,且一个名为 “prefetch landmark” 的标记(landmark)文件必须位于这两个区域的边界处。
在 eStargz 中,标记文件必须是一个常规文件条目,其内容为 4 位的 0xf。它必须作为一个 TOCEntry 记录到 TOC 中。预取标记必须命名为 .prefetch.landmark 。非预取标记必须命名为 .no.prefetch.landmark 。
Stargz 快照器利用 eStargz 的优先级(prioritized)文件进行基于工作负载的优化,以减少读取文件的开销。镜像的工作负载是Dockerfile中定义的运行时配置,包括入口点命令、环境变量和用户。
Stargz快照器提供了一个镜像转换命令 ctr-remote images optimize 用于创建经过优化的eStargz镜像。在转换镜像时,该命令会在沙箱环境中运行指定的工作负载,并记录所有文件的访问情况。此命令将所有被访问的文件视为优先级文件。然后,它通过以下方式构建eStargz:
将优先级文件从归档的顶部放入,并按访问顺序对它们进行排序,
将预取标记文件条目放在此范围的末尾,并且
将所有其他文件(非优先级文件)放在预取标记之后。
在运行容器之前,stargz 快照器会通过注册表支持的单个 HTTP 范围请求,预取并预缓存包含优先级文件的范围。这可以提高指定工作负载的缓存命中率,并减少运行时开销。
eStargz 中内容验证的目标是,基于计算出的摘要,确保所有文件已下载的元数据和内容与预期一致。镜像中其他组件(包括镜像清单)的验证不在 eStargz 的范围内。在 eStargz 层的验证步骤中,我们假设引用此 eStargz 层的清单已经过验证(例如,使用摘要标签等)。

非 eStargz 层可以通过重新计算摘要,并将其与经过验证的清单中引用该层的层描述符中所写的摘要进行比较来验证。然而,eStargz 层是从注册表中以文件(如果文件较大,则以块)为粒度延迟拉取的,因此每次获取时都需要对每个文件(或块)进行独立验证。
以下描述了如何使用经过验证的清单对 eStargz 进行验证。
eStargz 由以下需要验证的组件构成:
TOC(该层中包含的所有文件的一组元数据)
每个常规文件的内容块
TOC 包含 blob 中所有文件和块的元数据(名称、类型、模式等)。挂载 eStargz 时,文件系统会从注册表中获取 TOC。为了使用经过验证的清单来验证 TOC,我们定义了一个注释 containerd.io/snapshot/stargz/toc.digest。该注释的值是 TOC 的摘要,并且该摘要必须包含在引用此 eStargz 层的描述符中。通过这个注释,文件系统可以通过重新计算摘要并将其与注释值进行比较来验证 TOC。
每个文件的元数据都被编码为目录(TOC)中的一个目录项(TOCEntry)。对于常规文件的每个块,也会创建目录项。为了使用经过验证的清单来验证每个文件和块的内容,目录项有一个 chunkDigest 属性。chunkDigest 包含 reg 项或 chunk 项的内容摘要。如上所述,可使用特殊注释来验证目录。借助经过验证的目录中所写入的 chunkDigest 字段,可通过重新计算摘要并将其与该属性进行比较,来独立验证每个文件和块。
综上所述,eStargz 必须包含以下元数据:
在引用 eStargz 层的描述符中的 containerd.io/snapshot/stargz/toc.digest 注释:其值为目录(TOC)的摘要。
非空注册项或数据块 TOCEntry 的 chunkDigest 属性:该值为文件或数据块内容的摘要值。
Stargz Snapshotter 通过上述元数据验证 eStargz 层。如前所述,其他镜像组件(包括清单文件)的验证超出 Snapshotter 的范围。当该 Snapshotter 挂载 eStargz 层时,必须预先验证引用该层的清单文件,并将验证后清单中写入的 TOC 摘要注释传递至该 Snapshotter 。
挂载层时,stargz Snapshotter 会从注册表中获取TOC。然后,它通过重新计算摘要并将其与清单中写入的摘要进行比较来验证TOC。TOC验证通过后,Snapshotter 会使用TOC中记录的元数据来挂载该层。
在容器运行期间,此 Snapshotter 会延迟获取常规文件内容的块。在将块提供给文件系统用户之前,Snapshotter 会重新计算摘要,并检查该摘要是否与相应的 TOCEntry 中记录的摘要匹配。
此可选功能允许将 TOC 分离到另一个名为 TOC 镜像的镜像中。这种类型的 eStargz 与普通的 eStargz 相同,但在层 blob 中不包含 TOC JSON 文件(stargz.index.json),并且有一个特殊的页脚(Footer)。此功能通过避免在该 blob 中包含 TOC JSON 文件,能够创建更小的 eStargz blob。
页脚(Footer)具有以下结构:
// 页脚是一个空的gzip流,没有压缩,且带有一个额外头部。
//
// 46 comes from:
//
// 10 bytes gzip header
// 2 bytes XLEN (length of Extra field) = 21 (4 bytes header + len("STARGZEXTERNALTOC"))
// 2 bytes Extra: SI1 = 'S', SI2 = 'G'
// 2 bytes Extra: LEN = 17 (len("STARGZEXTERNALTOC"))
// 17 bytes Extra: subfield = "STARGZEXTERNALTOC"
// 5 bytes flate header
// 8 bytes gzip footer
// (End of the eStargz blob)TOC 镜像指的是包含 TOC 的 OCI 镜像。每个层的根目录中都有一个 TOC JSON 文件(stargz.index.json)。
清单中的层描述符必须包含一个注释 containerd.io/snapshot/stargz/layer.digest。该注释的值是与该 TOC 对应的 eStargz 层 blob 的摘要。
以下是TOC镜像中的一个示例层描述符。该层(sha256:64dedefd539280a5578c8b94bae6f7b4ebdbd12cb7a7df0770c4887a53d9af70)在根目录中包含TOC JSON文件(stargz.index.json),可用于摘要为 sha256:5da5601c1f2024c07f580c11b2eccf490cd499473883a113c376d64b9b10558f 的 eStargz层 blob。
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:64dedefd539280a5578c8b94bae6f7b4ebdbd12cb7a7df0770c4887a53d9af70",
"size": 154425,
"annotations": {
"containerd.io/snapshot/stargz/layer.digest": "sha256:5da5601c1f2024c07f580c11b2eccf490cd499473883a113c376d64b9b10558f"
}
}Stargz 快照器支持带有外部 TOC 的 eStargz。如果 eStargz blob 的页脚表明它需要 TOC 镜像,Stargz 快照器也会从注册表中拉取该镜像。
Stargz snapshotter 工具默认 TOC 镜像的引用名称与eStargz 相同,且带有 -esgztoc 后缀。例如,若 eStargz 镜像命名为 ghcr.io/stargz-containers/ubuntu:22.04-esgz,stargz 快照工具将从 ghcr.io/stargz-containers/ubuntu:22.04-esgz-esgztoc 获取 TOC 镜像。需注意:stargz snapshotter 未来版本将支持更多目录表镜像检索方式(例如允许自定义后缀、使用 OCI 引用类型等)。
一旦 stargz snapshotter 工具获取到 TOC 映像,它会通过查找 containerd.io/snapshot/stargz/layer.digest 注释,尝试定位与挂载的 eStargz blob 对应的 TOC。如上所述,获取到的 TOC JSON 将通过 containerd.io/snapshot/stargz/toc.digest 注释进行验证。
以下是一个示例目录(TOC)JSON:
{
"version": 1,
"entries": [
{
"name": "bin/",
"type": "dir",
"modtime": "2019-08-20T10:30:43Z",
"mode": 16877,
"NumLink": 0
},
{
"name": "bin/busybox",
"type": "reg",
"size": 833104,
"modtime": "2019-06-12T17:52:45Z",
"mode": 33261,
"offset": 126,
"NumLink": 0,
"digest": "sha256:8b7c559b8cccca0d30d01bc4b5dc944766208a53d18a03aa8afe97252207521f",
"chunkDigest": "sha256:8b7c559b8cccca0d30d01bc4b5dc944766208a53d18a03aa8afe97252207521f"
},
{
"name": "lib/",
"type": "dir",
"modtime": "2019-08-20T10:30:43Z",
"mode": 16877,
"NumLink": 0
},
{
"name": "lib/ld-musl-x86_64.so.1",
"type": "reg",
"size": 580144,
"modtime": "2019-08-07T07:15:30Z",
"mode": 33261,
"offset": 512427,
"NumLink": 0,
"digest": "sha256:45c6ee3bd1862697eab8058ec0e462f5a760927331c709d7d233da8ffee40e9e",
"chunkDigest": "sha256:45c6ee3bd1862697eab8058ec0e462f5a760927331c709d7d233da8ffee40e9e"
},
{
"name": ".prefetch.landmark",
"type": "reg",
"size": 1,
"offset": 886633,
"NumLink": 0,
"digest": "sha256:dc0e9c3658a1a3ed1ec94274d8b19925c93e1abb7ddba294923ad9bde30f8cb8",
"chunkDigest": "sha256:dc0e9c3658a1a3ed1ec94274d8b19925c93e1abb7ddba294923ad9bde30f8cb8"
},
... (omit) ...原文地址:https://github.com/containerd/stargz-snapshotter/blob/main/docs/estargz.md