k8s存储演进过程

最近在做一些CSI相关的工作,重新看了一下之前的笔记和资料,发现kubernetes从最初简单的Volume到现在复杂的CSI的设计,有一个逐步演进的过程,搞清楚一个技术的历史可以帮助我们更好的理解和掌握它。

Volume

要在一个 Pod 里声明 Volume,只要在 Pod 里加上 spec.volumes 字段即可。然后,你就可以在这个字段里定义一个具体类型的 Volume 了,比如:hostPath,emptyDir等。现在通常直接用Volume的情况局限在使用宿主机的本地存储,为什么不推荐通过Volume使用某个具体的网络存储呢?可以看下面这个例子:

 1apiVersion: v1
 2kind: Pod
 3metadata:
 4  name: rbd
 5spec:
 6  containers:
 7    - image: kubernetes/pause
 8      name: rbd-rw
 9      volumeMounts:
10      - name: rbdpd
11        mountPath: /mnt/rbd
12  volumes:
13    - name: rbdpd
14      rbd:
15        monitors:
16          - '10.16.154.78:6789'
17        pool: kube
18        image: foo
19        fsType: ext4
20        readOnly: true
21        user: admin
22        keyring: /etc/ceph/keyring
23        imageformat: "2"
24        imagefeatures: "layering"

可以发现,直接通过Volume使用网络存储有两个问题:

  1. 开发者需要熟悉所使用的存储的各种配置参数
  2. 暴露了存储服务api、用户名、授权文件等敏感信息

PVC/PV

为了解决上述两个问题,k8s项目引入了PVC/PV。

PVC面向开发人员,开发人员不用再知道大量的存储实现细节,只需要声明需要的存储容量和读写权限等:

 1kind: PersistentVolumeClaim
 2apiVersion: v1
 3metadata:
 4  name: pv-claim
 5spec:
 6  accessModes:
 7  - ReadWriteOnce
 8  resources:
 9    requests:
10      storage: 1Gi

而PV面向存储管理人员,他们熟知存储使用细节:

 1kind: PersistentVolume
 2apiVersion: v1
 3metadata:
 4  name: pv-volume
 5  labels:
 6    type: local
 7spec:
 8  capacity:
 9    storage: 10Gi
10  accessModes:
11    - ReadWriteOnce
12  rbd:
13    monitors:
14    - '10.16.154.78:6789'
15    pool: kube
16    image: foo
17    fsType: ext4
18    readOnly: true
19    user: admin
20    keyring: /etc/ceph/keyring

通过PVC/PV解决了开发者使用存储的困难,但是没有解决运维人员管理存储的困难。k8s虽然把网络存储attachmount到宿主机和mount到容器的流程自动化了(参考最后一节),但是创建(provision)网络存储的工作还没有自动化,运维人员还是需要手动创建网络存储和PV。

StorageClass

为了解决上述问题,引入了StorageClass,借助storageclass和external-storage库,可以使得存储的provision变得自动化(即自动的创建网络存储和PV)。比如声明下面这个sc:

1apiVersion: storage.k8s.io/v1
2kind: StorageClass
3metadata:
4  name: block-service
5provisioner: kubernetes.io/gce-pd
6parameters:
7  type: pd-ssd

则可以借助 kubernetes.io/gce-pd存储插件(基于external-storage库开发)自动创建网络存储和PV。

FlexVolume

现在看起来似乎没什么问题了。但还是有问题,随着各种云存储层出不穷,越来越多的存储厂商想要把自己的存储插件塞到k8s的主干代码(in-tree)中(pkg/volume)。所以k8s想提供一种抽象层,使得新增的存储插件不必和k8s主干一起演进和测试。随后就引入了FlexVolume这种Volume类型。

对于attachMount这两个操作,controller实际上是根据不同的存储类型,调用pkg/volume目录下的存储插件(Volume Plugin)代码,而对于FlexVolume这个Volume类型,就是对应 pkg/volume/flexvolume 这个目录里的代码。

但是这个目录和其他存储插件不一样,它只充当插件的入口,而没有复杂的业务逻辑。这个目录里的代码非常简单,比如mount操作,就是去调用宿主机上的二进制文件,所以当你编写完了 FlexVolume 的实现之后,一定要把它的可执行文件(比如 blobfuse)放在每个节点的插件目录下(/usr/libexec/kubernetes/kubelet-plugins/volume/exec)。

CSI

FlexVolume是一种out-of-tree的解决方案,但是依然不够完美。主要体现在它需要宿主机的权限并在宿主机上安装二进制文件(mount操作需要在worker node上安装二进制文件,attach操作需要在master node上安装二进制文件)

此外,在StorageClass这一节提到我们可以借助external-storage库来编写存储插件,实现dynamic provision的能力,但是要专门去写一个还是有点麻烦。

为了解决这些问题(以及其他类似问题),社区又提出了CSI方案,彻底把存储插件的管理逻辑和k8s主干代码解耦开来:

  1. 不需要节点权限,不需要在节点上安装可执行文件
  2. 把公共能力(动态provision、attach等)从k8s主干分支中抽离出来,放在kubernetes-csi这个项目中

此外值得注意的是:The Container Storage Interface (CSI) is a standard for exposing arbitrary block and file storage systems to containerized workloads on Container Orchestration Systems (COs) like Kubernetes.也就是说CSI是一个标准,除了k8s以外还可以兼容其他容器编排平台,只要按照这个标准进行实现即可,参考CSI spec

CSI本身的运行机制不是本篇的重点,可以参考kubernetes CSI官方文档设计文档。比如在设计文档里已经把CSI Driver各个组件的交互过程写的非常清楚了:Example Walkthrough ,无需赘述。

Volume的实现原理

这个主题其实和本文没什么关系,放在最后作参考用。

本地存储

  • emptyDir:直接用 /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字> 目录,所以emptyDir Volume的存储介质(比如Disk还是SSD)由kubelet根目录(一般是/var/lib/kubelet)所在的文件系统决定

  • hostPath:通过bind mount的方式把node上的某个路径mount到/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>

网络存储

除了NFS只需要mount操作:mount -t nfs <NFS服务器地址>:/ /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字> 以外,其他存储(块、对象)都需要两步:

  • attach: 把远程磁盘attach到宿主机,成为宿主机的一个块设备,比如gcloud compute instances attach-disk <虚拟机名字> --disk <远程磁盘名字>

  • mount: 把块设备格式化成文件系统(NFS不需要),并mount到宿主机上,比如:mount -t nfs <NFS服务器地址>:/ /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>

kubelet 在向 Docker 发起 CRI 请求时,要先准备好宿主机上的/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>这个目录,接着通过docker run -v /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>:/<容器内的目标目录> 我的镜像 … 就把Volume挂载进了容器。