Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

一、雕花的三笔代价

上一篇最后停在一个略显扫兴的地方:eStargz 做到了"pull 近乎为零",但它有三笔躲不掉的代价——压缩率变差、去重能力弱、一层一个 FUSE 挂载点,外加一个 resolve 超时就静默回退全量拉取的隐患。

如果把这三条并排放在一起看,会发现它们是同一件事的三种表现:

  • 压缩率变差,是因为一层内部共用一个 gzip 字典。你想让单个文件能被独立解压,就必须把这个字典拆掉;
  • 去重能力弱,是因为层 blob 的 digest 就是寻址单位。两层只要差一个字节就是两个不同的对象,制品库和本地缓存都没有更细的粒度可用;
  • N 层 N 个挂载点,是因为层是叠加单位。容器 rootfs 由 overlayfs 把每层 lowerdir 摞起来,镜像有多少层就得挂多少次。

三个症状,一个病根:在 OCI 的世界里,"层"同时扮演了三个角色——压缩单位、寻址单位、叠加单位。

eStargz 选择不动这个模型,只在层的内部做文章。所以它能保持 100% 兼容,也注定只能拿到"层模型允许它拿到"的那部分收益。上一篇把这条路线称为"在旧格式里雕花",不是贬义——雕花是有意识的取舍,代价换来的是零生态改造。

但如果允许你把这三顶帽子摘下来重新分配呢?

这就是这一篇的主角。Nydus 的回答很直接:**压缩和寻址的单位降到 chunk,叠加的单位升到整个镜像。**中间这一层"层",在运行时被彻底架空了。

这一篇讲清楚它是怎么做到的,以及为此付出了什么。真实集群的部署形态、和 Dragonfly 的合流、怎么从日志确认懒加载真的在生效——留给第三篇。


二、Nydus -- 打破OCI的禁锢

Nydus 是 CNCF Dragonfly 项目下的子项目。本身可以拆成两件事:

  • 一套为镜像加速专门设计的只读文件系统格式:RAFS(Registry Acceleration File System);
  • 一个按需读取远端数据的运行时守护进程:nydusd。

把这两件事、以及节点上的组件放进一张图,就是 Nydus 的全景——左边是节点,右边是构建期与远端存储:

Nydus 全景:构建期转换、启动控制面与运行期数据面

图里 ①~④ 是控制面:containerd 把 snapshot 的活外包给 nydus-snapshotter,后者落地 bootstrap 并拉起 nydusd,nydusd 挂载出整个镜像的只读视图,overlayfs 再叠一个可写层交给 runc。注意 lowerdir 只有一个,和镜像有多少层无关。⑤~⑦ 是数据面,容器跑起来之后才发生,第五节展开。

两条启动路径对照着看:

1
2
传统 OCI: 逐层下载 → 逐层解压落盘 → overlayfs 叠成 rootfs → 启动
Nydus : 拉 bootstrap(几 MB)→ 挂载 RAFS → overlayfs 叠可写层 → 启动 → 运行期按需取 chunk

前者把大量 I/O 前置到启动阶段,后者把文件数据的读取延后到实际访问的那一刻。所以有一句话得先说清楚,它经常被误解:**Nydus 的启动加速不是"网络变快了",而是"启动不再等待所有数据下载完成"。**这两件事的区别,会在第六节变成一笔要还的账。


三、RAFS:一棵树 + 一堆块

RAFS 的核心设计只有一句话:元数据与数据彻底分离,且元数据是镜像级的。

一个 Nydus 镜像由两部分构成:

  • bootstrap:一份元数据文件,描述整个镜像的文件系统;
  • blobs:若干数据文件,装着被切成块的文件内容。

3.1 bootstrap:整个镜像只有一棵树

bootstrap 里存的是完整的文件系统元数据:目录结构、文件名、权限属主、大小时间戳,以及最关键的——每个文件由哪些 chunk 组成、每个 chunk 在哪个 blob 的哪个偏移上,再加上校验摘要。

这里有一个特别容易被忽略、但影响极其深远的细节:

bootstrap 里的这棵树,是所有层合并之后的结果。层与层之间的覆盖关系、whiteout(删除标记)、opaque 目录,全部在构建期就被消解掉了。

对比一下 OCI 的做法:镜像有 28 层,运行时就得靠 overlayfs 把 28 个目录摞起来,每次 lookup 一个文件都要从上往下逐层找,直到命中或确认被 whiteout 掉。Nydus 把这件事提前到了 nydusify 转换的时候——运行时拿到的已经是一棵拍平的、没有歧义的树。

这就是上一篇结尾那句"Nydus 把整个镜像合并成单一的文件系统视图,一个 rootfs 只需要一个挂载点"的物理依据。不是运行时做得更聪明,是问题在构建期就被解决掉了。

bootstrap 的体积通常是几 MB 到几十 MB 量级——对应的镜像可能有几十 GB。这个比例就是"pull 近乎瞬时"的全部原因:拉一个 Nydus 镜像,本质上只是把这几 MB 的元数据下载下来。

3.2 blobs:定长 chunk、内容寻址、各自独立压缩

文件内容不再以"层的 tar 流"形式存在,而是被切成定长的 chunk(Native RAFS 常见 1 MiB),每个 chunk:

  • 独立压缩——可以被单独解压,不依赖前后文;
  • 按内容寻址——chunk 的身份是它内容的摘要,不是它在哪一层、哪个文件里;
  • 可单独取回——知道偏移和长度,一个 HTTP Range 请求就能拿到。

注意这里和 eStargz 的一个微妙差别。eStargz 也做了切分(每个文件一个 gzip 流,大文件再按 4 MiB 切 chunk),但它的切分服务于"能不能 seek",chunk 的身份仍然依附于"它在哪个 layer 的哪个偏移"。Nydus 的 chunk 是内容寻址的:相同的内容,无论出现在哪个文件、哪一层、哪个镜像里,都是同一个 chunk。

顺带解决了上一篇提到的另一个问题:压缩算法不再被格式绑架。eStargz 因为要维持"整体仍是一个合法 .tar.gz",只能用 gzip;RAFS 是全新格式,没有这个包袱,可以用 zstd——同等压缩率下解压更快,或者同等速度下压得更小。上一篇说 eStargz "为了可 seek 而丢掉跨文件字典,导致 blob 变大",Nydus 在这一点上两头都占了一些。

3.3 和 eStargz 摆在一起看

把两种布局画出来,差别一目了然:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
eStargz(每层一个文件,索引在层内)

layer 1 ──▶ [gzip(A)][gzip(B)][gzip(C)] ... [gzip(TOC)][footer]
layer 2 ──▶ [gzip(D)][gzip(E)] ... [gzip(TOC)][footer]
layer 3 ──▶ [gzip(F)][gzip(G)][gzip(H)] ... [gzip(TOC)][footer]
▲
└─ 每层各有一份索引,只认识自己层内的文件


RAFS(一份镜像级索引 + 一堆内容寻址的块)

┌──────────────────────────────────────────────┐
│ bootstrap(几 MB) │
│ ├─ 合并后的完整文件树(whiteout 已消解) │
│ ├─ 文件 ──▶ chunk 列表 │
│ └─ chunk ──▶ (blob id, offset, 压缩长度, 摘要) │
└───────────────────┬──────────────────────────┘
│ 引用
┌─────────────┴─────────────┬─────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ blob 1 │ │ blob 2 │ │ blob 3 │
│ ▢▢▢▢▢▢▢▢ │ │ ▢▢▢▢▢▢▢▢ │ │ ▢▢▢▢▢▢▢▢ │
└───────────┘ └───────────┘ └───────────┘
定长 chunk · 内容寻址 · 各自独立压缩 · 可单独 Range 取回

一句话概括这张图:TOC 是层内索引,bootstrap 是镜像级索引。

这一个差别,衍生出了后面所有的优势——去重能不能跨层、rootfs 需要几个挂载点、缓存以什么为单位复用,追到底都是这一句。

注:RAFS 有两条挂载形态

RAFS v5 是私有元数据格式,只能由 nydusd 在用户态解析,走 FUSE;RAFS v6 把元数据布局做成与内核里的 EROFS 兼容,元数据可以直接由内核解析,数据面通过 fscache 按需回源,命中路径不必穿越用户态,随机小文件的 IO 路径明显更短。代价是 fscache 路径要求内核 ≥ 5.19,老内核只能退回 FUSE 模式。这个门槛在第六节还会再出现一次。


四、去重:把上一篇那个 3 GB 的例子算完

上一篇留了一个坑没填完,现在填上。

场景是这样的:团队同时维护 PyTorch 2.3 和 2.4 两个训练镜像,各自有一个 3 GB 的 CUDA 层,内容 99% 重叠。在 eStargz(以及任何标准 OCI)的世界里,只要构建产物有一个字节不同,两层的 digest 就不同,制品库和节点本地缓存都只能按整层区分——于是完整存两份 3 GB,哪怕其中几百 MB 的 libcudnn.so 一模一样。

更尴尬的是,eStargz 连镜像内部的重复内容都不去重:同一个文件在两个层里各出现一次,就实打实存两份。

Nydus 的 chunk 是内容寻址的,这件事的答案就变了。

4.1 镜像内去重:自动的

nydusify 转换时,相同内容的 chunk 天然只会落一份,bootstrap 里两处文件引用同一个 chunk 就行了。这一层收益不需要任何配置。

对训练镜像来说,这块的空间往往比直觉大:多阶段构建留下的重复依赖、pip install 反复装进不同虚拟环境的同一批 wheel、几个层里各拷一份的模型配置文件——它们在层模型下都是"不同层的不同文件",在 chunk 模型下就是同一个 chunk。

4.2 跨镜像去重:要配

跨镜像共享不是白送的,它依赖 chunk-dict:转换时指定一个参考镜像(通常是团队的基础镜像),nydusify 会优先复用字典里已有的 chunk,而不是新建。

配好之后,回到开头那个例子:PyTorch 2.3 和 2.4 共享同一套 CUDA / cuDNN 底座,那几百 MB 相同的 .so 在制品库里只有一份,节点上的 blobcache 里也只有一份。镜像矩阵越大,这个收益越接近线性放大——10 个训练镜像共享一套底座,省掉的就是 9 份底座的存储与回源流量。

顺带一提完整性:每个 chunk 都带摘要,bootstrap 自身也有摘要,读到的每一块数据都能验;RAFS v6 的 EROFS 路径上还可以叠 dm-verity 做块级校验。去重和校验在这个模型里是同一套机制的两面——都建立在"内容即身份"上。

4.3 但这里有个诚实的边界

跨镜像去重的收益,强依赖构建规范。

chunk 能不能对上,取决于两次构建产出的字节是否完全一致。如果各团队各建各的底座、基础镜像 tag 用 latest、pip install 不锁版本、编译产物里带时间戳或构建路径——那么"看起来一样"的两个 CUDA 层,chunk 摘要全都对不上,chunk-dict 命中率接近零。

这时候 chunk 级去重就只是一个纸面能力。

所以真要吃到这块收益,前置条件不在 Nydus,而在你们的构建流水线:统一基础镜像、锁定依赖版本、尽量做可复现构建。这件事的难度经常被低估——它不是装个组件,是要改一群人的习惯。


五、数据面:容器跑起来之后

容器按第二节那条路径起来了,但此刻本地磁盘上除了几 MB 的 bootstrap,几乎什么都没有——镜像数据仍然躺在远端。真正的按需加载从这一刻开始,也就是那张图的 ⑤~⑦。

5.1 一次 read 的完整旅程

1
2
3
4
5
6
7
8
9
10
容器进程 read()
└─▶ overlayfs:upperdir 没有,落到 lowerdir
└─▶ RAFS 挂载点(FUSE,或 v6 的 EROFS/fscache 路径)
├─ 页缓存命中 ────────────────────────▶ 直接返回
├─ blobcache 命中 ───────────────────▶ 解压后返回
└─ 都没命中
├─ 查 bootstrap:这个偏移落在哪个 chunk
├─ 向 backend 发 Range GET 取回该 chunk(可合并相邻 chunk)
├─ 校验摘要 → 解压
└─ 写入 blobcache + 页缓存 → 返回

5.2 为什么"合并相邻 chunk"很重要

上一篇讲过 eStargz 的一个天然矛盾:极致的按需 = 极致的网络往返次数。100 个文件串行读,就是 100 次 RTT。

Nydus 在这里有一个结构性优势:因为 chunk 是定长的、在 blob 里连续排布的,nydusd 发现要取的几个 chunk 在物理上相邻时,可以合并成一个 Range 请求一次取回;顺序读大文件时,还能顺势做预读。

而 eStargz 的回源单位是"某个文件的那段 gzip 流"——文件之间在物理上是否相邻,取决于 tar 的排布,它没有一个稳定的合并粒度可用。这也是为什么 eStargz 必须靠构建期物理重排 + landmark 来解决这个问题:把启动期文件挪到一起,用一个大 Range 拉走。

两种预取思路的差别值得对照一下:

  • eStargz:靠物理重排。构建时 profiling 一遍,把启动期文件在 tar 流里挪到最前面,运行时一个大 Range 拉整段。代价是这个顺序被烧死在镜像里,profiling 不准就白搭。
  • Nydus:靠元数据指示。bootstrap 里可以带预取表,转换时指定要预取的目录或路径模式,nydusd 启动后用独立线程在后台批量拉。数据的物理布局不用动,预取策略是一份可以调的配置。

5.3 和 eStargz 的五处关键差异

把两条路线的数据面并排放一起,差异集中在五个地方:

eStargz Nydus
挂载模型 N 层 → N 个 FUSE 挂载 + overlayfs 叠加 整镜像一个合并挂载,与层数无关
回源单位 某文件对应的一段 gzip 流 RAFS chunk,可合并相邻 chunk 预读
解压与命中路径 用户态 FUSE + gzip 用户态 FUSE(v5)或内核态 EROFS + fscache(v6);zstd
缓存定位 按 layer blob 的字节范围 按 chunk(内容寻址)
内容复用 仅整层 blob 级,跨文件跨层都不去重 chunk 级,镜像内自动、跨镜像靠 chunk-dict
未访问的数据 永不下载 永不下载

最后一行是两者的共同点,也是懒加载的全部意义所在。

但这张表也藏着一个没写出来的东西:**上面每一次"未命中",都是一次挂在网络上的读。**这条路径有多漂亮,第六节的账单就有多具体。


六、代价:Nydus 的"缺"

Nydus 不是没有代价,它的代价有一个统一的名字:延迟付费。

传统 pull 是一次性把账付清——下载、解压、占盘,付完之后容器和网络就没关系了。Nydus 把这笔钱分期了:启动时只付几 MB 的首付,剩下的在运行期按 miss 次数分期还。

下面每一个坑,追到底都是这个模型的连带反应。

6.1 首读放大:钱没消失,只是挪了位置

先说一组我们自己在 POC 阶段的实测趋势(脱敏为倍数,具体数字与镜像信息不便公开,你们应当在自己的灰度节点上重测):

  • 拉取 + 启动:Nydus 比原生 OCI 快约一个数量级,镜像越大优势越明显;
  • 冷 blobcache 下的密集首读(把镜像里几十 GB 数据完整读一遍):Nydus 反而慢约一倍;
  • 缓存热起来之后:两者差距明显收敛。

第二条经常出乎意料,但它完全合乎逻辑:在 pull 阶段没付的成本,在 read 阶段按 miss 分期付了——每次未命中都要一次 Range 回源、一次校验、一次解压,还要写一遍 blobcache。原生 OCI 是顺序地、批量地、无竞争地把数据一次性拉完解完;懒加载是随机地、零散地、带网络往返地慢慢补。

由此有一个直接的推论,也是选型时最该问自己的问题:

如果你的任务"启动即读满整个镜像",懒加载的收益会被吃掉一大块。

上一篇第八节讲过这个边界:推理服务启动第一件事就是把 25 GB 权重全部 load 进显存,这部分是必须全量读的,"懒"不起来。训练场景要看具体形态——如果第一个 step 之前就要把数据集和权重扫一遍,那你只是把等待从 Pulling 挪到了第一个 step 之前,端到端未必赚。

6.2 运行时依赖变重

nydusd 是一个常驻进程,而且它的生命周期有一个反直觉的绑定关系:

nydusd 的生死绑在容器的 active snapshot 上,不是绑在镜像上。

容器 Prepare 时它出生,容器 Remove 时它消亡。这带来几个实践中会真的遇到的问题:

  • 强删 Pod 会留下孤儿。 kubectl delete --force 打断了 snapshotter 的 Remove 流程,"卸载挂载点 + 停 daemon"这一步没执行,于是留下一个孤儿 nydusd。它设计上就是"有人显式销毁才退",自己不会超时退出。节点上跑久了,孤儿进程会一个个攒起来。
  • 直接 kill 没用。 snapshotter 有 daemon 恢复机制,配置为恢复/重启策略时,它会依据自己的账本把被 kill 掉的 daemon 拉回来。要彻底清掉,得从最上游删 snapshot(crictl rmp 或 ctr snapshots rm),让 snapshotter 走一遍正常的销毁流程。
  • 排障链路变长了。 容器读文件报错,可能是应用的问题、overlayfs 的问题、nydusd 的问题、blobcache 的问题、backend 的问题、凭据过期的问题——原来只有前两种可能。

6.3 blobcache 不属于快照

第二个坑同样来自生命周期错配:blobcache 独立于快照生命周期。

  • 删容器、删快照,都不会清缓存。缓存目录会随着节点上跑过的镜像越来越多而持续增长,需要单独的容量治理策略。
  • 反过来,在 daemon 还活着的时候手动 rm 缓存目录,会直接出事:文件被 unlink 但 fd 还开着,nydusd 的内存态和磁盘态不再自洽,容器侧表现为 input/output error。

正确的顺序是:先让相关 daemon 全部退出,再清缓存。这条规则在一次半夜的"磁盘满了赶紧删点东西"里特别容易被违反。

监控上要多加两个指标:blobcache 容量增长曲线和缓存命中率。前者决定你什么时候要扩盘或清理,后者决定 8.1 那笔首读的账到底有多贵。

6.4 产物非原生 OCI:没有安全网

这是 Nydus 和 eStargz 之间最本质的工程差别。

eStargz 有一张安全网:产物仍是合法的 OCI 层,不认识 eStargz 的运行时会把它当普通镜像全量拉取,退化但能跑。你上错了节点、灰度漏了一台、回滚忘了改配置,最坏结果是"没加速"。

Nydus 没有这张网。转换后的镜像不是原生 OCI 层,没有 Nydus 运行时的节点直接起不来。这意味着:

  • 灰度必须按节点分组做,调度策略要跟上,不能让引用 Nydus 镜像的 Pod 落到没装组件的节点;
  • 制品库里通常要同时维持原生镜像和 Nydus 镜像两份,构建流水线多一个转换环节,存储成本和构建时长都要加;
  • 安全扫描、镜像签名、准入策略这些工具链,得逐个验证它们认不认得转换后的产物——这一步在合规要求严的组织里可能比技术改造本身更耗时。

6.5 内核版本门槛

第三节那个注在这里兑现:RAFS v6 的 fscache 路径要求内核 ≥ 5.19。

老内核可以退回 FUSE 模式,功能都在,但 v6 相对 v5 最大的那部分性能优势(元数据内核态解析、命中路径不穿越用户态)就打折了。混合内核版本的集群要按内核分组灰度,并且注意别把社区那些"接近 loop device 的顺序读性能"的数字,拿去为 FUSE 模式背书——那些是 fscache 路径的结果。

6.6 后端可达性 = 镜像可用性

最后一条,也是运维上最需要提前想清楚的一条。

传统 pull 的健壮性来自"下载完就切断依赖":镜像拉下来之后,制品库挂了不影响已经在跑的容器。懒加载把这个依赖拉长到了整个容器生命周期——运行到第三天的训练任务,去读一个从没读过的文件,仍然要走一次网络。

后果是:

  • backend 不可达、blob 被误删、凭据过期、网络抖动,都会让运行中的容器直接 input/output error;
  • 故障面从"启动阶段"扩散到"整个生命周期",一次制品库抖动可能同时打断几百个已经跑了几小时的训练任务。

上一篇的结论是"制品库高可用没保障时,别急着上懒加载"。放到 Nydus 上,这条要再加重一档:你的镜像后端从此和你的训练任务同生共死。

6.7 两条路线的完整对照

技术部分到此为止,用一张表收口:

维度 eStargz Nydus(RAFS v6)
格式本质 OCI tar.gz 的可随机读扩展(多 gzip 流 + TOC) 专用只读文件系统 RAFS:bootstrap + chunk 化 blobs
与 OCI 兼容 ✅ 完全兼容,旧运行时退化为全量拉取 ⚠️ 产物非原生 OCI,必须有 Nydus 运行时
制品库要求 任意标准制品库(需支持 HTTP Range) 任意标准制品库(当普通 blob 存)或对象存储
去重粒度 仅整层 blob-digest 级,跨文件跨层都不去重 chunk 级:镜像内自动,跨镜像需 chunk-dict
挂载模型 按层挂载,N 层 → N 个 FUSE + overlayfs 叠加 整镜像一个合并挂载,与层数无关
按需加载位置 用户态 FUSE + gzip 解压 用户态 FUSE(v5)或内核态 EROFS + fscache(v6)
压缩算法 受限于 gzip gzip / zstd 等
随机小文件 IO 一般 潜力更好(尤其 fscache 路径),需按实际后端验证
预取机制 构建期物理重排 + landmark bootstrap 预取表,运行时后台批量拉
运行时依赖 1 个 snapshotter 进程 snapshotter + nydusd + mount helper
P2P 分发 无一等公民支持 与 Dragonfly 深度集成
完整性校验 ChunkDigest chunk / bootstrap 摘要,v6 可叠 dm-verity
转换工具 nerdctl image convert --estargz nydusify convert
改造成本 / 能力上限 低 / 中 中 / 高

七、写在技术博客之外

到这里,技术上的账基本算清了:Nydus 能力更强,代价更重。但真到了要不要上的时候,你会发现决定性的变量往往不在这张表里。

决定选型的,通常不是哪个技术更好,而是谁在为启动时间付钱。

7.1 被吐槽十年的 OCI,为什么还是主流

tar.gz 反随机访问这件事,社区骂了十年。上一篇整整一节都在讲它有多不适合懒加载。但它今天依然是所有人的默认选择,原因只有一个:

所有人都实现了它。

想想有多少东西建立在"层 + digest"这个模型上:制品库(Harbor、Artifactory、ECR、各家云厂商的镜像服务)、构建工具(docker build、BuildKit、Kaniko)、漏洞扫描(Trivy、Clair)、签名与验签(cosign、Notary)、准入控制策略、CI/CD 流水线、离线分发与灾备、审计合规。它们的共同假设都是同一个:镜像是一堆按 digest 寻址的层。

**格式的价值不在优雅,在于它是所有人的最大公约数。**任何替代品要想上位,都得先说服这条链上的每一环各改一次——这不是技术问题,是协调成本问题,而协调成本随参与方数量超线性增长。

回头看上一篇,eStargz 选择"在旧格式里雕花",不是因为它的作者想不到重新设计格式更干净。恰恰相反,那是一个非常清醒的判断:它选择用能力上限换掉整条工具链的改造成本。理解了这一点,才能理解为什么一个"技术上明显更弱"的方案会有大量拥趸。

7.2 两条路线的替换成本清单

把改造成本单独拉出来对照一遍,比性能表更能决定你最后选谁:

eStargz Nydus
构建期改造 一次 convert,产物仍是 OCI 层 nydusify 转换,产物非原生 OCI,灰度期通常双份存储
节点侧改造 1 个进程 + containerd 配置 snapshotter + nydusd + mount helper + containerd 配置
生态兼容 扫描 / 签名 / 准入 / 旧运行时照常工作 需逐项验证工具链是否认得转换后的产物
未适配的节点 自动退化为全量拉取,能跑 直接起不来,必须按节点分组灰度
回退成本 摘掉 snapshotter 即可,镜像不用重推 需保证原生镜像仍在、tag 切得回去
认知成本 低,故障模式接近传统 pull 高,多一个 daemon 和一个缓存目录的排障链路

最后一行是最容易被低估的一项。

**真正贵的不是装组件,是运维认知的重建。**第六节里那些坑——孤儿 nydusd、blobcache 清不得、backend 抖动导致运行中容器 EIO——每一个都是团队里"从来没人见过的新故障"。SRE 手册要重写、监控项要新增、告警阈值要重调、故障演练要补场景。而这些工作没有一件会出现在 POC 的性能报告里。

组织层面还有几个问题得有人回答:谁来长期维护那条转换流水线?谁为节点上 blobcache 的容量负责?镜像出问题的时候,是平台团队的锅还是业务团队的锅?这些问题没有答案的话,技术选型做得再对,最后也会烂在无人认领的位置上。

7.3 启动慢,对谁才是问题

现在问一个更根本的问题:你的启动慢,真的是个问题吗?

对大规模长时间训练来说,答案往往是否定的。一个训练任务跑几天甚至几周,启动多花五分钟,摊进整个生命周期里连零头都算不上。这类场景真正的痛点在别处:训练稳定性、checkpoint 的保存与恢复、通信效率、故障恢复速度。镜像启动那几分钟,排不进前五。

说得更尖锐一点:**在这种场景下引入懒加载,是拿第六节那一整套新的运行时故障面,去换一个占比不到 1% 的时间开销。ROI 完全可能是负的。**一次因为 blobcache 异常导致的训练中断,损失的时间就能把你省下来的启动时间全部吃掉。

但也有例外,共同点是启动成本会被"次数"放大:

  • Spot / 抢占式训练:被抢占一次就换一次节点,启动成本乘以抢占频率;
  • 大集群的故障重调度:节点规模上去之后,"总有节点在挂",重调度是常态;
  • 调试期与高频发版:改一行代码重跑一次,工程师的等待时间是实打实的成本。

所以判断口径不该是"单次启动要多久",而应该是:

启动时间 × 启动次数 ÷ 总运行时间

这个比值低到某个程度,就别折腾了。

7.4 但 serverless 是另一个世界

把同样的技术放到 RunPod、Modal 这类 serverless GPU 平台上,结论会完全反过来。

在那里,计费单位是秒。用户点下"运行"到能用之间的等待,不是一个可以摊销的固定成本——它就是产品本身。冷启动 90 秒和冷启动 3 秒,是两个不同的产品,卖不同的价钱,服务不同的客户。

镜像在这里的定位也变了。它不再是"环境的一部分",而是请求路径上的一环。它和 CDN、和网页的 page load time 是同一类问题:延迟直接转化为体验,体验直接转化为留存。

更关键的是,规模效应的方向也反过来了:

  • 训练集群里,一个镜像在一个节点上跑一周——chunk 级去重、blobcache 跨镜像复用这些能力,几乎用不上;
  • serverless 平台上,同一个节点一天之内要反复起几十上百个不同租户的不同镜像,而这些镜像大概率共享同一批底座(同一个 CUDA、同一个 PyTorch、同一批常见依赖)。chunk 级去重和缓存复用的价值在这里被放大到极致:第 N 个用户的镜像可能有 80% 的 chunk 已经躺在这台机器的 blobcache 里了。

所以结论是清楚的:**对 serverless GPU 平台,镜像加速不是优化项,是准入门槛。**这也解释了为什么这条赛道上的玩家几乎都自研或深度定制了懒加载方案——不是他们更懂技术,是他们没有不做的选项。

一句话收束:同一项技术,在训练平台是"锦上添花且带风险",在 serverless 是"没有就做不了生意"。差别不在技术本身,在计费单位。

7.5 那到底怎么决定

不用急着选技术,先回答三个问题:

  1. **启动时间占总运行时间多少?**低于百分之几就别碰了,去优化别的。
  2. **镜像矩阵有多大的共享底座?**只有一两个镜像,chunk 去重的收益趋近于零,eStargz 的改造成本更划算;几十个镜像共享一套底座,Nydus 的优势才真正兑现。
  3. **组织里有没有人能长期维护这条链路?**没有的话,选那个"坏了也能自动退化"的方案。

最后保持一个诚实的立场:本文写的所有倍数和趋势,都是特定镜像、特定网络、特定后端下的结果。**收益必须在你自己的灰度节点上、用你自己的镜像重测一遍。**社区和厂商公开材料里那些漂亮数字,几乎都测在"缓存热、后端近"的理想前提下,不要直接引用为你的预期。


八、小结

三句话收束技术部分:

  • **eStargz 是在旧格式里雕花,Nydus 是重画一张图纸。**前者把压缩、寻址、叠加三个角色继续绑在"层"上,后者把压缩和寻址降到 chunk、把叠加升到整个镜像。
  • 重画的收益是 chunk 级去重、与层数无关的单挂载点、可选的内核态数据通路,以及不再被 gzip 绑架的压缩算法。
  • 代价是一整套"延迟付费"的运行时依赖:首读放大、常驻 daemon 与它的生命周期陷阱、独立于快照的 blobcache、非原生 OCI 产物没有退化安全网,以及从此和你的镜像后端同生共死。

而在技术之外还有一层:OCI 的统治力不来自它的设计,来自它的普及度;替换成本里最贵的一项不是组件,是运维认知的重建;以及最重要的——启动慢对长时间训练可能根本不是问题,对 serverless 却是生死线。

评论