存储管理5:实践Local PV

在开始使用 Local Persistent Volume 之前,我们首先需要在集群里配置好磁盘或者块设备。在公有云上,这个操作等同于给虚拟机额外挂载一个磁盘,比如 GCE 的 Local SSD 类型的磁盘就是一个典型例子。而在我们部署的私有环境中,有两种办法来完成这个步骤。

  • 第一种,当然就是给宿主机挂载并格式化一个可用的本地磁盘,这也是最常规的操作;

  • 第二种,对于实验环境,你其实可以在宿主机上挂载几个 RAM Disk(内存盘)来模拟本地磁盘。

接下来,我们使用第二种方法,在本地实验环境的 Kubernetes 集群上进行实践。

1.1、准备本地磁盘

首先,在名叫 node1 的宿主机上创建一个挂载点,比如 /mnt/disks;然后,用几个 RAM Disk 来模拟本地磁盘,如下所示:

1
2
3
4
5
6
# 在node1上执行
$ mkdir /mnt/disks
$ for vol in vol1 vol2 vol3; do
mkdir /mnt/disks/$vol
mount -t tmpfs $vol /mnt/disks/$vol
done

需要注意的是,如果我们希望其他节点也能支持 Local Persistent Volume 的话,那就需要为它们也执行上述操作,并且确保这些磁盘的名字(vol1、vol2 等)都不重复。

1.2、创建PV

1.2.1、定义PV

接下来,我们就可以为这些本地磁盘定义对应的 PV 了,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
local:
path: /mnt/disks/vol1
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node1

可以看到,在这个 PV 的定义里:local 字段,指定了它是一个 Local Persistent Volume;而 path 字段,指定的正是这个 PV 对应的本地磁盘的路径,即:/mnt/disks/vol1。当然了,这也就意味着如果 Pod 要想使用这个 PV,那它就必须运行在 node1 上。所以,在这个 PV 的定义里,需要有一个 nodeAffinity 字段指定 node1 这个节点的名字。这样,调度器在调度 Pod 的时候,就能够知道PV 与节点的对应关系,从而做出正确的选择。这正是 Kubernetes 实现“在调度的时候就考虑 Volume 分布”的主要方法。

1.2.2、创建PV

接下来,我们就可以使用 kubectl create 来创建这个 PV,如下所示:

1
2
3
4
5
6
$ kubectl create -f local-pv.yaml 
persistentvolume/example-pv created

$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
example-pv 5Gi RWO Delete Available local-storage 16s

可以看到,这个 PV 创建后,进入了 Available(可用)状态。

1.3、创建StorageClass

正如我们在前面的文章中所建议的那样,使用 PV 和 PVC 的最佳实践,是要创建一个 StorageClass 来描述这个 PV.

1.3.1、定义StorageClass

首先,我们定义StorageClass,如下所示:

1
2
3
4
5
6
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

这个 StorageClass 的名字,叫作 local-storage。需要注意的是,在它的 provisioner 字段,我们指定的是 no-provisioner。这是因为 Local Persistent Volume 目前尚不支持 Dynamic Provisioning,所以它没办法在用户创建 PVC 的时候,就自动创建出对应的 PV。也就是说,我们前面创建 PV 的操作,是不可以省略的。

在这个 StorageClass 中还定义了一个 volumeBindingMode=WaitForFirstConsumer 的属性。它是 Local Persistent Volume 里一个非常重要的特性,即:延迟绑定。

1.3.2、理解延迟绑定机制

我们知道,当我们提交了 PV 和 PVC 的 YAML 文件之后,Kubernetes 就会根据它们俩的属性,以及它们指定的 StorageClass 来进行绑定。只有绑定成功后,Pod 才能通过声明这个 PVC 来使用对应的 PV。可是,如果使用的是 Local Persistent Volume 的话,就会发现,这个流程根本行不通。

比如,现在我们有一个 Pod,它声明使用的 PVC 叫作 pvc1。并且,我们规定,这个 Pod 只能运行在 node2 上。而在 Kubernetes 集群中,有两个属性(比如:大小、读写权限)相同的 Local 类型的 PV。其中,第一个 PV 的名字叫作 pv1,它对应的磁盘所在的节点是 node1。而第二个 PV 的名字叫作 pv2,它对应的磁盘所在的节点是 node2。

假设现在,Kubernetes 的 Volume 控制循环里,首先检查到了 pvc1 和 pv1 的属性是匹配的,于是就将它们俩绑定在一起。然后,我们用 kubectl create 创建了这个 Pod。这时候,问题就出现了。调度器看到,这个 Pod 所声明的 pvc1 已经绑定了 pv1,而 pv1 所在的节点是 node1,根据“调度器必须在调度的时候考虑 Volume 分布”的原则,这个 Pod 自然会被调度到 node1 上。可是,我们前面已经规定过,这个 Pod 根本不允许运行在 node1 上。所以。最后的结果就是,这个 Pod 的调度必然会失败。这就是为什么,在使用 Local Persistent Volume 的时候,我们必须想办法推迟这个“绑定”操作。

那么,具体推迟到什么时候呢?

答案是:推迟到调度的时候。

所以说,StorageClass 里的 volumeBindingMode=WaitForFirstConsumer 的含义,就是告诉 Kubernetes 里的 Volume 控制循环:虽然我们已经发现这个 StorageClass 关联的 PVC 与 PV 可以绑定在一起,但请不要现在就执行绑定操作(即:设置 PVC 的 VolumeName 字段)。而要等到第一个声明使用该 PVC 的 Pod 在调度器中出现后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。

这样,在上面的例子里,由于这个 Pod 不允许运行在 pv1 所在的节点 node1,所以它的 PVC 最后会跟 pv2 绑定,并且 Pod 也会被调度到 node2 上。所以,通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。

当然,在具体实现中,调度器实际上维护了一个与 Volume Controller 类似的控制循环,专门负责为那些声明了“延迟绑定”的 PV 和 PVC 进行绑定工作。通过这样的设计,这个额外的绑定操作,并不会拖慢调度器的性能。而当一个 Pod 的 PVC 尚未完成绑定时,调度器也不会等待,而是会直接把这个 Pod 重新放回到待调度队列,等到下一个调度周期再做处理。

1.3.3、创建StorageClass

在明白了这个机制之后,我们就可以创建 StorageClass 了,如下所示:

1
2
$ kubectl create -f local-sc.yaml 
storageclass.storage.k8s.io/local-storage created

1.4、创建PVC

接下来,我们只需要定义一个非常普通的 PVC,就可以让 Pod 使用到上面定义好的 Local Persistent Volume 了

1.4.1、定义PVC

PVC的定义如下所示:

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

可以看到,这个 PVC 没有任何特别的地方。唯一需要注意的是,它声明的 storageClassName 是 local-storage。所以,将来 Kubernetes 的 Volume Controller 在看到这个 PVC 的时候,不会为它进行绑定操作。

1.4.2、创建PVC

现在,我们来创建这个 PVC:

1
2
3
4
5
6
$ kubectl create -f local-pvc.yaml 
persistentvolumeclaim/example-local-claim created

$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
example-local-claim Pending local-storage 7s

可以看到,尽管这个时候,Kubernetes 里已经存在了一个可以与 PVC 匹配的 PV,但这个 PVC 依然处于 Pending 状态,也就是等待绑定的状态。

1.5、创建Pod

1.5.1、定义Pod

然后,我们编写一个 Pod 来声明使用这个 PVC,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kind: Pod
apiVersion: v1
metadata:
name: example-pv-pod
spec:
volumes:
- name: example-pv-storage
persistentVolumeClaim:
claimName: example-local-claim
containers:
- name: example-pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: example-pv-storage

这个 Pod 没有任何特别的地方,我们只需要注意,它的 volumes 字段声明要使用前面定义的、名叫 example-local-claim 的 PVC 即可。

1.5.2、创建Pod

我们一旦使用 kubectl create 创建这个 Pod,就会发现,我们前面定义的 PVC,会立刻变成 Bound 状态,与前面定义的 PV 绑定在了一起,如下所示:

1
2
3
4
5
6
$ kubectl create -f local-pod.yaml 
pod/example-pv-pod created

$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
example-local-claim Bound example-pv 5Gi RWO local-storage 6h

也就是说,在我们创建的 Pod 进入调度器之后,“绑定”操作才开始进行。

1.6、测试Local PV的持久性

这时候,我们可以尝试在这个 Pod 的 Volume 目录里,创建一个测试文件,比如:

1
2
3
$ kubectl exec -it example-pv-pod -- /bin/sh
# cd /usr/share/nginx/html
# touch test.txt

然后,登录到 node1 这台机器上,查看一下它的 /mnt/disks/vol1 目录下的内容,你就可以看到刚刚创建的这个文件:

1
2
3
# 在node1上
$ ls /mnt/disks/vol1
test.txt

而如果我们重新创建这个 Pod 的话,就会发现,我们之前创建的测试文件,依然被保存在这个持久化 Volume 当中:

1
2
3
4
5
6
7
$ kubectl delete -f local-pod.yaml 

$ kubectl create -f local-pod.yaml

$ kubectl exec -it example-pv-pod -- /bin/sh
# ls /usr/share/nginx/html
test.txt

这就说明,像 Kubernetes 这样构建出来的、基于本地存储的 Volume,完全可以提供容器持久化存储的功能。所以,像 StatefulSet 这样的有状态编排工具,也完全可以通过声明 Local 类型的 PV 和 PVC,来管理应用的存储状态。

1.7、删除Local PV的流程

需要注意的是,我们上面手动创建 PV 的方式,即 Static 的 PV 管理方式,在删除 PV 时需要按如下流程执行操作:

  • 删除使用这个 PV 的 Pod;

  • 从宿主机移除本地磁盘(比如,umount 它);

  • 删除 PVC;

  • 删除 PV。

如果不按照这个流程的话,这个 PV 的删除就会失败。

1.8、自动化

由于上面这些创建 PV 和删除 PV 的操作比较繁琐,Kubernetes 提供了一个 Static Provisioner 来帮助我们管理这些 PV。比如,我们所有的磁盘,都挂载在宿主机的 /mnt/disks 目录下。那么,当 Static Provisioner 启动后,它就会通过 DaemonSet,自动检查每个宿主机的 /mnt/disks 目录。然后,调用 Kubernetes API,为这些目录下面的每一个挂载,创建一个对应的 PV 对象出来。这些自动创建的 PV,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE
local-pv-ce05be60 1024220Ki RWO Delete Available local-storage 26s

$ kubectl describe pv local-pv-ce05be60
Name: local-pv-ce05be60
...
StorageClass: local-storage
Status: Available
Claim:
Reclaim Policy: Delete
Access Modes: RWO
Capacity: 1024220Ki
NodeAffinity:
Required Terms:
Term 0: kubernetes.io/hostname in [node-1]
Message:
Source:
Type: LocalVolume (a persistent volume backed by local storage on a node)
Path: /mnt/disks/vol1

这个 PV 里的各种定义,比如 StorageClass 的名字、本地磁盘挂载点的位置,都可以通过 provisioner 的配置文件指定。当然,provisioner 也会负责前面提到的 PV 的删除工作。而这个 provisioner 本身,其实是一个External Provisioner

1.9、参考

https://time.geekbang.org/column/intro/100015201


存储管理5:实践Local PV
https://kuberxy.github.io/2020/10/05/存储管理5:实践Local PV/
作者
Mr.x
发布于
2020年10月5日
许可协议