在StatefulSet有状态应用中使用到了PV与PVC。
PV与PVC
PV 描述的,是持久化存储数据卷。 这个 API 对象主要定义的是一个持久化存储在宿主机上的目录,比如一个 NFS 的挂载目录。通常情况下,PV 对象是由运维人员事先创建在 Kubernetes 集群里待用的。一个示例如下:
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.244.1.4
path: "/"PVC 描述的,则是 Pod 所希望使用的持久化存储的属性。比如大小、可读写权限。PVC通常由开发人员创建,或者以PVC模板的方式称为StatefulSet的一部分,由StatefulSet控制器负责创建带编号的PVC。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs
spec:
accessModes:
- ReadWriteMany
storageClassName: manual
resources:
requests:
storage: 1Gi用户创建的PVC要想使用,就必须和某个符合条件的PV绑定起来,绑定需要两个条件:
- 两者的spec字段,比如PV的大小必须满足PVC的需求。
- PV和PVC的storageClassName字段必须一样。
成功绑定之后,用户就能够像使用Volume一样声明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: nfsPV和PVC的设计使得开发人员无需关注底层存储的实现。
PersistentVolumeController
如果Pod创建的时候,定义的PVC没有PV与它绑定,那么Pod的启动就会报错。运维发现这个错误会立即创建PV,k8s也会立即将没有绑定的PVC进行绑定。这个机制就是Volume Controller。
其实就是用来管理持久化存储的控制器,Volume Controller维护者多个控制循环,其中一个循环就是PersistentVolumeController。它会不断查看当前每一个PVC是否处于绑定状态,没有绑定,那么就会尝试遍历所有可用的PV来与它进行绑定。这样k8s保证了每个PVC都能很快绑定到合适的PV上去。
所谓将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上。
PV如何成为容器中的持久化存储
在容器概念中也介绍过Volume,其实就是将宿主机上的目录与容器上的一个目录绑定挂载了一起。而持久化就是说这个Volume:
- 不与宿主机绑定。就算容器删除,这个目录也不会被清理掉。就算容器在其它节点重启,也仍然能够挂载到这个Volume,访问到里面的数据。
所以大多数情况下,持久化Volume的实现都依赖于一个远程存储服务,比如远程文件存储,远程块存储等等。
k8s就是利用这些存储服务,来为用户提供一个持久化的宿主机目录让容器挂载,这样容器往这个目录中写数据都会保存在远程存储中。
这个持久化宿主机目录的过程,可以称为“两阶段处理”。
持久化宿主机目录
当一个Pod被调度到一个节点上后,kubelet就要负责为这个Pod创建它的Volume目录,默认路径如下:
/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>kubelet会根据Volume的类型来进行操作。如果你的 Volume 类型是远程块存储,比如 Google Cloud 的 Persistent Disk(GCE 提供的远程磁盘服务),那么 kubelet 就需要先调用 Goolge Cloud 的 API,将它所提供的 Persistent Disk 挂载到 Pod 所在的宿主机上。
这是两阶段中的第一阶段,在k8s中称为Attach。
Attach完成后,kubelet还要进行第二个操作:格式化这个磁盘设备,然后将它挂载到宿主机指定的挂载点上。这个挂载点就是前面创建的对应的目录。相当于执行:
# 通过lsblk命令获取磁盘设备ID
$ sudo lsblk
# 格式化成ext4格式
$ sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/<磁盘设备ID>
# 挂载到挂载点
$ sudo mkdir -p /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>将磁盘设备格式化并挂载到 Volume 宿主机目录的操作,对应的正是“两阶段处理”的第二个阶段,我们一般称为:Mount。
如果Volume类型是远程文件存储的话,kubelet能够直接从第二步开始处理,因为远程文件存储并没有一个“存储设备”需要挂载到宿主机上。直接将其远程服务器的目录挂载到对应的目录即可:
$ mount -t nfs <NFS服务器地址>:/ /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字> 经过这两步处理之后,就得到了一个持久化的宿主机目录,kubelet把这个Volume目录通过CRI里的Mounts参数传递给Docker,就可以为Pod里的容器挂载这个“持久化”的Volume了。相当于如下命令:
$ docker run -v /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>:/<容器内的目标目录> 我的镜像 ...删除PV的时候需要经过umount和dettach两阶段反向操作。
PV的两阶段处理与Pod和容器的处理没有太多的耦合,只要在kubelet调用CRI之前,确保持久化的宿主机目录已经处理完毕即可。在 Kubernetes 中,上述关于 PV 的“两阶段处理”流程,是靠独立于 kubelet 主控制循环(Kubelet Sync Loop)之外的两个控制循环来实现的。
处理PV的两个控制循环
第一阶段的attach(dettach)操作,是由Volume Controller负责维护的控制循环:AttachDetachController。而它的作用,就是不断地检查每一个 Pod 对应的 PV,和这个 Pod 所在宿主机之间挂载情况。从而决定,是否需要对这个 PV 进行 Attach(或者 Dettach)操作。
作为一个 Kubernetes 内置的控制器,Volume Controller 自然是 kube-controller-manager 的一部分。所以,AttachDetachController 也一定是运行在 Master 节点上的。因为Attach只需要调用公有云或者存储项目的API,并不需要在具体的宿主机上执行,所以没有问题。
而“第二阶段”的 Mount(以及 Unmount)操作,必须发生在 Pod 对应的宿主机上,所以它必须是 kubelet 组件的一部分。这个控制循环的名字,叫作:VolumeManagerReconciler,它运行起来之后,是一个独立于 kubelet 主循环的 Goroutine。
将Volume同kubelet的主控制循环解耦,避免了这些远程操作拖慢kubelet的主控制循环。kubelet 的一个主要设计原则,就是它的主控制循环绝对不可以被 block。
StorageClass
在实际生产中每个PVC都要有对应PV,这个PV是需要由运维人员来完成的,但是在大规模生产环境中,有成千上万的PVC,都要让运维人员来创建是非常麻烦的,更何况还不断有新的PVC被提交。
所以,Kubernetes 为我们提供了一套可以自动创建 PV 的机制,即:Dynamic Provisioning。
Dynamic Provisioning的核心就是创建一个名为StorageClass的API对象,这个API对象的作用就是创建PV模板。
StorageClass对象会定义如何两部分内容:
- PV的属性,比如存储类型、Volume的大小。
- 创建这种PV需要用到的插件,比如Ceph等等。
有了这些信息,k8s能够根据用户提交的PVC,然后找到一个对应的StorageClass,k8s就会调用StorageClass声明的存储插件,创建出需要的PV。
举个例子,假如我们的 Volume 的类型是 GCE 的 Persistent Disk 的话,运维人员就需要定义一个如下所示的 StorageClass:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: block-service
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd这里定义了一个名为block-service的storageClass。provisioner是存储插件的名字,而parameters就是PV的参数。
然后使用YAML文件创建StorageClass就行了
$ kubectl create -f sc.yaml然后在PVC里指定要使用的StorageClass就行了
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
storageClassName: block-service
resources:
requests:
storage: 30Gi那么创建这个PVC
$ kubectl create -f pvc.yaml就会绑定一个k8s自动创建的PV:
$ kubectl describe pvc claim1
Name: claim1
Namespace: default
StorageClass: block-service
Status: Bound
Volume: pvc-e5578707-c626-11e6-baf6-08002729a32b
Labels: <none>
Capacity: 30Gi
Access Modes: RWO
No Events.查看这个PV的信息就会发现这个PV也有StorageClass 字段,并且值与PVC的相同。这是因为,Kubernetes 只会将 StorageClass 相同的 PVC 和 PV 绑定起来。
$ kubectl describe pv pvc-e5578707-c626-11e6-baf6-08002729a32b
Name: pvc-e5578707-c626-11e6-baf6-08002729a32b
Labels: <none>
StorageClass: block-service
Status: Bound
Claim: default/claim1
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 30Gi
...
No events.有了Dynamic Provisioning机制,运维人员只需要创建数量优先的StorageClass对象就行了。这样开发人员提交PVC包含StorageClass字段就能够自动创建出PV了。
StorageClass 并不是专门为了 Dynamic Provisioning 而设计的。就是可以利用这个字段来掌控PV与PVC的绑定关系,就算没有对应的StorageClass也可。
集群已经开启了名叫 DefaultStorageClass 的 Admission Plugin,它就会为 PVC 和 PV 自动添加一个默认的 StorageClass;否则,PVC 的 storageClassName 的值就是“”,这也意味着它只能够跟 storageClassName 也是“”的 PV 进行绑定。
tags: 容器持久化存储