K8s 1.36 ImageVolume GA:OCI 镜像不再只能跑容器

📅 2026/6/30 4:17:53
K8s 1.36 ImageVolume GA:OCI 镜像不再只能跑容器
背景OCIOpen Container Initiative是 2015 年在 Linux 基金会支持下成立的开放项目Docker、CoreOS后来被 Red Hat 收购和容器行业的主要厂商一起围绕容器格式和运行时制定了开放标准。Docker 捐出了自己的镜像格式作为基础社区在此基础上逐步形成了 Runtime、Image 和 Distribution 三大规范。2017 年 image-spec v1.0 发布容器镜像的格式算是定下来了。在 OCI 标准下运行一个容器的过程就是从 Registry 下载 OCI 镜像 → 解压到 OCI Bundle → OCI Runtime 运行这个 Bundle。整个流程标准化之后不同 Runtime、不同 Registry 之间可以互操作不用强行绑定 Docker。Docker 也把 libcontainer 的实现移动到 runC 捐给了 OCI社区有了第一个 OCI Runtime 的参考实现。OCI Artifacts虽然 OCI 标准最初完全是围绕容器设计的镜像格式里的 mediaType、config 结构都是为「跑容器」量身定做的。但是如果我不想跑容器只想把一堆文件打包分发呢人们很快发现OCI Registry 的分层存储和分发机制天然适合分发更多东西。OCI 镜像的本质是什么就是一堆只读的层layer加上一个 manifest 描述这些层的组织方式再通过 Registry 的 API 完成分发。同时 OCI Registry 提供了一整套“可寻址、可校验、可去重、可控访问”的分发原语然后这个模型并不绑定「容器」。所以社区很自然地想到能不能把 OCI Registry 当成一个通用的内容分发平台来用实际上社区很早就开始在 OCI Registry 里存非镜像内容了但早期的做法都是 hack——把非镜像内容伪装成容器镜像塞进去Registry 其实并不知道这些东西不是用来跑的Helm 从 3.0 开始支持把 Chart 推到 OCI Registry算是最早的「Registry 当通用存储」的生产实践。Cosign 直接把容器签名、SBOM 也存进 OCI Registry用镜像层来承载签名数据。ORASOCI Registry As Storage更猛WASM 模块、OPA 策略、Falco 规则都能往里塞相当于把 OCI Registry 当成一个通用的对象存储来用。OCI Artifacts 就是这么来的,把各种产物存进 OCI Registry、当成通用内容仓库来分发。这些用法推动了 OCI 规范本身的演进。2024 年image-spec v1.1.0 正式加入了artifactType字段允许 Manifest 声明「我不是容器镜像我是一个签名 / 一个 Helm Chart / 一个模型权重」。OCI 对非镜像内容的支持从社区 hack 变成了规范的一部分OCI Registry 正式成为了一个通用的内容仓库。K8s ImageVolume现在 OCI Registry 已经变成了一个通用的内容仓库但问题来了Helm、Cosign、ORAS 这些工具都在往里塞东西但到了 Kubernetes 这边OCI 镜像还是只能拿来跑容器缺少一个原生的消费方式。ImageVolume 就是来补上这一块的。它允许在 Pod 中将 OCI 镜像直接作为 Volume 挂载让 OCI Artifacts 在 K8s 里也能被原生消费不再只是跑容器。就像这样kind: Pod spec: containers: - … volumeMounts: - name: my-volume mountPath: /path/to/directory volumes: - name: my-volume image: reference: my-image:tag不过有一点要注意ImageVolume 挂载是只读的如果需要在运行时修改挂载的文件还是得用 PVC。后面会详细讨论这个限制。ImageVolume 让 OCI Artifacts 在 K8s 里有了第一个原生的消费方式。不过这个能力并不是一步到位的从 Alpha 到 GA 走了近两年。2. ImageVolume 从 Alpha 到 GAImageVolume 这个特性来源于 KEP-4639由 SIG Node 和 SIG Storage 共同推动。从 v1.31 Alpha 到 v1.36 GA 走了近两年具体时间线如下阶段K8s 版本发布时间Feature Gate 默认值说明Alphav1.312024-08false需要手动开启 Feature GateBeta默认关v1.332025-04falseBeta 代码合入但仍默认关闭Beta默认关v1.342025-08false移除 noexec 限制仍默认关闭Beta默认开v1.352025-12true首次默认启用GAv1.362026-04true锁定Feature Gate 锁定v1.39 移除Containerd v2.1.0 才正式支持 ImageVolume而且没有回移到 v2.0.x 分支所以用 containerd 的话必须升级到 v2.1.0。接下来过一遍每个阶段的变化。2.1 subPath 支持Alpha → BetaAlpha 阶段 ImageVolume 不支持subPath也就是说你只能挂载镜像的整个文件系统没法只挂载其中的某个子目录。Beta 阶段v1.33解除了这个限制subPath和subPathExpr都可以用了。对应的 CRI API 也新增了image_sub_path字段来支持这个功能。现在你可以这样用# 只挂载镜像中的 models/Qwen2-0.5B 子目录到 /models/qwen2 containers: - name: app image: busybox:1.36 volumeMounts: - name: model-volume mountPath: /models/qwen2 subPath: Qwen2-0.5B # 挂载镜像中的这个子路径 readOnly: true volumes: - name: model-volume image: reference: registry.example.com/models/all-models:v1 pullPolicy: IfNotPresent # 如果 subPath 指定的路径在镜像中不存在容器创建会报错很多时候一个镜像里会放多个目录有了 subPath 就不用把整个镜像都挂进来了。2.2 noexec 限制移除Alpha → BetaAlpha 阶段 ImageVolume 挂载时强制加了noexec选项挂载进来的文件不能被执行。这个限制在 Beta 阶段2025-06PR #5354被移除了。社区讨论后觉得noexec限制过于严格ImageVolume 的主要用途是分发只读数据强制 noexec 没有必要反而限制了某些合理的使用场景比如挂载包含可执行工具的镜像。不过 ImageVolume 仍然是只读挂载ro读写支持还得等后续的 KEP。2.3 Kubelet 监控指标Alpha → BetaBeta 阶段新增了 3 个 Kubelet 指标方便监控 ImageVolume 的使用情况。kubelet_image_volume_requested_total— 请求的 ImageVolume 数量kubelet_image_volume_mounted_succeed_total— ImageVolume 挂载成功的数量kubelet_image_volume_mounted_errors_total— ImageVolume 挂载失败的数量GA 阶段这些指标提升到了 BETA 稳定性级别可以在 Prometheus 里配告警了。2.4 Feature Gate 锁定GAv1.36 GA 后ImageVolumeFeature Gate 被锁定为默认开启没法关了。按照 K8s 的惯例Feature Gate 会在 GA 后 3 个版本移除也就是 v1.39 会彻底删掉这个 Gate。所以现在的状态就是不再需要手动开启 Feature Gate 了v1.36 集群开箱即用API 字段上的featureGateImageVolume注解也被移除了E2E 测试提升为 Conformance 级别这是 GA 的标志之一2.5 containerd 原生支持这个虽然不是 K8s 代码的变化但可能是对使用者影响最大的变化。Alpha 阶段containerd 不支持 ImageVolume想玩的话只能自己动手。参考我之前的文章 xxx 我当时手动 checkout 了 containerd 的 PR #10579编译替换二进制文件。编译倒是不难但是那个 PR 还有个 bugkubelet 没有把readOnly参数透传到 CRI mounts 配置中导致 containerd 校验 readOnly 失败一直报错。。。没办法还得手动注释掉校验逻辑整个流程折腾下来真的挺崩溃的。现在 containerd v2.1.0 已经原生支持 ImageVolume直接用就行不需要任何 hack。CRI-O 的话从 v1.31 就支持了v1.34 还增加了 subPath 支持一直走在前面。2.6 变化总结整理一下变化项Alpha (v1.31)Beta (v1.33-v1.35)GA (v1.36)subPath❌ 不支持✅ 支持✅ 支持noexec 限制强制 noexec移除无限制监控指标无Alpha 级别BETA 级别Feature Gate默认关需手动开v1.33/34 默认关v1.35 默认开锁定开启containerd需手动编译 PRv2.1.0 原生支持v2.1.0CRI-Ov1.31 支持v1.34 支持 subPath同左挂载模式只读只读只读3. 现在怎么用变化聊完了实际用起来是什么感觉呢。GA 之后用起来比 Alpha 阶段简单太多了不用再折腾 Feature Gate 和手动编译 containerd 了。3.1 环境要求Kubernetes v1.36Container Runtimecontainerd v2.1.0CRI-O v1.31subPath 需要 v1.34就这么简单不需要额外配置任何 Feature Gate。本次验证环境是使用 KubeClipper 安装的 K8s 集群版本如下Kubernetes v1.36.1containerd v2.2.43.2 构建目标镜像使用方式和 Alpha 阶段基本一致。先构建一个包含模型文件的 OCI 镜像用FROM scratch就行不需要任何基础镜像。为了后面演示 subPath这里在镜像里放两个模型目录再放一个配置文件mkdir -p models/Qwen2-0.5B models/Llama2-7B echo qwen2 model weights models/Qwen2-0.5B/model.bin echo qwen2 config models/Qwen2-0.5B/config.json echo llama2 model weights models/Llama2-7B/model.bin echo llama2 config models/Llama2-7B/config.json echo app config v1 app.conf目录结构如下image-builder/ ├── Dockerfile ├── app.conf └── models/ ├── Qwen2-0.5B/ │ ├── config.json │ └── model.bin └── Llama2-7B/ ├── config.json └── model.binDockerfile 如下FROM scratch COPY ./models /models COPY ./app.conf /app.conf构建并推送到镜像仓库docker build -t registry.example.com/demo/image-volume:v1 . docker push registry.example.com/demo/image-volume:v13.3 基本挂载创建 Pod 挂载这个镜像apiVersion: v1 kind: Pod metadata: name: image-volume-demo spec: containers: - name: app image: busybox:1.36 command: [sleep, 3600] volumeMounts: - name: model-volume mountPath: /models readOnly: true volumes: - name: model-volume image: reference: registry.example.com/demo/image-volume:v1 pullPolicy: IfNotPresent应用到集群等 Pod Running 后查看挂载内容$ kubectl apply -f pod.yaml pod/image-volume-demo created $ kubectl get pod image-volume-demo NAME READY STATUS RESTARTS AGE image-volume-demo 1/1 Running 0 30s $ kubectl exec image-volume-demo -- ls -la /models/ total 16 drwxr-xr-x 1 root root 4096 Jun 16 13:03 . drwxr-xr-x 1 root root 4096 Jun 16 13:03 .. -rw-r--r-- 1 root root 14 Jun 16 13:03 app.conf drwxr-xr-x 2 root root 4096 Jun 16 13:03 Qwen2-0.5B drwxr-xr-x 2 root root 4096 Jun 16 13:03 Llama2-7B $ kubectl exec image-volume-demo -- cat /models/app.conf app config v1 $ kubectl exec image-volume-demo -- cat /models/Qwen2-0.5B/config.json qwen2 config镜像里的文件都挂载进来了跟预期一致。3.4 subPath 挂载上面那个镜像里放了两个模型目录如果 Pod 只需要 Qwen2-0.5B不需要把整个镜像都挂进来用 subPath 就行apiVersion: v1 kind: Pod metadata: name: image-volume-subpath spec: containers: - name: app image: busybox:1.36 command: [sleep, 3600] volumeMounts: - name: model-volume mountPath: /models/qwen2 subPath: Qwen2-0.5B readOnly: true volumes: - name: model-volume image: reference: registry.example.com/demo/image-volume:v1 pullPolicy: IfNotPresent验证一下挂载目录里只有 Qwen2-0.5B 的内容$ kubectl exec image-volume-subpath -- ls -la /models/qwen2/ total 12 drwxr-xr-x 2 root root 4096 Jun 16 13:03 . drwxr-xr-x 3 root root 4096 Jun 16 13:03 .. -rw-r--r-- 1 root root 14 Jun 16 13:03 config.json -rw-r--r-- 1 root root 22 Jun 16 13:03 model.bin $ kubectl exec image-volume-subpath -- cat /models/qwen2/config.json qwen2 config只挂载了 Qwen2-0.5B 目录Llama2-7B 和 app.conf 都不在。如果 subPath 指定的路径在镜像中不存在容器创建会直接报错$ kubectl get pod image-volume-subpath-err -o jsonpath{.status.containerStatuses[0].state.waiting.message} failed to mount image volume: ImageVolumeMountFailed: failed to ensure image subpath not-exist-dir in ...: openat not-exist-dir: no such file or directory3.5 只读挂载验证ImageVolume 挂载是只读的尝试写入会报Read-only file system$ kubectl exec image-volume-demo -- sh -c echo test /models/test.txt sh: cant create /models/test.txt: Read-only file system最后提几个实际使用中的注意事项。ImageVolume 挂载是只读的如果需要运行时修改文件还是得用 PVC目前没有读写支持的 KEP。Pod 重建时 ImageVolume 会重新解析远端镜像所以生产环境建议用 digest 而不是 tag 引用镜像避免 Pod 重建后 tag 被覆盖导致拿到非预期版本。镜像层共享能省磁盘两个 ImageVolume 引用的镜像有相同层的话 containerd 只存一份但大模型镜像多了也要注意节点磁盘压力。4. 小结OCI 从 2017 年 image-spec v1.0 发布到今天走了一条挺清晰的路先是容器镜像格式标准化然后 OCI Registry 被社区 hack 成通用内容仓库接着 image-spec v1.1.0 和 distribution-spec v1.1.0 把这种用法正式写入规范artifactType referrers API。OCI Artifacts 从社区变通变成了标准的一部分。ImageVolume GA 等于 Kubernetes 也跟上了OCI Artifacts 在 K8s 里有了第一个原生的消费方式。