Kubernetes in Action 笔记 —— 通过 PersistentVolume 持久化数据

Pods 与底层存储技术的解耦

理想情况下,将应用部署到 Kubernetes 上的开发者不需要知道集群提供的存储技术,就像他们不需要知道运行 Pods 的物理服务器的属性。基础设施的细节应该由集群的维护者去掌控。

比如在 Pod 中挂载一个 NFS 共享作为持久存储,Pod 的清单文件中就需要包含 NFS 服务器的 IP 地址和共享文件的路径,从而导致该 Pod 的定义与特定的集群绑定在一起,阻止其用在其他地方。
A pod manifest with infrastructure-specific volume information is not portable to other clusters

Persistent volumes and claims

为了令 Pod 清单文件面向不同的集群是可移植的,针对存储卷的环境相关的信息被移动到 PersistentVolume 对象中,再通过一个 PersistentVolumeClaim 对象将 Pod 与 PersistentVolume 连接在一起。
Using persistent volumes and persistent volume claims to attach network storage to pods

顾名思义,PersistentVolume 对象代表一种存储卷,用来持久化应用数据。该对象包含了底层存储的信息,从而将这些信息从 Pod 中解耦。即 Pod 的清单文件中与存储相关的部分,不必再包含基础设施相关的信息(转移到了 PersistentVolume 中),使得同样的清单文件能够部署在不同的集群上。

Pod 并不会直接引用 PersistentVolume 对象,而是指向一个 PersistentVolumeClaim 对象。PersistentVolumeClaim 代表用户对 PV 的请求或者声明,有着独立于 Pod 的生命周期,从而允许 PV 的所属权(ownership)与 Pod 解耦。
用户在使用 PV 前必须先声明一个 PVC 对象。Pod 可以在任意时间删除,用户并不会因此失去对 PV 的所属权。当 PV 不再被需要时,用户可以通过删除 PVC 来释放它。

Pod 清单文件中的存储定义部分只需要包含 PVC 的名称,不需要任何基础设施相关的信息,比如 NFS 服务器的 IP 地址。PVC 会负责将其绑定的代表 NFS 存储的 PV 挂载到 Pod 中。
Mounting a persistent volume into the pod’s container(s)

Using the same persistent volume claim in multiple pods

使用 PV 和 PVC 的优势

为了让 Pod 使用某个存储卷,借助 PV 和 PVC 这两种额外的对象,肯定比直接在 Pod 清单文件中定义要复杂得多。
使用 PV 和 PVC 的最大优势在于,基础设施相关的细节从 Pod 代表的应用中解脱了出来。集群管理员比任何人都更了解数据中心本身,他们负责创建 PV 对象;软件开发者则可以集中精力通过 Pod 和 PVC 来描述应用本身的需求。
Persistent volumes are provisioned by cluster admins and consumed by pods through persistent volume claims

应用开发人员不需要了解底层基础设施的任何细节,就可以直接创建 Pod 清单文件和 PVC 对象;同样的,集群管理员也可以在不了解应用的所有细节的前提下,创建一系列不同大小的存储卷。
更进一步的,借助 PV 的动态创建功能,管理员根本不需要提前创建好存储卷。如果集群中安装了 automated volume provisioner,物理存储卷和 PV 对象会在用户创建 PVC 之后按需自动生成。

创建 PV 和 PVC

创建 PV 对象

测试环境使用的是 Minikube,因此这里使用工作节点的本地路径来创建 PV,而不使用网络存储。其清单文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolume
metadata:
name: quiz-data
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
hostPath:
path: /var/quiz-data

其中的 capacity 选项用来指定底层存储卷的大小。每个 PV 都必须指定其容量,以便于在 PVC 和 PV 绑定时,Kubernetes 可以判断具体哪个 PV 符合要求。
每个 PV 都必须指定其支持的 accessModes 列表。依赖于底层存储的具体实现,PV 可能支持也可能不支持同时被多个工作节点以 r/w 或 r/o 模式挂载。
注意 accessModes 影响的是 Nodes 而不是 Pods。一个 PV 只要能够被某个节点挂载,同时也就支持被该节点上的多个 Pods 挂载。
accessModesReadWriteOnceReadOnlyManyReadWriteMany 三种模式。

创建和查看 PV

创建 PV:

1
2
$ kubectl apply -f pv.quiz-data.hostpath.yaml
persistentvolume/quiz-data created

查看 PV:

1
2
3
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
quiz-data 1Gi RWO,ROX Retain Available 2m55s

声明一个 PV

创建一个 PVC 对象
需要创建 PVC 对象来声明一个 PV,PVC 对象中会指定 PV 必须符合的要求,包括最小容量、访问模式等,通常是由不同应用的具体需求来决定的。因而 PVC 对象应该由应用的作者而不是集群管理员来创建。

PVC 对象的清单文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: quiz-data
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce
storageClassName: ""
volumeName: quiz-data

上面的 PVC 对象描述了一系列需要 PV 满足的要求。比如至少 1G 大小、能够在单节点上以读写模式挂载。
storageClassName 字段用来配置 PV 的动态生成,如果需要绑定一个已经预先创建好的 PV,则该字段必须为空。
因为前面创建的 PV 名字为 quiz-data,所以 PVC 中的 volumeName 字段也必须为 quiz-data;若不指定此字段,则 Kubernetes 有可能会绑定其他满足要求的 PV。
如果集群管理员创建了一系列没有指定名称的 PV,用户也并不在意具体会绑定哪个 PV,则可以跳过 volumeName 字段,让 Kubernetes 随机选择满足要求的 PV。

创建和查看 PVC

创建 PVC:

1
2
$ kubectl apply -f pvc.quiz-data.static.yaml
persistentvolumeclaim/quiz-data created

查看 PVC:

1
2
3
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
quiz-data Bound quiz-data 1Gi RWO,ROX 31s

再次查看之前创建的 PV 的状态,可以看到此时 quiz-data PV 的 STATUS 变成了 BOUND

1
2
3
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
quiz-data 1Gi RWO,ROX Retain Bound default/quiz-data 92m

在 Pod 中使用 PVC 和 PV

参考如下清单文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: quiz
spec:
volumes:
- name: quiz-data
persistentVolumeClaim:
claimName: quiz-data
containers:
- name: quiz-api
image: luksa/quiz-api:0.1
ports:
- name: http
containerPort: 8080
- name: mongo
image: mongo
volumeMounts:
- name: quiz-data
mountPath: /data/db

使用 kubectl apply -f pod.quiz.pvc.yaml 命令创建 Pod,待创建完成后,可以执行如下 Shell 命令向 mongo 容器中插入数据:

1
2
3
4
5
6
7
8
kubectl exec -it quiz -c mongo -- mongo kiada <<EOF
db.questions.insert({
id: 1,
text: "What does k8s mean?",
answers: ["Kates", "Kubernetes", "Kooba Dooba Doo!"],
correctAnswerIndex: 1
})
EOF

运行如下命令查看插入的数据:

1
2
$ kubectl exec -it quiz -c mongo -- mongo kiada --quiet --eval "db.questions.find()"
{ "_id" : ObjectId("625fe24a095faed6c085f539"), "id" : 1, "text" : "What does k8s mean?", "answers" : [ "Kates", "Kubernetes", "Kooba Dooba Doo!" ], "correctAnswerIndex" : 1 }

在 Pod 中重复使用 PVC

当删除某个 Pod 中的 PVC 时,对应的底层存储卷会从工作节点解除挂载。但 PV 对象仍旧是跟 PVC 相关联的。若之后创建另一个 Pod 指向同样的 PVC,则新的 Pod 可以访问 PV 对应的存储卷和文件。

1
2
3
4
5
6
$ kubectl delete -f pod.quiz.pvc.yaml
pod "quiz" deleted
$ kubectl apply -f pod.quiz.pvc.yaml
pod/quiz created
$ kubectl exec -it quiz -c mongo -- mongo kiada --quiet --eval "db.questions.find()"
{ "_id" : ObjectId("625fe24a095faed6c085f539"), "id" : 1, "text" : "What does k8s mean?", "answers" : [ "Kates", "Kubernetes", "Kooba Dooba Doo!" ], "correctAnswerIndex" : 1 }
释放 PV

删除 PVC 会释放对应的 PV。

1
2
3
4
$ kubectl delete pod quiz
pod "quiz" deleted
$ kubectl delete pvc quiz-data
persistentvolumeclaim "quiz-data" deleted

查看此时 PV 的状态:

1
2
3
$ kubectl get pv quiz-data
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
quiz-data 1Gi RWO,ROX Retain Released default/quiz-data 45h

STATUS 项变成了 Released 状态。

若此时重新创建删除的 PVC,对应的 PV 不会再次被绑定。
原因在于,PV 已经被使用过,有可能包含一些旧的数据,需要在绑定给另一个 PVC 之前进行清理。这也是为什么 PV 已经 Released 之后,仍然显示关联的 CLAIMdefault/quiz-data,为了方便集群管理员确认这些数据能否被安全的删除。

令释放的 PV 重新可用

重新创建删除的 PVC 不会自动绑定之前的 PV,该 PV 会处于 Released 状态。为了让 PV 对应的数据再次可用,需要删除并重新创建 PV。
PV 对象只是一个指向底层存储的指针,它本身并不存储任何应用数据。删除并重新创建 PV 相当于创建了一个新的指向同一个底层存储卷的指针。数据和之前是相同的。

1
2
3
4
5
6
7
8
9
10
$ kubectl delete pv quiz-data
persistentvolume "quiz-data" deleted
$ kubectl apply -f pv.quiz-data.hostpath.yaml
persistentvolume/quiz-data created
$ kubectl apply -f pvc.quiz-data.static.yaml
persistentvolumeclaim/quiz-data created
$ kubectl apply -f pod.quiz.pvc.yaml
pod/quiz created
$ kubectl exec -it quiz -c mongo -- mongo kiada --quiet --eval "db.questions.find()"
{ "_id" : ObjectId("625fe24a095faed6c085f539"), "id" : 1, "text" : "What does k8s mean?", "answers" : [ "Kates", "Kubernetes", "Kooba Dooba Doo!" ], "correctAnswerIndex" : 1 }
PV 的 reclaim policy

PV 被释放后的动作取决于其 reclaim policy。此 policy 由 PV 对象的 .spec.persistentVolumeReclaimPolicy 条目进行配置。前面 quiz-data 的 reclaim policy 是 Retain

1
2
3
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
quiz-data 1Gi RWO,ROX Retain Bound default/quiz-data 5m57s

两种不同的 reclaim policy:

  • Retain:当 PV 被释放后(即对应的 PVC 被删除后),Kubernetes 会保留该 PV。集群管理员则必须手动回收 Volume。这是手动创建 PV 的默认配置
  • Delete:PV 对象和对应的底层存储会在 PV 释放后自动删除。这是动态创建 PV 的默认配置
删除绑定中的 PV

如果集群管理员删除了某个正在使用中的 PV(已经绑定给了某个 PVC):

1
2
3
$ kubectl delete pv quiz-data
persistentvolume "quiz-data" deleted
^C

上述命令会告诉 Kubernetes API 删除 PV 对象,并等待 Kubernetes 控制器完成该操作。
事实上该操作并不会完成,直到待删除的 PV 最终被释放(绑定的 PVC 被删除)。
可以按 Ctrl - C 取消等待,但删除动作并不会被取消:

1
2
3
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
quiz-data 1Gi RWO,ROX Retain Terminating default/quiz-data 19m

该 PV 会一直处于 Terminating 状态,直到对应的 PVC 被删除。

删除使用中的 PVC

和删除 PV 类似,删除使用中的 PVC 的动作并不会立即完成。可以强制中断删除命令的执行,但并不会取消该删除流程。

1
2
3
$ kubectl delete pvc quiz-data
persistentvolumeclaim "quiz-data" deleted
^C

删除动作会被 Pod 阻塞。毫无疑问,删除一个正在使用中的 PVC 并不会立即对 Pod 中运行的应用产生任何影响。Kubernetes 并不会因为集群管理员需要回收一些存储空间而杀掉某个 Pod。
只有删掉引用了该 PVC 的 Pod,删除该 PVC 的进度才会完成。

理解手动创建的 PV 的生命周期

The lifecycle of statically provisioned persistent volumes, claims and the pods that use them

在使用手动创建的 PV 时,底层存储卷的生命周期与 PV 对象的生命周期是分离的。
PV 创建后的初始状态是 Available,当 PVC 出现且其要求能被某个 PV 满足时,PV 与 PVC 完成绑定。在此之前 PVC 处于 Pending 状态,绑定完成后 PV 和 PVC 都处于 Bound 状态。
在这之后,一个或多个 Pod 可以通过引用 PVC 来使用对应的存储。当所有的 Pods 运行结束后,PVC 对象可以被删除。PVC 对象删除后,PV 的回收策略决定了对 PV 和底层存储的后续操作。若策略为 Delete,则 PV 和 底层存储都会被删除;若策略为 Retain,PV 对象和底层存储都会被保留,PV 的状态变为 Released,无法再次被绑定。底层存储以及其中的文件会继续存在,可以通过创建一个新的指向同样位置的 PV 来再次访问这些文件。

PV 的动态创建

前面的章节中,集群管理员必须提前创建 PV 对象,每次 PV 释放后还需要管理员手动删除存储卷中的数据。
为了保证集群平稳地运行,管理员需要提前创建数十甚至上百个 PV,还要持续地跟踪可用的 PV 数量,确保其没有耗尽。所有这些手动操作并没有遵循 Kubernetes 自动管理的哲学。

更好的方式是动态创建 PV。集群管理员部署一个 PV provisioner,该 provisioner 可以自动化执行实时的 PV 创建流程。
Dynamic provisioning of persistent volumes

与静态创建 PV 相反,在动态创建过程中,用户先创建 PVC,然后 provisioner 再从底层存储创建对应的 PV 对象。

StorageClass 对象

列出 storage classes

1
2
3
$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
standard (default) k8s.io/minikube-hostpath Delete Immediate false 158d

进一步检查 storage class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ kubectl get sc standard -o yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"},"labels":{"addonmanager.kubernetes.io/mode":"EnsureExists"},"name":"standard"},"provisioner":"k8s.io/minikube-hostpath"}
storageclass.kubernetes.io/is-default-class: "true"
creationTimestamp: "2021-11-15T05:36:28Z"
labels:
addonmanager.kubernetes.io/mode: EnsureExists
name: standard
resourceVersion: "298"
uid: 3d1fb350-928d-4d53-a2eb-558358814839
provisioner: k8s.io/minikube-hostpath
reclaimPolicy: Delete
volumeBindingMode: Immediate

StorageClass 对象代表某种可以被动态创建的存储类型。每一个 StorageClass 都会指定在动态创建 Volume 时需要使用的 provisioner 以及需要传递的参数。由用户来决定每一个 PVC 具体使用那种 StorageClass。

The relationship between storage classes, persistent volume claims and dynamic volume provisioners

用 default storage class 动态创建 PV

可以创建一个 PVC 对象并将其 storageClassName 条目设置为 standard,或者不指定任何 storageClassName,Kubernetes 会自动选择默认的 storage class。

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: quiz-data-default
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce

创建和查看 PVC:

1
2
3
4
5
kubectl apply -f pvc.quiz-data-default.yaml
persistentvolumeclaim/quiz-data-default created
$ kubectl get pvc quiz-data-default
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
quiz-data-default Bound pvc-eac89776-b02b-49c1-9878-b50b9780bd3c 1Gi RWO standard 47s

对应的 PV 会在 PVC 创建后自动生成和绑定。

1
2
3
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-eac89776-b02b-49c1-9878-b50b9780bd3c 1Gi RWO Delete Bound default/quiz-data-default standard 113s

动态生成 PV 的生命周期

The lifecycle of dynamically provisioned persistent volumes, claims and the pods using them

参考资料

Kubernetes in Action, Second Edition