基本概念
PV
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: "/"
PVC
而 PVC
描述的,则是 Pod
所希望使用的持久化存储的属性。比如,Volume
存储的大小、可读写权限等等。
PVC
对象通常由开发人员创建;或者以 PVC 模板的方式成为 StatefulSet
的一部分,然后由 StatefulSet 控制器负责创建带编号的 PVC。
比如,开发人员可以声明一个 1 GiB
大小的 PVC
,如下所示:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs
spec:
accessModes:
- ReadWriteMany
storageClassName: manual
resources:
requests:
storage: 1Gi
而用户创建的 PVC 要真正被容器使用起来,就必须先和某个符合条件的 PV 进行绑定。这里要检查的条件,包括两部分:
1,当然是 PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须满足 PVC 的要求。
2,则是 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
可以看到,Pod
需要做的,就是在 volumes
字段里声明自己要使用的 PVC
名字。接下来,等这个 Pod
创建之后,kubelet 就会把这个 PVC 所对应的 PV,也就是一个 NFS 类型的 Volume,挂载在这个 Pod 容器内的目录上。
PVC
可以理解为持久化存储的“接口”,它提供了对某种持久化存储的描述,但不提供具体的实现;而这个持久化存储的实现部分则由 PV 负责完成。
在上面的讲述中,我们来看下另一个情况。
比如,你在创建 Pod 的时候,系统里并没有合适的 PV 跟它定义的 PVC 绑定,也就是说此时容器想要使用的 Volume 不存在。这时候,Pod 的启动就会报错。
但是,过了一会儿,运维人员赶紧创建了一个对应的 PV。这时候,我们当然希望 Kubernetes 能够再次完成 PVC 和 PV 的绑定操作,从而启动 Pod。
所以在 Kubernetes 中,实际上存在着一个专门处理持久化存储的控制器,叫作 Volume Controller
。这个 Volume Controller
维护着多个控制循环,其中有一个循环,扮演的就是撮合 PV 和 PVC 的角色。它的名字叫作 PersistentVolumeController
。
PersistentVolumeController
会不断地查看当前每一个 PVC,是不是已经处于 Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个“单身”的 PVC 进行绑定。这样,Kubernetes
就可以保证用户提交的每一个 PVC
,只要有合适的 PV 出现,它就能够很快进入绑定状态,从而结束“单身”之旅。
而所谓将一个 PV
与 PVC
进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC
对象的 spec.volumeName
字段上。所以,接下来 Kubernetes
只要获取到这个 PVC
对象,就一定能够找到它所绑定的 PV。
持久化存储
我们再来看 这个 PV 对象,又是如何变成容器里的一个持久化存储的呢.
在前面讲解容器基础的时候,已经详细剖析了容器 Volume 的挂载机制。用一句话总结,所谓容器的 Volume
,其实就是将一个宿主机上的目录,跟一个容器里的目录绑定挂载在了一起。
而所谓的“持久化 Volume”
,指的就是这个宿主机上的目录,具备“持久性”
。即:这个目录里面的内容,既不会因为容器的删除而被清理掉,也不会跟当前的宿主机绑定。这样,当容器被重启或者在其他节点上重建出来之后,它仍然能够通过挂载这个 Volume
,访问到这些内容。
显然,我们前面使用的 hostPath
和 emptyDir
类型的 Volume 并不具备这个特征:它们既有可能被 kubelet
清理掉,也不能被“迁移”
到其他节点上。
所以,大多数情况下,持久化 Volume
的实现,往往依赖于一个远程存储服务,比如:远程文件存储(比如,NFS、GlusterFS)、远程块存储(比如,公有云提供的远程磁盘)等等。
而 Kubernetes 需要做的工作,就是使用这些存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载时使用。而所谓“持久化”
,指的是容器在这个目录里写入的文件,都会保存在远程存储中,从而使得这个目录具备了“持久性”
。
两阶段处理
这个准备“持久化”
宿主机目录的过程,我们可以形象地称为“两阶段处理”
。
接下来,通过一个具体的例子来说明。
当一个 Pod 调度到一个节点上之后,kubelet 就要负责为这个 Pod 创建它的 Volume 目录。默认情况下,kubelet 为 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 所在的宿主机上。
这相当于执行:
$ gcloud compute instances attach-disk <虚拟机名字> --disk <远程磁盘名字>
这一步为虚拟机挂载远程磁盘的操作,对应的正是
“两阶段处理”
的第一阶段。在 Kubernetes 中,我们把这个阶段称为Attach
。
Attach 阶段完成后,为了能够使用这个远程磁盘,kubelet 还要进行第二个操作,即:格式化这个磁盘设备,然后将它挂载到宿主机指定的挂载点上。不难理解,这个挂载点,正是我在前面反复提到的 Volume 的宿主机目录。所以,这一步相当于执行:
# 通过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
。
Mount 阶段完成后,这个 Volume 的宿主机目录就是一个“持久化”的目录了,容器在它里面写入的内容,会保存在 Google Cloud 的远程磁盘中。
而如果你的 Volume 类型是远程文件存储(比如 NFS)的话,kubelet 的处理过程就会更简单一些。
因为在这种情况下,kubelet 可以跳过“第一阶段”(Attach)
的操作,这是因为一般来说,远程文件存储并没有一个“存储设备”
需要挂载在宿主机上。所以,kubelet 会直接从“第二阶段”(Mount)
开始准备宿主机上的 Volume 目录。
在这一步,kubelet 需要作为 client,将远端 NFS 服务器的目录(比如:“/”目录),挂载到 Volume 的宿主机目录上,即相当于执行如下所示的命令:
$ mount -t nfs <NFS服务器地址>:/ /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>
通过这个挂载操作,Volume 的宿主机目录就成为了一个远程 NFS 目录的挂载点,后面你在这个目录里写入的所有文件,都会被保存在远程 NFS 服务器上。所以,我们也就完成了对这个 Volume 宿主机目录的“持久化”
。
在具体的 Volume 插件的实现接口上,Kubernetes 分别给这两个阶段提供了两种不同的参数列表:对于“第一阶段”(Attach)
,Kubernetes 提供的可用参数是 nodeName
,即宿主机的名字。而对于“第二阶段”(Mount)
,Kubernetes 提供的可用参数是 dir,即 Volume 的宿主机目录。
所以,作为一个存储插件,你只需要根据自己的需求进行选择和实现即可。
而经过了“两阶段处理”
,我们就得到了一个“持久化”
的 Volume 宿主机目录。所以,接下来,kubelet 只要把这个 Volume 目录通过 CRI
里的 Mounts 参数
,传递给 Docker,然后就可以为 Pod 里的容器挂载这个“持久化”的 Volume 了。其实,这一步相当于执行了如下所示的命令:
$ docker run -v /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>:/<容器内的目标目录> 我的镜像 ...
备注:对应地,在删除一个 PV 的时候,Kubernetes 也需要 Unmount 和 Dettach 两个阶段来处理。
实现控制器
实际上,你可能已经发现,这个 PV 的处理流程似乎跟 Pod 以及容器的启动流程没有太多的耦合,只要 kubelet 在向 Docker 发起
CRI
请求之前,确保“持久化”的宿主机目录已经处理完毕即可。
所以,在 Kubernetes 中,上述关于 PV 的“两阶段处理”
流程,是靠独立于 kubelet 主控制循环(Kubelet Sync Loop
)之外的两个控制循环来实现的。
其中,“第一阶段”
的 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 的主循环解耦,Kubernetes 就避免了这些耗时的远程挂载操作拖慢 kubelet
的主控制循环,进而导致 Pod 的创建效率大幅下降的问题。
实际上,kubelet 的一个主要设计原则,就是它的主控制循环绝对不可以被 block。
StorageClass
我在前面介绍 PV 和 PVC 的时候,曾经提到过,PV 这个对象的创建,是由运维人员完成的。但是,在大规模的生产环境里,这其实是一个非常麻烦的工作。
这是因为,一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC,这就意味着运维人员必须得事先创建出成千上万个 PV。更麻烦的是,随着新的 PVC 不断被提交,运维人员就不得不继续添加新的、能满足条件的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败。在实际操作中,这几乎没办法靠人工做到。
所以,Kubernetes 为我们提供了一套可以自动创建 PV 的机制,即:
Dynamic Provisioning
。
相比之下,前面人工管理 PV 的方式就叫作
Static Provisioning
。
Dynamic Provisioning
机制工作的核心,在于一个名叫 StorageClass
的 API 对象。而 StorageClass
对象的作用,其实就是创建 PV 的模板。
具体地说,StorageClass
对象会定义如下两个部分内容:
1,PV 的属性。比如,存储类型、Volume 的大小等等。
2,创建这种 PV 需要用到的存储插件。比如,Ceph 等等。
有了这样两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass
了。然后,Kubernetes 就会调用该 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
在这个 YAML 文件里,我们定义了一个名叫 block-service
的 StorageClass
。这个 StorageClass
的 provisioner
字段的值是:kubernetes.io/gce-pd
,这正是 Kubernetes 内置的 GCE PD 存储插件的名字。
而这个 StorageClass
的 parameters
字段,就是 PV 的参数。比如:上面例子里的 type=pd-ssd
,指的是这个 PV 的类型是“SSD 格式的 GCE 远程磁盘”。
有了 StorageClass 的 YAML 文件之后,运维人员就可以在 Kubernetes 里创建这个 StorageClass 了:
有了 StorageClass 的 YAML 文件之后,运维人员就可以在 Kubernetes 里创建这个 StorageClass 了:
这时候,作为应用开发者,我们只需要在 PVC 里指定要使用的 StorageClass 名字即可,如下所示:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
storageClassName: block-service
resources:
requests:
storage: 30Gi
可以看到,我们在这个 PVC 里添加了一个叫作 storageClassName 的字段,用于指定该 PVC 所要使用的 StorageClass 的名字是:block-service
。
下面可以一起来实践一下这个过程:
$ kubectl create -f pvc.yaml
可以看到,我们创建的 PVC 会绑定一个 Kubernetes 自动创建的 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 的属性,你就可以看到它跟我们在 PVC 里声明的存储的属性是一致的,如下所示:
$ 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.
此外,你还可以看到,这个自动创建出来的 PV 的 StorageClass 字段的值,也是 block-service。这是因为,Kubernetes 只会将 StorageClass
相同的 PVC 和 PV 绑定起来。
有了
Dynamic Provisioning
机制,运维人员只需要在 Kubernetes 集群里创建出数量有限的StorageClass
对象就可以了。这就好比,运维人员在 Kubernetes 集群里创建出了各种各样的 PV 模板。这时候,当开发人员提交了包含 StorageClass 字段的 PVC 之后,Kubernetes 就会根据这个 StorageClass 创建出对应的 PV。
小结
我们用如下所示的一幅示意图描述之间的关系:
1,PVC 描述的,是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等。
2,PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。
3,而 StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass 的 PV 和 PVC,才可以绑定在一起。
StorageClass
的另一个重要作用,是指定 PV 的 Provisioner
(存储插件)。这时候,如果你的存储插件支持 Dynamic Provisioning
的话,Kubernetes 就可以自动为你创建 PV 了。
本地持久化卷
Kubernetes 能够直接使用宿主机上的本地磁盘目录,而不依赖于远程存储服务,来提供“持久化”的容器 Volume。
这样做的好处很明显,由于这个 Volume 直接使用的是本地磁盘,尤其是 SSD 盘,它的读写性能相比于大多数远程存储来说,要好得多。这个需求对本地物理服务器部署的私有 Kubernetes 集群来说,非常常见。
Kubernetes 在 v1.10 之后,就逐渐依靠 PV、PVC 体系实现了这个特性。这个特性的名字叫作:
Local Persistent Volume
。
不过,首先需要明确的是,Local Persistent Volume
并不适用于所有应用。事实上,它的适用范围非常固定,比如:高优先级的系统应用,需要在多个不同节点上存储数据,并且对 I/O 较为敏感。典型的应用包括:分布式数据存储比如 MongoDB、Cassandra 等,分布式文件系统比如 GlusterFS、Ceph 等,以及需要在本地磁盘上进行大量数据缓存的分布式应用。
其次,相比于正常的 PV,一旦这些节点宕机且不能恢复时,Local Persistent Volume
的数据就可能丢失。这就要求使用 Local Persistent Volume
的应用必须具备数据备份和恢复的能力,允许你把这些数据定时备份在其他位置。
设计难点
Local Persistent Volume 的设计,主要面临两个难点。
第一个难点在于:如何把本地磁盘抽象成 PV。
首先我们绝不应该把一个宿主机上的目录当作 PV 使用。这是因为,这种本地目录的存储行为完全不可控,它所在的磁盘随时都可能被应用写满,甚至造成整个宿主机宕机。而且,不同的本地目录之间也缺乏哪怕最基础的 I/O 隔离机制。
所以,一个
Local Persistent Volume
对应的存储介质,一定是一块额外挂载在宿主机的磁盘或者块设备(“额外”的意思是,它不应该是宿主机根目录所使用的主硬盘)。这个原则,我们可以称为“一个 PV 一块盘”
。
第二个难点在于:调度器如何保证 Pod 始终能被正确地调度到它所请求的 Local Persistent Volume 所在的节点上呢?
造成这个问题的原因在于,对于常规的 PV 来说,Kubernetes 都是先调度 Pod 到某个节点上,然后,再通过“两阶段处理”来“持久化”这台机器上的 Volume 目录,进而完成 Volume 目录与容器的绑定挂载。
但是,对于 Local PV 来说,节点上可供使用的磁盘(或者块设备),必须是运维人员提前准备好的。它们在不同节点上的挂载情况可以完全不同,甚至有的节点可以没这种磁盘。
所以,这时候,调度器就必须能够知道所有节点与 Local Persistent Volume
对应的磁盘的关联关系,然后根据这个信息来调度 Pod。
这个原则,我们可以称为“在调度的时候考虑 Volume 分布”
。在 Kubernetes 的调度器里,有一个叫作 VolumeBindingChecker
的过滤条件专门负责这个事情。在 Kubernetes v1.11 中,这个过滤条件已经默认开启了。
基于上述讲述,在开始使用 Local Persistent Volume
之前,你首先需要在集群里配置好磁盘或者块设备。在公有云上,这个操作等同于给虚拟机额外挂载一个磁盘,比如 GCE 的 Local SSD 类型的磁盘就是一个典型例子。
实践
接下来,我们使用 RAM Disk
(内存盘)来模拟本地磁盘,在集群上进行实践。
首先,在名叫 node-1
的宿主机上创建一个挂载点,比如 /mnt/disks
;然后,用几个 RAM Disk 来模拟本地磁盘,如下所示:
# 在node-1上执行
$ 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
等)都不重复。
接下来,我们就可以为这些本地磁盘定义对应的 PV 了,如下所示:
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/vol1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-1
可以看到,这个 PV 的定义里:local
字段,指定了它是一个 Local Persistent Volume
;而 path
字段,指定的正是这个 PV 对应的本地磁盘的路径,即:/mnt/disks/vol1
。
当然了,这也就意味着如果 Pod 要想使用这个 PV,那它就必须运行在 node-1
上。所以,在这个 PV 的定义里,需要有一个 nodeAffinity
字段指定 node-1
这个节点的名字。这样,调度器在调度 Pod 的时候,就能够知道一个 PV 与节点的对应关系,从而做出正确的选择。这正是 Kubernetes 实现“在调度的时候就考虑 Volume 分布”的主要方法
。
接下来,我们就可以使用 kubect create 来创建这个 PV,如下所示:
$ 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(可用)状态。
而正如我在上一篇文章里所建议的那样,使用 PV 和 PVC 的最佳实践,是你要创建一个 StorageClass
来描述这个 PV,如下所示:
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
里一个非常重要的特性,即:延迟绑定
。
我们知道,当你提交了 PV 和 PVC 的 YAML 文件之后,Kubernetes 就会根据它们俩的属性,以及它们指定的 StorageClass 来进行绑定。只有绑定成功后,Pod 才能通过声明这个 PVC 来使用对应的 PV。
可是,如果你使用的是 Local Persistent Volume
的话,就会发现,这个流程根本行不通。
比如,现在你有一个 Pod,它声明使用的 PVC 叫作 pvc-1。并且,我们规定,这个 Pod 只能运行在 node-2 上。
假设现在,Kubernetes 的 Volume 控制循环里,首先检查到了 pvc-1 和 pv-1 的属性是匹配的,于是就将它们俩绑定在一起。
然后,你用 kubectl create 创建了这个 Pod。
调度器看到,这个 Pod 所声明的 pvc-1 已经绑定了 pv-1,而 pv-1 所在的节点是 node-1,根据“调度器必须在调度的时候考虑 Volume 分布”
的原则,这个 Pod 自然会被调度到 node-1 上。
可是,我们前面已经规定过,这个 Pod 根本不允许运行在 node-1 上。所以。最后的结果就是,这个 Pod 的调度必然会失败。
这就是为什么,在使用 Local Persistent Volume 的时候,我们必须想办法推迟这个“绑定”操作。
调度时绑定
那么,具体推迟到什么时候呢?答案是:推迟到调度的时候
。
所以,StorageClass
里的 volumeBindingMode=WaitForFirstConsumer
的含义,就是告诉 Kubernetes 里的 Volume 控制循环:虽然你已经发现这个 StorageClass
关联的 PVC 与 PV 可以绑定在一起,但请不要现在就执行绑定操作(即:设置 PVC 的 VolumeName 字段
)。
而要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。
这样,在上面的例子里,由于这个 Pod
不允许运行在 pv-1
所在的节点 node-1
,所以它的 PVC 最后会跟 pv-2
绑定,并且 Pod 也会被调度到 node-2
上。
所以,通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。
通过这样的设计,这个额外的绑定操作,并不会拖慢调度器的性能。而当一个 Pod 的 PVC 尚未完成绑定时,调度器也不会等待,而是会直接把这个 Pod 重新放回到待调度队列,等到下一个调度周期再做处理。
在明白了这个机制之后,我们就可以创建 StorageClass
了,如下所示:
$ kubectl create -f local-sc.yaml
storageclass.storage.k8s.io/local-storage created
接下来,我们只需要定义一个非常普通的 PVC,就可以让 Pod 使用到上面定义好的 Local Persistent Volume 了,如下所示:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: example-local-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-storage
可以看到,这个 PVC 没有任何特别的地方。唯一需要注意的是,它声明的 storageClassName
是 local-storage
。所以,将来 Kubernetes 的 Volume Controller 看到这个 PVC 的时候,不会为它进行绑定操作。
现在,我们来创建这个 PVC:
$ 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
状态,也就是等待绑定的状态。
然后,我们编写一个 Pod 来声明使用这个 PVC,如下所示:
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 即可。
而我们一旦使用 kubectl create 创建这个 Pod,就会发现,我们前面定义的 PVC,会立刻变成 Bound
状态,与前面定义的 PV 绑定在了一起,如下所示:
$ 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 进入调度器之后,“绑定”操作才开始进行。
这时候,我们可以尝试在这个 Pod 的 Volume 目录里,创建一个测试文件,比如:
$ kubectl exec -it example-pv-pod -- /bin/sh
# cd /usr/share/nginx/html
# touch test.txt
然后,登录到 node-1
这台机器上,查看一下它的 /mnt/disks/vol1
目录下的内容,你就可以看到刚刚创建的这个文件:
# 在node-1上
$ ls /mnt/disks/vol1
test.txt
而如果你重新创建这个 Pod 的话,就会发现,我们之前创建的测试文件,依然被保存在这个持久化 Volume 当中:
$ 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
# touch test.txt
这就说明,像 Kubernetes 这样构建出来的、基于本地存储的 Volume,完全可以提供容器持久化存储的功能。所以,像 StatefulSet
这样的有状态编排工具,也完全可以通过声明 Local 类型的 PV 和 PVC,来管理应用的存储状态。
删除
需要注意的是,我们上面手动创建 PV 的方式,即 Static 的 PV 管理方式,在删除 PV 时需要按如下流程执行操作:
1,删除使用这个 PV 的 Pod;
2,从宿主机移除本地磁盘(比如,umount 它);
3,删除 PVC;
4,删除 PV。
如果不按照这个流程的话,这个 PV 的删除就会失败。
当然,由于上面这些创建 PV 和删除 PV 的操作比较繁琐,Kubernetes 其实提供了一个
Static Provisioner
来帮助你管理这些 PV。
那么,当 Static Provisioner
启动后,它就会通过 DaemonSet
,自动检查每个宿主机的 /mnt/disks
目录。然后,调用 Kubernetes API,为这些目录下面的每一个挂载,创建一个对应的 PV 对象出来。这些自动创建的 PV。