一只肥羊的思考

kubernetes volume 管理

什么是 Volume 特性

在 kubernetes 中,数据存储是一个非常关键的问题。用户的某些类型的服务经常会有对数据存储方面的一些需求,比如:

  • 数据需要在容器重启之后,甚至 pod 被删除后,仍旧被保留
  • 有些数据需要在同一个 pod 之间进行共享

此时就需要用到 kubernetes 的 volume 特性[1]了。

而 kubernetes 中的 volume 特性使用起来也是非常简单的,我们只需要在 pod template spec 中指定我们希望使用的 volume,以及这个 volume 希望被这个 pod 中的每一个 container 以什么样子的方式来使用就好了。

举个栗子

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
apiVersion: v1
kind: Pod
metadata:
name: test-volume
spec:
containers:
- name: test-container1
image: busybox
volumeMounts:
- name: config-vol
mountPath: /etc/config
- name: test-container2
image: busybox
volumeMounts:
- name: config-vol
mountPath: /mnt/config
- name: data
mountPath: /data
volumes:
- name: config-vol
configMap:
name: log-config
- name: data
persistentVolumeClaim:
claimName: test-pvc

上面的这个 pod 其实就使用了两个 volume,分别叫做

  • config-vol
  • data

而这两个 volume 分别是用不同的方式提供的

  • config-vol 的数据是由一个叫做 log-config 的 configmap 来提供的
  • data 的数据则是由一个叫做 test-pvc 的 persistentVolumeClaim 对象来提供的

而这个 pod 中的两个 container 在使用两个 volume 时的使用姿势也是有差别的

  • test-container1 仅仅使用了 config-vol 这个 volume,并且挂载到了自己的 /etc/config 路径下面
  • test-container2 也使用了 config-vol 这个 volume,但是它挂载到了自己的 /mnt/config 路径下面;另外它也是用了 data 这个 volume,并且挂载到了 /data 目录下面。

所以总结来看 kubernetes 中的 volume 是为了满足 pod 的各种数据存储需求所提供的特性。

  • 每一个 pod 使用的 volume 背后可能由不同的存储方式来提供的,比如 configmap,emptyDir,persistentVolumeClaim 等等
  • pod 中的每一个 container 都可以用自己的方式来使用这个 pod 中的任意一个 volume

而至于这个 volume 中的数据的生命周期,则跟 volume 背后是由哪种存储方式提供强相关。

  • 但是可以保证一点的是,无论采用哪种存储方式提供,volume 机制都能够保证在 container 发生重启的时候数据不会丢失。

常见的 Volume 类型

在上一节中我们有一个非常重要的结论:

  • volume 中的数据的生命周期,则跟 volume 背后是由哪种存储方式提供强相关。

所以根据我们的业务场景的中对数据的持久性,读写性能等等的不同需求,我们也需要选择不同的 Volume 类型来满足

比如我们平时比较常见的 Volume 类型包括

  • configmap:常用于为业务提供启动配置文件,这种小文件读取的需求;特点是只读,并且生命周期独立于 pod,即 pod 消失 configmap 仍旧存在
  • emptyDir:常用于同一个 pod 中的多个 container 进行少量文件的数据共享;特点是可读写,但是读写性能有限(取决于 kubelet 业务数据盘背后采用什么存储),并且没有 quota 限制,生命周期和 pod 完全一致,pod 被销毁,emptyDir 也会被删除
  • hostPath: 常用于 pod 希望访问主机上的某一个路径的场景,特点是可读写,生命周期独立于 pod
  • cephfs/nfs/rbd: 常用于 pod 希望使用网络存储来对数据进行持久化的场景
  • persistentVolumeClaim[2]:最新的一种让 pod 申请 volume 存储的形式,pod 可以通过创建一个 persistentVolumeClaim 对象来指明自己对这块 volume 存储的需求,比如大小,存储类型(比如 nfs,ceph rbd),生命周期完全独立于 pod。

PersistentVolumeClaim 机制

在上小节我们提到的 volume 的类型很丰富,包括 nfs,ceph rbd,cephfs,awsElasticBlockStore 等等,这些类型在 persistentVolumeClaim 概念出现之前是可以直接在 pod volume 字段中直接写明对这种 volume 的需求的,比如 cephfs,如果不采用 persistentVolumeClaim 机制的话,你需要在 volume 的字段中显式的写入跟这个 cephfs 的 volume 相关的配置,比如 monitors 的地址等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: cephfs
spec:
containers:
- name: cephfs-rw
image: kubernetes/pause
volumeMounts:
- mountPath: "/mnt/cephfs"
name: cephfs
volumes:
- name: cephfs
cephfs:
monitors:
- 10.16.154.78:6789
- 10.16.154.82:6789
- 10.16.154.83:6789
# by default the path is /, but you can override and mount a specific path of the filesystem by using the path attribute
# path: /some/path/in/side/cephfs
user: admin
secretFile: "/etc/ceph/admin.secret"
readOnly: true

但是引入 persistentVolumeClaim 机制后,这些跟存储类型相关的全局配置信息都可以被封装到一个叫做 storageClass[3] 的对象的描述中,比如针对上面的例子,我们可以创建一个叫做 cephfs 的 storageClass 类型

1
2
3
4
5
6
7
8
9
10
11
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: cephfs
parameters:
adminId: admin
adminSecretName: ceph-admin-secret
adminSecretNamespace: kube-system
monitors: 192.168.213.25:6789,192.168.213.27:6789,192.168.213.28:6789
provisioner: ceph.com/cephfs
reclaimPolicy: Delete

此时,如果用户还是想要申请一块 cephfs 类型的 volume 来使用的话,它只需要创建一个 persistentVolumeClaim 对象即可,并且该对象中应该写明,希望的 storageClass 为 cephfs,并且还有一些其他的配置要求,比如大小,读写模式等等

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cephfs-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Ti
storageClassName: cephfs

此时,这个 pod 的 volume 就可以修改一下改为使用这个 persistentVolumeClaim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: cephfs
spec:
containers:
- name: cephfs-rw
image: kubernetes/pause
volumeMounts:
- mountPath: "/mnt/cephfs"
name: cephfs
volumes:
- name: cephfs
persistentVolumeClaim:
claimName: cephfs-pvc

可见采用 persistentVolumeClaim 的好处有很多,不仅让接口更加清晰,也让用户对存储资源的管理更加方便。

persistentVolumeClaim 管理逻辑

persistentVolumeClaim 机制是现在比较常见的存储使用方式,对于它的理解将非常有助于我们解决对使用 pvc 的 pod 在启动,停止过程中遇到的各种问题。

那么当我们为一个 pod 创建了一个 pvc ,并且把这个 pod 和这个 pvc 绑定之后都发生了什么事情呢?都有哪些组件参与到了这个过程中呢?

我们在这一篇博文中仅粗略介绍一下这个 pvc 被申请,被初始化,再到被 pod 所使用的整个流程,并且每一个步骤我们还会通过单独的博文从代码的角度来分析具体的工作逻辑。并且我是针对我们公司使用的一个具体场景来进行介绍(ceph rbd with external provisioner)

整个 pvc 管理周期主要分为以下几个

  • Provisioning
  • Binding
  • Using

Provisioning

当 PVC 被创建后,第一步就是 Provisioning,PersistentVolumeClaim 从字面上来看也可以看出来,它只是一个对存储资源的需求的声明,针对这个声明,kubernetes 需要创造一个相应的“实体”,才能满足这个声明的需求。那么在 kubernetes 中,这个实体就是 PersistentVolume 对象。

所以如果想要 PersistentVolumeClaim 声明的存储需求被满足,我们就必须有能够满足需求的 PersistentVolume 存在,那么这个 PersistentVolume 是如何被创建出来的呢?

PersistentVolume 有两种途径被创建

  • static provisioning: 顾名思义,需要集群管理员,在 PersistentVolumeClaim 还没创建好的时候就要手动创建好一些 PersistentVolume

  • dynamic provisioning: 顾名思义,这种场景下,会有一个组件,去动态获取新创建的 PersistentVolumeClaim 信息,来直接动态创建和这个 pvc 需求相符的 PersistentVolume

其中 dynamic provisioning 是一种更加灵活的方式,社区中已经针对不同的存储技术开发了不同的组件来完成这个 dynamic provisioning 的流程,详情可以参考 https://github.com/kubernetes-incubator/external-storage

Binding

在 PersistentVolumeClaim 被创建后,而且也有了被创建好的 PersistentVolume 之后,就要开启 Binding 的步骤。很显然就是要为 PersistentVolumeClaim 选取最为合适的 PersistentVolume。PersistentVolumeClaim 和 PersistentVolume 是一对一的关系,不允许有一对多的关系存在。

所以当一个 PersistentVolumeClaim 申请了 100G 的容量时,但是目前系统中仅仅存在 200G 的 PersistentVolume 时,k8s 也会把二者 bind 起来

当然如果 PersistentVolume 是 dynamic provision 的方式创建出来的话,那么这个 PersistentVolumeClaim 和 PersistentVolume 之间的对应关系就已经被建立好了,无需再进行选择。

这个部分的工作使用 kube-controller-manager 中的 PvController 组件来完成的。

另外在 bind 之后,kube-controller-manager 中还存在一个 AttachDetachController,这个 controller 的工作是当某个 pvc 所对应的 pod 被调度到某台机器上面之后,AttachDetachController 会在逻辑上先把这个 PersistentVolume 和这个机器 attach 到一起,保证那些不支持多个 pod 共享的 pv 类型不会被同时两个 pod 所使用。

Using

最后一步就是 using,也就是说,当某一个使用这个 pvc 的 pod 被调度到某一台机器上以后,每台机器上面的 kubelet 会最终完成对这个 PersistentVolume 的实体化初始化,并且最终 mount 到这个 pod 的容器的指定目录里面去。

当然这个过程还有很多细节,比如 kubelet 会首先等待 AttachDetachController 把这个 PV 和所在的机器 attach 完成之后再进行后续操作,另外 PV 对应的 volume 会被首先 mount 到一个全局路径下,然后再 remount 到每一个 pod 的某个路径下。这样做是因为有些 PV 是可以支持被多个 Pod 所同时使用的,等等。这一系列的动作都是有 kubelet 中的 volumeManager 来完成的。

在后面的博文中,我们会针对 volume 整个生命周期中涉及到的所有组件进行一一解析,为大家还原 kubernetes 对 volume 的管理逻辑。

参考引用

  1. https://kubernetes.io/docs/concepts/storage/volumes
  2. https://kubernetes.io/docs/concepts/storage/persistent-volumes/
  3. https://kubernetes.io/docs/concepts/storage/storage-classes