K8s v1.31 新特性:ImageVolume,允许将镜像作为 Volume 进行挂载

本文主要分享一个 K8s 1.31 增加的一个新 Feature:ImageVolume。允许直接将 OCI 镜像作为 Volume 进行挂载,加速 artifact 分发。

1.背景

Kubernetes 社区正在积极发展,以更好地支持未来的人工智能 (AI) 和机器学习 (ML) 场景。

为满足这些用例的需求之一,Kubernetes 正在增强对开放容器倡议 (OCI) 兼容镜像和工件(即 OCI 对象)作为原生卷源的支持

这样,用户可以专注于使用符合 OCI 标准的工具,并将 OCI 注册表作为存储和分发任何内容的手段。

基于这一愿景,Kubernetes v1.31 引入了一个新的 Alpha 特性:ImageVolume,允许在 Pod 中将 OCI 镜像作为卷使用。该功能可以将一个 OCI 镜像作为 volume 挂载到一个 Pod 中使用,从而可以在 Pod 中访问 OCI 镜像中存储的文件。

该功能是在当前 AI 技术大行其道背景下应运而生的,部署在 K8S 上的 AI 应用急需一种高效且通用的方式分发模型权重文件,此功能可以像分发镜像一样分发模型文件,用户只需要制作好包含了模型权重文件的 OCI 镜像,就可以在 POD 中挂载 OCI 镜像访问其内的模型文件,不再需要复制或者下载模型文件。

通过这一特性,用户可以在 Pod 中引用镜像作为卷,并在容器中以挂载的形式重用这些镜像。

就像这样:

yaml 复制代码
kind: Pod
spec:
  containers:
    - ...
      volumeMounts:
        - name: my-volume
          mountPath: /path/to/directory
  volumes:
    - name: my-volume
      image:
        reference: my-image:tag

这将为在 Kubernetes 中更灵活地管理镜像和卷的需求铺平道路,特别是对于 AI 和 ML 工作负载的复杂需求。

比如在大模型场景,有了 ImageVolume 支持,我们可以直接将大模型打包到镜像里,使用时直接挂载该镜像即可,而且是整个集群都可以使用。

之前通过 PVC 存储时由于 PVC 是 Namespace 范围的,导致同一个模型不同 namespace 下使用都需要重复下载。

大模型体检比较大,每次下载都会花费不少时间。

注意:目前 ImageVolume 仅作为 Alpha 特性引入,未来呈现方式可能有所变化,如果需要体验该功能需要配置 K8S 以启用该特性。

2.环境准备

ImageVolume 特性来源于KEP-4639, 由 SIG NodeSIG Storage 共同完成。

要使用该功能,我们需要先做一些准备工作:

  • 启用 ImageVolume Feature Gates
    • k8s 1.31
    • kube-apiserver 启用
    • kubelet 启用 FeatureGate
  • Container Runtime 支持
    • 当前仅 CRI-O 1.31 版本支持(因为 CRI-O 是和 k8s 同时发版的)
    • containerd 需要等待 PR #10579 合并才行

部署 v1.31 版本集群

首先我们要准备一个 1.31 版本的 k8s 集群。

推荐使用 kubeclipper 来安装集群,这里我们直接使用 master 分支,先安装 kcctl:

bash 复制代码
curl -sfL https://oss.kubeclipper.io/get-kubeclipper.sh | KC_REGION=cn KC_VERSION=master bash

确认版本

bash 复制代码
[root@imagevolume ~]# kcctl version
kcctl version: version.Info{Major:"1", Minor:"4+", GitVersion:"v1.4.0-53+d78681d3b56134", GitCommit:"d78681d3b56134af88a0520c5f2892cdcefce82c", GitTreeState:"clean", BuildDate:"2024-10-18T01:20:12Z", GoVersion:"go1.22.5", Compiler:"gc", Platform:"linux/amd64"}

使用 kcctl 部署 KubeClipper:

bash 复制代码
# 指定下载 master 版本离线包
version=master
wget https://oss.kubeclipper.io/release/${version}/kc-amd64.tar.gz
# 然后使用刚下载的离线包进行部署
node=192.168.10.6
passwd=Thinkbig1
kcctl deploy --server $node --agent $node --passwd $passwd --pkg kc-amd64.tar.gz --v 5

部署完成再次查看版本:

bash 复制代码
[root@imagevolume ~]# kcctl version
kcctl version: version.Info{Major:"1", Minor:"4+", GitVersion:"v1.4.0-53+d78681d3b56134", GitCommit:"d78681d3b56134af88a0520c5f2892cdcefce82c", GitTreeState:"clean", BuildDate:"2024-10-18T01:20:12Z", GoVersion:"go1.22.5", Compiler:"gc", Platform:"linux/amd64"}
kubeclipper-server version: version.Info{Major:"1", Minor:"4+", GitVersion:"v1.4.0-53+d78681d3b56134", GitCommit:"d78681d3b56134af88a0520c5f2892cdcefce82c", GitTreeState:"clean", BuildDate:"2024-10-18T01:14:17Z", GoVersion:"go1.22.5", Compiler:"gc", Platform:"linux/amd64"}

至此,KubeClipper 安装完成。

KubeClipper 默认离线包内置集群不是最新的 1.31版本,因此我们需要手动 push 一下离线包。

从 oss 下载然后打成 tar.gz 包即可,命令如下:

Bash 复制代码
mkdir -p k8s/v1.31.1/amd64

pushd k8s/v1.31.1/amd64

# 下载准备好的离线包
wget https://oss.kubeclipper.io/packages/k8s/v1.31.1/amd64/manifest.json
wget https://oss.kubeclipper.io/packages/k8s/v1.31.1/amd64/images.tar.gz
wget https://oss.kubeclipper.io/packages/k8s/v1.31.1/amd64/configs.tar.gz

popd

tar -zcvf k8s-v1.31.1-amd64.tar.gz  k8s  

然后使用 kcctl resource push 命令进行推送

Bash 复制代码
kcctl resource push --pkg k8s-v1.31.1-amd64.tar.gz --type k8s

查看

Bash 复制代码
[root@imagevolume ~]# kcctl resource list
+--------------+---------------+---------------+---------+-------+
| 192.168.10.6 |     TYPE      |     NAME      | VERSION | ARCH  |
+--------------+---------------+---------------+---------+-------+
| 1.           | cni           | calico        | v3.26.1 | amd64 |
| 2.           | cri           | containerd    | 1.6.4   | amd64 |
| 3.           | k8s           | k8s           | v1.27.4 | amd64 |
| 4.           | k8s           | k8s           | v1.31.1 | amd64 |
| 5.           | k8s-extension | k8s-extension | v1      | amd64 |
| 6.           | kc-extension  | kc-extension  | latest  | amd64 |
+--------------+---------------+---------------+---------+-------+

后续就可以创建 1.31.1 版本的 k8s 了,通过--k8s-version v1.31.1 指定安装 1.31 版本。

Bash 复制代码
node=192.168.10.6
kcctl create cluster --name imagevolume --master $node --untaint-master --k8s-version v1.31.1 

等待几分钟之后,我们就得到了一个 1.31 版本的 k8s 集群。

k8s 启 Feature Gates

然后分别为 kube-apiserver 和 kubelet 启用对应的 Feature Gates。

kube-apiserver

配置 apiserver 启动参数 --feature-gates

kube-apiserver 以 static pod 方式启动,修改配置需要编辑 /etc/kubernetes/manifests/kube-apiserver.yaml 文件:

bash 复制代码
vi /etc/kubernetes/manifests/kube-apiserver.yaml

找到启动命令行,增加以下参数,保存后,等待 kube-apiserver 自动重启完成

bash 复制代码
--feature-gates=ImageVolume=true

kubelet

kubelet 开启 FeatureGates 则是修改配置文件:

bash 复制代码
vi /var/lib/kubelet/config.yaml

在末尾添加如下内容

yaml 复制代码
featureGates:
  ImageVolume: true

然后重启 kubelet

bash 复制代码
systemctl restart kubelet

以上操作完成后,当前集群就具备了 ImageVolume 特性,但是该功能还依赖了 CRI 的种类与版本,需要特别注意。

CRI 配置

目前支持 ImageVolume 特性的 CRI 包括 cri-o 以及 containerd:

  • cri-o 必须 >= v1.31 版本
  • containerd 目前的 patch 还没合并完成,目前的解决方法是手动 merge patch,然后构建二进制文件进行替换。

CRI-O

cri-o 基本上是与 k8s 同时发布新版本,所以一般来说,k8s 有什么新特性,只要依赖了 cri 版本的,那么 cri-o 大多都是最早支持的,ImageVolume 也不例外。

只需要在安装 K8S 集群时,使用 >= v1.31 版本的 cri-o 即可。

如何安装本文档不再赘述,有需求的可以参考相关文档 --> Running Kubernetes with CRI-O

Containerd

社区已经有人提交了 containerd 支持 ImageVolume PR,只是该 PR 目前还未合并,目前我们需要使用 containerd 作为 CRI 来体验 ImageVolume 特性的话,需要自己手动 checkout PR,然后构建二进制文件替换到集群中即可。

相关 patch:#10579 Add OCI/Image Volume Source support

  • clone containerd main 分支代码
bash 复制代码
git clone https://github.com/containerd/containerd.git
bash 复制代码
# 安装 gh:https://github.com/cli/cli
# 先登录:gh auth login

# checkout 对应 pr
gh pr checkout 10579

注意:此 PR 代码有一处校验容器 mounts 是否为 readOnly,如果容器 mounts 配置里 readOnly 不为 true,那么会直接抛出错误。

经过查阅当前 k8s 实现 ImageVolume 时,kubelet 在调度创建涉及 ImageVolume 的容器中,并没有将 Pod 中 volumeMounts 配置的 readOnly 参数透传到 CRI mounts 配置中,这会导致 CRI mounts 中 readOnly 始终为 false,从而导致 containerd 创建容器一直报错。

kubelet 处理核心逻辑:

go 复制代码
// kubernetes/pkg/kubelet/kubelet_pods.go makeMounts 方法
if imageVolumes != nil && utilfeature.DefaultFeatureGate.Enabled(features.ImageVolume) {
                        if image, ok := imageVolumes[mount.Name]; ok {
                                mounts = append(mounts, kubecontainer.Mount{
                                        Name:          mount.Name,
                                        ContainerPath: mount.MountPath,
                                        Image:         image,
                                })
                                continue
                        }
                }
... 

containerd 中的校验

go 复制代码
// containerd/internal/cri/server/container_image_mount.go 中mutateImageMount 方法
...
        if !extraMount.GetReadonly() {
                return fmt.Errorf("readonly must be true while mount image: %+v", extraMount)
        }
...

为了让整个流程先能跑通,我们先暂时把 Containerd 中的这个校验注释掉。

  • 修改之后重新构建 containerd 二进制文件
bash 复制代码
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 make binaries

构建后的参数在 containerd/bin 目录下,会生成以下几个文件:

bash 复制代码
-rwxr-xr-x 1 x 40M  9 20 16:37 containerd
-rwxr-xr-x 1 x 14M  9 20 16:37 containerd-shim-runc-v2
-rwxr-xr-x 1 x 20M  9 20 16:37 containerd-stress
-rwxr-xr-x 1 x 21M  9 20 16:37 ctr

将其全部复制到 K8S 集群节点 /usr/local/bin/ 目录,做好旧二进制文件备份。

  • 替换现有环境的 containerd
bash 复制代码
# 停止 containerd
systemctl stop containerd
# cp 自己构建的二进制文件到 /usr/local/bin
cp containerd containerd-shim-runc-v2 containerd-stress ctr /usr/local/bin
systemctl start containerd

最后等待集群启动,查看节点当前的 cri 版本

bash 复制代码
kubectl get node -owide

至此,集群就可以使用 ImageVolume 功能了。

3. 使用

构建目标镜像

这里就简单创建一个包含了 Qwen2-0.5B 大模型权重文件的 OCI 镜像。

  • 下载 Qwen2-0.5B 大模型
bash 复制代码
mkdir models && cd models
git lfs install
git clone https://www.modelscope.cn/qwen/Qwen2-0.5B.git

模型内容如下:

bash 复制代码
[root@docker models]# ll Qwen2-0.5B/ -lhS
total 1.2G
-rw-r--r-- 1 root root 1.2G Oct 12 08:35 model.safetensors
-rw-r--r-- 1 root root 6.8M Oct 12 08:39 tokenizer.json
-rw-r--r-- 1 root root 2.7M Oct 12 08:39 vocab.json
-rw-r--r-- 1 root root 1.6M Oct 12 08:39 merges.txt
-rw-r--r-- 1 root root  12K Oct 12 08:39 LICENSE
-rw-r--r-- 1 root root 4.8K Oct 12 08:39 README.md
-rw-r--r-- 1 root root 1.3K Oct 12 08:39 tokenizer_config.json
-rw-r--r-- 1 root root  661 Oct 12 08:39 config.json
-rw-r--r-- 1 root root  138 Oct 12 08:39 generation_config.json
-rw-r--r-- 1 root root   48 Oct 12 08:39 configuration.json
  • 新建 Dockerfile,拷贝 models 目录到镜像指定目录

Dockerfile 如下:

Dockerfile 复制代码
FROM scratch
COPY ./models /models

目录结构如下所示:

bash 复制代码
[root@docker ~]# tree image-builder/
image-builder/
├── Dockerfile
└── models
    └── Qwen2-0.5B
        ├── config.json
        ├── configuration.json
        ├── generation_config.json
        ├── LICENSE
        ├── merges.txt
        ├── model.safetensors
        ├── README.md
        ├── tokenizer_config.json
        ├── tokenizer.json
        └── vocab.json
  • 构建镜像
bash 复制代码
cd image-builder 
docker build  -t lixd96/qwen2-0.5b:v1 .

后续将其作为 OCI 镜像挂载到 Pod 中使用。

创建 Pod 挂载 OCI 镜像

创建一个 Pod 挂载上述 OCI 镜像,完整 yaml 内容如下:

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: oci-pod
spec:
  containers:
    - name: test
      image: busybox:1.36
      imagePullPolicy: IfNotPresent
      command:
        - sleep
        - "3600"
      volumeMounts:
        - name: volume
          mountPath: /volume
          readOnly: true
  volumes:
    - name: volume
      image:
        reference: lixd96/qwen2-0.5b:v1
        pullPolicy: IfNotPresent

应用到集群中

bash 复制代码
kubectl apply -f pod.yaml

等待 Pod 调度成功后,进入 Pod 中查看 /volume 目录就可以看到模型权重文件了

bash 复制代码
[root@imagevolume ~]# k exec -it oci-pod -- ls -al /volume/models/Qwen2-0.5B/
total 1221384
drwxr-xr-x    2 root     root           247 Oct 12 08:40 .
drwxr-xr-x    3 root     root            24 Oct 12 08:42 ..
-rw-r--r--    1 root     root          1519 Oct 12 08:39 .gitattributes
-rw-r--r--    1 root     root         11344 Oct 12 08:39 LICENSE
-rw-r--r--    1 root     root          4819 Oct 12 08:39 README.md
-rw-r--r--    1 root     root           661 Oct 12 08:39 config.json
-rw-r--r--    1 root     root            48 Oct 12 08:39 configuration.json
-rw-r--r--    1 root     root           138 Oct 12 08:39 generation_config.json
-rw-r--r--    1 root     root       1671839 Oct 12 08:39 merges.txt
-rw-r--r--    1 root     root     1239173352 Oct 12 08:35 model.safetensors
-rw-r--r--    1 root     root       7028015 Oct 12 08:39 tokenizer.json
-rw-r--r--    1 root     root          1289 Oct 12 08:39 tokenizer_config.json
-rw-r--r--    1 root     root       2776833 Oct 12 08:39 vocab.json

4. 小结

本文主要分享了 k8s 1.31 新特性 ImageVolume,包括配置以及使用方式。

要使用该功能,我们需要先做一些准备工作:

  • 启用 ImageVolume Feature Gates
    • k8s 1.31
    • kube-apiserver 启用
    • kubelet 启用 FeatureGate
  • Container Runtime 支持
    • 当前仅 CRI-O 1.31 版本支持(因为 CRI-O 是和 k8s 同时发版的)
    • containerd 需要等待 PR #10579 合并才行

目标镜像构建也和普通镜像一样:

dockerfile 复制代码
FROM scratch
COPY ./models /models

挂载方式:

yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  name: oci-pod
spec:
  containers:
    - name: test
      image: docker.io/library/busybox:latest
      imagePullPolicy: IfNotPresent
      command:
        - sleep
        - "3600"
      volumeMounts:
        - name: volume
          mountPath: /volume
          readOnly: true
  volumes:
    - name: volume
      image:
        reference: registry.cn-beijing.aliyuncs.com/kubeclipper/qwen1.5-0.5b-chat:latest
        pullPolicy: IfNotPresent

体验下来感觉 ImageVolume 功能在 AI 相关场景应该是有较大的发挥空间的,可以让 artifact 分发更加方便。


【Kubernetes 系列】 持续更新中,搜索公众号【探索云原生】订阅,阅读更多文章。


5.参考

Kubernetes 1.31: Read Only Volumes Based On OCI Artifacts (alpha)

Running Kubernetes with CRI-O

Kubeclipper

相关推荐
KubeSphere 云原生4 小时前
云原生周刊:在 Kubernetes 上运行机器学习
云原生·容器·kubernetes
企鹅侠客11 小时前
k8s-dashboard-v2.0.0-beta6部署
云原生·容器·kubernetes
奋斗的蛋黄11 小时前
SRE 进阶:AI 驱动的集群全自动化排查指南(零人工干预版)
运维·人工智能·kubernetes·自动化
戮戮13 小时前
一次深入排查:Spring Cloud Gateway TCP 连接复用导致 K8s 负载均衡失效
tcp/ip·spring cloud·kubernetes·gateway·负载均衡·netty
能不能别报错15 小时前
K8s学习笔记(二十四) ingress
笔记·学习·kubernetes
能不能别报错15 小时前
K8s学习笔记(二十三) 网络策略 NetworkPolicy
笔记·学习·kubernetes
suknna16 小时前
记一次 Kubebuilder Operator 开发中的 CRD 注解超限问题
kubernetes
victory043118 小时前
K8S 安装 部署 文档
算法·贪心算法·kubernetes
能不能别报错1 天前
K8s学习笔记(二十二) 网络组件 Flannel与Calico
笔记·学习·kubernetes
lijun_xiao20091 天前
DevOps(devops/k8s/docker/Linux)学习笔记
docker·kubernetes·devops