PV和PVC StorageClass的各种关系

在前面的文章中,分析了Kubernetes的各种编排能力

再这些编排中,比较麻烦的,莫过于对其状态的管理,最常见的状态,莫过于存储状态了

所以,我们通过几篇文章来剖析Kuberentes项目处理容器持久化存储的核心原理,其中,PV描述的是,持久化存储数据卷,这个API对象定义的主要是一个持久化存储在宿主机上的目录,比如一个NFS的挂载目录

通常情况下,PV是交由运维人员事先创建在Kubernetes集群中等待使用的,比如,运维人员可以定义一个NFS类型的PV,

apiVersion: v1

kind: PersistentVolume

metadata:

name: nfs

spec:

storageClassName: manual

capacity:

storage: 1Gi

accessModes:

– ReadWriteMany

nfs:

server: 10.244.1.4

path: “/”

而PV,C则是由开发人员,以模板的方式交给StatefulSet,然后由StatefulSet控制器创建带有编号的PVC

如下所示

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: nfs

spec:

accessModes:

– ReadWriteMany

storageClassName: manual

resources:

requests:

storage: 1Gi

而用户创建的PVC真正的被容器使用起来,必须要先和某个符合条件的PV进行绑定,这里面要检查的条件,包含两个部分

第一个条件,PV和PVC的spec字段,比如PV存储的大小,必须要满足PVC的要求

第二个条件,则是PV和PVC的storageClassName必须一致

成功的将PVC和PV进行绑定之后,Pod就能像是使用hostPath等常规的类型的Volume一样,在自己的YAML文件中声明使用PVC了

apiVersion: v1

kind: Pod

metadata:

labels:

role: web-frontend

spec:

containers:

– name: web

image: nginx

ports:

– name: web

containerPort: 80

volumeMounts:

– name: nfs

mountPath: “/usr/share/nginx/html”

volumes:

– name: nfs

persistentVolumeClaim:

claimName: nfs

有了这个PVC,那么,等这个Pod创建了之后,kubectl就绑定一个PV,挂载到这个Pod的目录上

这个PVC和PV的设计,其实和面向对象的思想完全一致

PVC可以理解为持久化存储的接口,提供了某种持久化存储的描述,不提供具体的实现,具体的持久化的实现部分交给PV负责完成

这样,作为开发者的我们,就不太需要关系PVC的具体实现的PV究竟是NFS还是Ceph

而且,为了避免一种情况,比如创建Pod的时候,系统没有合适的PV和PVC绑定,所以此时容器想要使用Volume不存在,Pod就会报错

但是过了一会,再进行创建的话,是否会再次进行绑定呢?

所以在Kubernetes中,实际上存在着一个专门处理持久化存储的控制器,叫做Volume Controller,这个Volume Controller维护着多个控制循环,其中有个循环,就负责PV和PVC的匹配绑定,名字叫做PersistentVolumeController,会不断的查看每个PVC,是不是已经处于Bound状态,如果不是,就会遍历所有的PV,尝试和这个PVC进行绑定,这样Kubernetes就能保证用户提交的每一个PVC,只要有合适的PV出现,就能进入绑定的状态

而将一个PV和PVC进行绑定的操作,就是将这个PV对象的名字,填在了PVC的spec.volumeName字段上,所以Kuberentes上只要使用PVC进行存储的时候,就能找到绑定的PV

这个PV对象,如何变成容器中的一个持久化存储的呢?

所谓的Volume,就是宿主机上的目录,具有持久性罢了,这个目录里面的内容,既不会因为容器的删除而被清理,也不会因为和当前的宿主机进行绑定,每当容器被重启或者从其他的节点上重建的时候,仍然可以挂载这个Volume,访问这些内容

这就是比起原生的hostpath和emptyDir的Volume不具备的能力,其可能被kubelet清理掉,也不能被迁移到其他的节点上

所以,大部分的Volume的实现,依赖于一个远程存储服务,比如远程文件存储NFS GlusterFS,远程块存储,公有云的远程磁盘等

而Kubernetes需要的工作,就是使用这些存储服务,来为容器准备一个持久化的目录,可以绑定使用,这样的持久化,就是容器生成的文件,就会保存在远程存储中,具有持久性

准备持久化宿主机的目录,可以归为二阶段处理

当一个Pod调度到一个节点上,Kubectl 为负责的Pod创建Volueme目录,默认下Kubelet为Volume创建的目录是一个宿主机上的路径

/var/lib/kubelet/pods/<POD的ID>/volumes/kubernetes.io~<Volumes类型>/<Volume名字>

接下来Kuberentes的操作取决于Volume类型了

如果Volume是远程块的存储,比如Persistent Disk,那么Kubelet就需要先调用Google Cloud的API,将其提供的Persistent Disk 挂载到Pod所在的宿主机上

这就好比是一块新的磁盘挂载到系统上了

就是相当于使用了命令

gcloud compute instances attach-disk<虚拟机名字> –disk<远程磁盘名字>

这一步为虚拟机挂载远程磁盘的操作,对应的正式两阶段处理的第一阶段,在Kubernetes中,这一阶段被称为Attach

Attach这个阶段完成后,为了能够使用这个远程磁盘,还需要第二个操作,也就是第二阶段

就是将新挂载的磁盘进行格式化,让后将其挂载到宿主机指定的挂载点,这个挂载点,就是我们提到的Volume的宿主机的目录,所以这一步相当于执行一个格式化命令,然后进行挂载到挂载点

这一步的操作,叫做Mount

这个Mount阶段完成后,Volume的宿主机目录就是一个持久化的目录,容器在里面写入的内容,保存在Google Cloud的远程磁盘

如果是类似文件存储的话,诸如NFS,kubelet的处理过程会简单些

这样就不会去进行Attach的操作,这是因为,远程文件存储,是不需要挂载一个存储设备到宿主机上的,然后,Kubelet会直接从第二阶段,开始准备宿主机上的Volume目录

这一步,kubelet需要作为client,将远程NFS服务器的目录,挂载到Volume的宿主机目录上,相当于如下的命令

$ mount -t nfs <NFS服务器地址>:/ /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>

挂载操作,Volume的宿主机目录就成了一个远程NFS目录的挂载点,后面所有的文件,都会被保存在远程NFS服务器上,就完成了这个Volume宿主机目录的持久化

那么Kubernetes是如何区分两个阶段的呢?

第一个阶段,Kuberenetes提供的可用参数,是nodeName,是宿主机的名字

第二个阶段,Kubernetes提供的参数是dir,即Volume的宿主机目录

作为一个存储插件,可以根据自己的需求进行选择和实现

这样,我们得到了一个持久化的Volume的宿主机目录,所以Kubeley只要将这个Volume的目录通过CRI的Mounts参数,传给Doker,就可以为Pod中的容器挂载这个持久化的Volume了,这就好比执行了如下命令

docker run -v /var/lib.kubelet/pods/<Pod的ID>/volumes/kuberetnetes.io~<Volumes类型>/<Volume名字>

这就是Kubernetes处理PV的具体流程了

对应的,删除一个PV的时候,也需要进行Unmount和Dettach阶段了

这个PV的处理流程和容器的启动流程耦合度不深,只需要Kubectl在向着Docker发起CRI请求之前,确保持久化的宿主机目录已经处理完毕即可

所以,上面的PV的二阶段处理,是独立于kubelet主循环控制的两个控制循环来实现的,

其中第一阶段的Attach的操作,是交给Voloume Controller负责维护的,这个控制循环的AttachDetachController,这个作用,就是不断的检查每一个Pod对应的PV,是否已经挂载,然后判断是否需要进行Attach和Dettach操作

第二阶段的Mount和Unmount操作,必须要发生在Pod对应的宿主机上,必须要是kubelet组件的一部分,这个控制循环名字,叫做VolumeMangerReconciler,这是一个独立于主循环的线程执行

这样Volume的绑定,是利用了异步的方式,避免了创建的效率低下的问题,而且必须要说一句,kubelet的主线程的运行,是绝对不能被block的

我们最后了解storageClass的概念

在介绍PV和PVC的时候,PV一直被认为是由运维人员进行创建的,可以这样理解,但是如果PV对象多了,在一个大规模环境中,是一个非常麻烦的事,毕竟不能每一个PVC都由运维人员手动创建一个PV去绑定,这是无法靠人工做到的,做到了也太麻烦了

Kubernetes提供了一个API对象,就是StorageClass,也就是创建PV对象的模板

StorageClass对象定义了两个部分的内容

PV的属性,比如存储类型 Volume的大小

创建这种PV所需要的存储插件,比如Ceph等

有了这两个信息,Kubernetes就可以根据PVC,自动创建PV

加入,我们Volume的类型是GCE的Disk,那么我们想要自动的创建PV,只需要定义个如下的所示StrageClass

apiVersion: storage.k8s.io/v1

kind: StorageClass

metadata:

name: block-service

provisioner: kubernetes.io/gce-pd

parameters:

type: pd-ssd

这就定义了一个名为block-service的StorageClass,这个StorageClass的provisioner的字段的值为kubernetes.io/gce-pd,正是Kubernetes内置的GCE PD插件的名字

还有就是我们的Rook存储服务,可以定义如下的StorageClass

apiVersion: ceph.rook.io/v1beta1

kind: Pool

metadata:

name: replicapool

namespace: rook-ceph

spec:

replicated:

size: 3

apiVersion: storage.k8s.io/v1

kind: StorageClass

metadata:

name: block-service

provisioner: ceph.rook.io/block

parameters:

pool: replicapool

#The value of “clusterNamespace” MUST be the same as the one in which your rook cluster exist

clusterNamespace: rook-ceph

这样,我们定义了一个名为block-service的StorageClass,不过使用的插件式Rook

这样,我们就可以在PVC中声明指定的StorageClass名字,就和指定PV名字一样

apiVersion: v1

kind: PersistentVolumeClaim

metadata:

name: claim1

spec:

accessModes:

– ReadWriteOnce

storageClassName: block-service

resources:

requests:

storage: 30Gi

我们在PVC中添加了一个叫做storageClassName的字段,用于指定该PVC使用的StorageClass的名字,block-service

这样,我们创建了PVC,SGC就会自动的调用对应的API,创建对应的pv对象,并绑定对应的PV

有了这个Dynamic Provisioning机制,运维人员只需要在Kubernetes中创建几个StorageClass对象,然后就可以自动绑定对应的PVC了

如果不希望使用StorageClass,那么大可以去声明一个不存在的StorageClass作为StorageClassName,这样,进行的就是Static Provisioning定义,这样做的好处也很明显,PVC和PV的绑定关系,完全在自我掌握之中

如果,没有声明StorageClass的话,那么如果集群开启了DefaultStorageClass的Admission Plugin,就会为PVC和PV自动添加一个默认的StorageClass;

我们今天了解了一下PVC和PV的设计和实现原理,并且阐述了StorageClass是干啥用的,逻辑概念如下所示

图片

PVC描述的是Pod想要使用的是持久化存储的属性,比如存储的大小,读写权限

PV描述的是一个具体的Volume的属性,比如类型,挂载目录,远程服务器地址

StorageClass的作用,就是充当PV的模板,并且只有同属于一个StorageClass的PV和PVC,才能绑定再一次

Storage的另一个重要作用,就是指定PV的Provisioner存储插件,这时候,存储插件Dynamic Provisioning 就会自动创建PV了

最后留一个思考,在了解了PV和PVC的设计和实现原理之后,是否觉着有PV和PVC有种过度设计的嫌疑?

在格式化中,只有Raw格式需要格式

对于这个问题,为了避免开发人员过多的接触远程存储的实现细节,我们将其抽象出了pv和pvc的概念,开发人员只需要声明自己需要的远程存储信息即可,具体实现由PV实现,而为了避免PV生成的繁琐,我们利用了StorageClass,将其动态分配,动态绑定,避免了运维人员的操作过于繁琐,但是如果声明多个PVC共用一个PV的话,会不会产生磁盘竞争呢?

发表评论

邮箱地址不会被公开。 必填项已用*标注