# 一. 简介
PVC 和 PV 的设计,其实跟“面向对象”的思想完全一致。PVC 可以理解为持久化存储的“接口”,它提供了对某种持久化存储的描述,但不提供具体的实现,而 PV 负责完成持久化存储的实现。而StorageClass 对象的作用,其实就是创建 PV 的模板,减少手动操作的重复工作以及错误情况。
关于本文的项目的代码,都放于链接:[GitHub资源](https://github.com/wyattup/devops/tree/main/demos/kubernetes/PV)
# 二. PV
## 2.1 Volume
容器的 Volume,其实就是将一个宿主机上的目录,跟一个容器里的目录绑定挂载在了一起。
而所谓的“持久化 Volume”,指的就是这个宿主机上的目录,具备“持久性”。即:这个目录里面的内容,既不会因为容器的删除而被清理掉,也不会跟当前的宿主机绑定。
而我们常见的 `hostPath` 和 `emptyDir` 类型的 Volume 并不具备这个特征:它们既有可能被 kubelet 清理掉,也不能被“迁移”到其他节点上。这也是我们需要使用PV与PVC的原因。
## 2.2 PV持久化
PV的持久化可以分为2阶段流程:
- Attach阶段
这一步为虚拟机挂载远程磁盘的操作。Kubernetes 提供的可用参数是 `nodeName`,即宿主机的名字。在 Kubernetes 中,我们把这个阶段称为 `Attach`。
- Mount
这个将磁盘设备格式化并挂载到 Volume 宿主机目录。Kubernetes 提供的可用参数是 `dir`,即 Volume 的宿主机目录。在 Kubernetes 中,我们把这个阶段称为 `Mount`。
经过了“两阶段处理”,我们就得到了一个“持久化”的 Volume 宿主机目录。kubelet 只要把这个 Volume 目录通过 `CRI` 里的 `Mounts` 参数,传递给 `Docker`,然后就可以为 Pod 里的容器挂载这个“持久化”的 Volume 了。
## 2.3 PV与PVC绑定控制器
`PersistentVolumeController` 会不断地查看当前每一个 PVC,是不是已经处于 `Bound`(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个“单独”的 PVC 进行绑定。这样,Kubernetes 就可以保证用户提交的每一个 PVC,只要有合适的 PV 出现,它就能够很快进入绑定状态。
而所谓将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 `spec.volumeName` 字段上。所以,接下来 Kubernetes 只要获取到这个 PVC 对象,就一定能够找到它所绑定的 PV。
## 2.4 小结
关于 PV 的“两阶段处理”流程,是靠独立于 kubelet 主控制循环(Kubelet Sync Loop)之外的两个控制循环来实现的。
- 第一阶段的 Attach(以及 Dettach)操作,是由 `Volume Controller` 负责维护的,这个控制循环的名字叫作:`AttachDetachController`。而它的作用,就是不断地检查每一个 Pod 对应的 PV,和这个 Pod 所在宿主机之间挂载情况。从而决定,是否需要对这个 PV 进行 `Attach(或者 Dettach)`操作。
- 第二阶段的 `Mount(以及 Unmount)`操作,必须发生在 Pod 对应的宿主机上,所以它必须是 kubelet 组件的一部分。这个控制循环的名字,叫作:`VolumeManagerReconciler`,它运行起来之后,是一个独立于 kubelet 主循环的 `Goroutine`。
通过这样将 Volume 的处理同 kubelet 的主循环解耦,Kubernetes 就避免了这些耗时的远程挂载操作拖慢 kubelet 的主控制循环,进而导致 Pod 的创建效率大幅下降的问题。
# 三. StorageClass
StorageClass 对象的作用,其实就是创建 PV 的模板。
## 3.1 PV创建方式
- Static Provisioning
人工管理 PV 的方式就叫作 Static Provisioning,通过手动创建PV方式具有极高的重复性。
- Dynamic Provisioning
Dynamic Provisioning 机制工作的核心,在于一个名叫 `StorageClass` 的 API 对象。
## 3.2 StorageClass范围
StorageClass 对象会定义如下两个部分内容:
1. PV 的属性。比如,存储类型、Volume 的大小等等。
2. 创建这种 PV 需要用到的存储插件。比如,Ceph 等等。
有了这样两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 `StorageClass` 了。然后,Kubernetes 就会调用该 `StorageClass` 声明的存储插件,创建出需要的 PV。
## 3.3 案例
### 3.3.1 StorageClass定义
“demo-storageclass.yaml”文件内容如下:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: demo-storageclass
provisioner: docker.io/hostpath
parameters:
type: pd-ssd
```
在这个 YAML 文件里,我们定义了一个名叫 `demo-storageclass` 的 StorageClass。这个 StorageClass 的 `provisioner` 字段的值是:`docker.io/hostpath`,因为我使用的是mac docker desktop 。而这个 StorageClass 的 `parameters` 字段,就是 PV 的参数。比如:上面例子里的 `type=pd-ssd`。
### 3.3.2 PVC定义
“demo-pvc.yaml”的定义如下:
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: demo-storageclass
resources:
requests:
storage: 1Gi
```
可以看到,我们在这个 PVC 里添加了一个叫作 `storageClassName` 的字段,用于指定该 PVC 所要使用的 `StorageClass` 的名字是:`demo-storageclass`。
### 3.3.3 验证
通过按序创建StorageClass与PVC,指令如下:
```shell
kubectl apply -f demo-storageclass.yaml
kubectl apply -f demo-pvc.yaml
```
我们再通过如下的指令查看`volume`情况:
```shell
kubectl describe pvc demo-pvc
# result
Name: demo-pvc
Namespace: default
StorageClass: demo-storageclass
Status: Bound
Volume: pvc-cd36fd9f-d15a-49f7-836b-9b6605a75f87
Labels: <none>
Annotations: pv.kubernetes.io/bind-completed: yes
pv.kubernetes.io/bound-by-controller: yes
volume.beta.kubernetes.io/storage-provisioner: docker.io/hostpath
Finalizers: [kubernetes.io/pvc-protection]
Capacity: 1Gi
Access Modes: RWO
VolumeMode: Filesystem
Mounted By: <none>
Events:
```
执行如下指令,再查看对应的PV内容:
```shell
kubectl describe pv pvc-cd36fd9f-d15a-49f7-836b-9b6605a75f87
# result
Name: pvc-cd36fd9f-d15a-49f7-836b-9b6605a75f87
Labels: <none>
Annotations: docker.io/hostpath: /var/lib/k8s-pvs/demo-pvc/pvc-cd36fd9f-d15a-49f7-836b-9b6605a75f87
pv.kubernetes.io/provisioned-by: docker.io/hostpath
Finalizers: [kubernetes.io/pv-protection]
StorageClass: demo-storageclass
Status: Bound
Claim: default/demo-pvc
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 1Gi
Node Affinity: <none>
Message:
```
这个自动创建出来的 PV 的 `StorageClass` 字段的值,也是 `demo-storageclass`,Kubernetes 只会将 `StorageClass` 相同的 PVC 和 PV 绑定起来。
实操结果如下图:

## 3.4. 小结
有了 `Dynamic Provisioning` 机制,运维人员只需要在 Kubernetes 集群里创建出数量有限的 `StorageClass` 对象。当开发人员提交了包含 `StorageClass` 字段的 PVC 之后,Kubernetes 就会根据这个 `StorageClass` 创建出对应的 PV。
# 四. 总结
基于Pod,PVC,PV与StorageClass之间的关系,我总结出如下的架构图:

从图中我们可以总结为:
- PVC 描述了 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等。
- PV 描述了一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。
- StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 `StorageClass` 的 PV 和 PVC,才可以绑定在一起。同时,可以指定 PV 的 Provisioner(存储插件),当存储插件支持 `Dynamic Provisioning` 的话,Kubernetes 就可以自动创建 PV 了。
欢迎收藏个人博客: [Wyatt's Blog](https://blog.wyatt.plus) ,非常感谢~
| 个人博客: [Wyatt's Blog](https://blog.wyatt.plus) | 个人公众号:Wyatt的成长之路 |
| :----------------------------------------------------------: | :----------------------------------------------------------: |
| <img src="https://os.wyatt.plus/blog/image_1616376288212.png" width = "200" height = "200" alt="微信号" align=center /> | <img src="https://os.wyatt.plus/blog/43_768_56a21dedcd68e50f951ed74cbe9ff627_26644997eca03ec8600b492ab6935dba_1616376836829.png" width = "200" height = "200" alt="公众号" align=center /> |
# Reference
https://kubernetes.io/docs/concepts/storage/storage-classes/
https://kubernetes.io/zh/docs/concepts/storage/dynamic-provisioning/
https://time.geekbang.org/column/article/42698?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title

Kubernetes:PV与StorageClass剖析