基于kata direct volume特性, 实现安全容器KataContainer的 CSI block volume直通方案

Kubernetes 中集成 Kata Containers 可以为容器运行时提供更好的安全性隔离性,但在存储方面仍然还存在一些限制与不足。

目前方案:virtiofs协议

Kata Containers 在 2.4 版本之前,挂载 PV 的整个过程与 CSI 之间是没有任何交互的,而且也不能直接使用 CSI 挂载的 PV,只能通过 virtiofs 协议 将宿主机上的存储卷以文件共享的方式提供给 Kata Containers 虚拟机中的 container 使用。

virtiofs 协议 的实现方式如下图所示:

这种方式虽然能够解决 PV 存储挂载的问题,但与直接在宿主机上使用存储卷相比,由于 virtiofs 实现方式的 I/O 路径过长,会带来不少的性能损耗。

经过环境实测,总结出以下几方面问题:

  1. 稳定性方面:在实际环境中,对 virtiofs 共享的盘进行 fio 测试,我们经常能观察到 io 不连续的现象,并且在对盘进行加压测试时,会影响到容器中其他进程的响应,比如 ssh 进程响应会超时;

  2. 性能方面:与直接在宿主机上使用存储卷相比,virtiofs 共享盘在 iops带宽方面有不少差距;

  3. 功能方面virtiofs 共享盘方式无法在线调整 PV 大小,需要通过重启 Pod 才能使 VM 能感知到 PV 大小变化;

优化方案

基于 virtiofs 现存问题,Kata Containers 在 2.4 版本提供了 direct assigned volume 功能,能够将文件系统挂载操作从宿主机移动到 Guest 中,相当于是一种 block volume 直通方案。和 virtiofs 相比,不仅能提供接近直接宿主机上使用存储卷的性能,而且还能支持 native FS。由于不需要借助于 virtiofs,在使用上会更加稳定,也能带来安全性方面的提升。同时,这个特性还支持在线修改 PV 存储大小。

Kata Containers 存储卷直通方案如下图所示:

具体kata direct assigned volume 特性设计:https://github.com/kata-containers/kata-containers/blob/main/docs/design/direct-blk-device-assignment.md

在 CSI 中实现 direct volume

前提条件

  1. 由于需要在 host 上创建文件,CSI node 服务在部署时需要把 /run/kata-containers/shared/direct-volumes 目录以 hostpath 方式挂载到 pod 里。

  2. CSI 在挂载时需要明确知道所挂载的 volume 是否是以 direct volume 这种方式挂载,所以需要有一种机制能通知到 CSI,可以借助以下三种方式:

    通过 StorageClass 指定 direct volume 属性

    PVC 对象里通过 annotation 打上 direct volume 属性,同时 CSI 插件需要打开 --extra-create-metadata 属性来帮助 CSI 能从 K8s apiserver 查询到 PVCannotation 信息

    通过查询 Podruntimeclass 信息来判断是否是 Kata direct volume 挂载

实现步骤

以下步骤主要都在 CSI NodePublishVolume 接口里实现:

  1. 把远程存储的 block device 挂载到 host 上。
  2. 根据实际需求场景对 block device 做文件系统格式化。
  3. 生成 mountinfo.json 信息并把 mountinfo.json 信息传递给 Kata。

mountInfo 的内容是 json 格式,主要数据结构如下:

// MountInfo contains the information needed by Kata to consume a host block device and mount it as a filesystem inside the guest VM.
type MountInfo struct {
	// The type of the volume (ie. block)
	VolumeType string `json:"volume-type"`
	// The device backing the volume.
	Device string `json:"device"`
	// The filesystem type to be mounted on the volume.
	FsType string `json:"fstype"`
	// Additional metadata to pass to the agent regarding this volume.
	Metadata map[string]string `json:"metadata,omitempty"`
	// Additional mount options.
	Options []string `json:"options,omitempty"`
}

其中 CSI 侧主要需要传递以下三个字段信息即可,例如:

mountInfo := &volume.MountInfo{
				VolumeType: "block",  // 设备类型
    				Device:     dev/sdd,  // 块设备路径
    				FsType:     ext4,     // 文件系统类型
			}
volume.Add("/run/kata-containers/shared/direct-volumes/volume-path(base64加密)/", mountInfo) 

CSI 负责把 mountinfo.json 传递给 KataKata 会在容器所在 host/run/kata-containers/shared/direct-volumes/volume-path(base64加密)/ 目录下生成 mountinfo.json 文件,目前 CSI 有两种方式可以传递 mountinfo.json 信息

  1. 通过调用 kata-container 代码里 direct volume 模块的 add 方法传递 mountinfo.json 信息,部分代码实例如下:
import (
    "encoding/json"
    volume "github.com/kata-containers/kata-containers/src/runtime/pkg/direct-volume"
    "google.golang.org/grpc/status"
    klog "k8s.io/klog/v2"
)

// NodePublishVolume 中发布
func AddDirectVolume(volumePath, device, fsType string) error {
    mountInfo := &volume.MountInfo{
        VolumeType: "block",
        Device:     device,
        FsType:     fsType,
    }

    mi, err := json.Marshal(mountInfo)
    if err != nil {
        klog.Errorf("addDirectVolume - json.Marshal failed: ", err.Error())
        return status.Errorf(codes.Internal, "json.Marshal failed: %s", err.Error())
    }
    
    if err := volume.Add(volumePath, string(mi)); err != nil { 
        klog.Errorf("addDirectVolume - add direct volume failed: ", err.Error())
        return status.Errorf(codes.Internal, "add direct volume failed: %s", err.Error())
    }

    klog.Infof("add direct volume done: %s%s", volumePath, string(mi))
    return nil
}

//  NodeUnpublishVolume 中remove掉
	if err := volume.Remove(targetPath); err != nil {
		log.Errorf("NodeUnpublishVolume: kata direct volume remove failed: %s", err.Error())
	}
  1. 通过 kata-runtime CLI 命令传递 mountinfo.json 信息:
$ kata-runtime direct-volume add --volume-path [volumePath] --mount-info [mountinfo.json]

$ kata-runtime direct-volume remove --volume-path [volumePath] --mount-info [mountinfo.json]

最后会在容器所在 host 的 /run/kata-containers/shared/direct-volumes/volume-path(base64加密)/ 目录下生成 mountinfo.json 文件,然后 Kata Containers 会在启动容器时检查该目录是否有 mountinfo.json 文件并解析该文件,同时更新容器 specmount 信息,将直通卷的信息加入进去,然后将修改后的 spec 传给 kata-agent;

方案限制:

  1. 使用 direct volume 方式的 PV 只能给一个 Pod 使用,所以在创建 PVC 时需要指定 accessModeReadWriteOnce
  2. 使用 direct volume 方式不支持更高级的 volume 功能,比如:fsGroupfsGroupChangePolicysubPath

未来:kata 联动 kubelet/kube-apiserver,实现CSI 卷的运行时辅助挂载

社区未通过的最终方案:KEP-2857:持久卷的运行时辅助安装

Demo

---
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  runtime-class: kata-qemu  # 方式一:通过设置runtime-class,使用kata 运行时; 其下面的卷都是直通卷
  containers:
  - name: app
    image: centos
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
    volumeMounts:
    - name: persistent-storage
      mountPath: /data
  volumes:
  - name: persistent-storage
    persistentVolumeClaim:
      claimName: ebs-claim
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  annotations:
    skip-hostmount: "true"  # 方式二:通知csi为卡通直通卷
  name: ebs-claim
spec:
  accessModes:
    - ReadWriteOncePod
  volumeMode: Filesystem
  storageClassName: ebs-sc
  resources:
    requests:
      storage: 1Gi
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: ebs-sc
provisioner: ebs.csi.aws.com # 方式三:特定的csi driver实现 kata direct assigned volume
volumeBindingMode: WaitForFirstConsumer
parameters:
  csi.storage.k8s.io/fstype: ext4