12 存储:PV的生命周期与动态供应
你好,我是雪飞。
上节课最后,我带你动手部署了 PV 和 PVC,PV 代表了 K8s 集群中的存储资源,而 PVC 是 Pod 对 PV 存储资源的需求声明。PVC 找到合适的 PV 绑定,而 Pod 通过挂载 PVC 从而使用 PV 定义的存储资源。现在,你可以思考一下,如果我们要删除这个 Pod 和 PVC,那么我们在PV 存储资源中创建的共享文件会被删除吗?
要回答这个问题,就需要了解 PV 的生命周期。
PV 的生命周期
查看 PV 信息时,返回结果中有个 STATUS 字段,它表示 PV 的状态。在 PV 的生命周期中,可能会有以下四种状态:
- Available(可用): 表示 PV 处于可用状态,还未被任何 PVC 绑定。
- Bound(已绑定): 表示 PV 已经被 PVC 绑定。
- Released(已释放): 表示 PV 被解绑,但是 PV 及存储空间的数据还保留。
- Failed(失败): 表示该 PV 的自动回收失败。
下面我们结合 PVC 和 PV 的使用流程,介绍一下 PV 的生命周期及状态变化:
-
Provisioning:供应 PV,也就是创建 PV 的过程,有静态和动态两种创建 PV 的方式。
-
Static:静态供应 PV,通常由管理员或运维人员按照业务需求手动对集群的存储资源进行划分,提前创建并部署好多个 PV,这时 PV 的状态是 Available(可用), 等待 PVC 进行绑定和使用。
- Dynamic:动态供应PV,这种方式不需要提前创建 PV,而是 K8s 通过第三方插件(Provisioner)来为 PVC 动态创建 PV 存储资源。
- Binding:绑定 PV,创建 PVC 之后,K8s 会根据这个 PVC 的存储资源需求在集群中寻找满足条件的 PV。如果找到合适的 PV,就会将该 PV 资源绑定到 PVC 上,这时 PV 的状态是 Bound(已绑定)。在找到可用的 PV 之前,PVC 会保持未绑定状态。如果是动态供应,则 PV 的创建和绑定是同时完成的。
- Using:使用 PV,应用 Pod 通过在容器中挂载 PVC,从而使用 PVC 绑定的 PV 定义的存储资源。应用就可以在挂载目录中进行目录和文件的操作,从而实现文件的共享和持久化。
-
Releasing 和Recycling:释放和回收 PV,删除 PVC 可以释放或者删除绑定的 PV。这时候会根据创建 PV 的 YAML 文件中的 “ persistentVolumeReclaimPolicy” 回收策略,来决定 PV 的操作和状态。
-
Retain(保留):默认策略,此时 PV 状态将变成 Released(已释放),同时还保留着之前的数据,管理员需要手动处理 PV 以及数据,这样可以减少数据丢失风险。 Released 状态的 PV 不能被其他 PVC 绑定。
- Delete(删除): 这个策略会在删除 PVC 时自动删除 PV 资源,同时删除存储资源中的数据。通常用于动态供应 PV 的方式,需要存储资源提供的插件的支持,否则 PV 状态为 Failed(失败)。
所以现在我们可以回答开头提出的问题,删除 Pod 对 PV 没有任何影响,但是删除 PVC 时,会根据 PV 的回收策略来决定是否保留 PV 和存储资源中的数据。如果要自动删除 PV 和数据,需要存储资源的插件支持。
动态供应
之前讲的例子是 PV 的静态供应方式,管理员或运维人员需提前创建 PV,这种方式的缺点是维护成本高,需要提前规划和部署 PV。因此,K8s 集群提供了动态供应 PV 的方式,该方式可以根据 Pod 的 PVC 存储资源需求自动创建和绑定 PV,提供了更好的灵活性和扩展性。
动态供应需要先了解一个概念:StorageClass,它定义了某一种类型的存储(如 NFS、Ceph 等),包含该存储类型的配置参数(parameters)、数据回收策略(reclaimPolicy)和供应器插件(provisioner)等信息。集群管理员可以根据需要定义多个 StorageClass,以满足分配不同类型的存储资源。动态供应需要在 PVC 中指定要绑定 PV 的StorageClass,K8s 会通过 StorageClass 中指定的存储资源的供应器插件来自动创建 PV,并将其与 PVC 绑定。
下面我带你动手完成一个实验:动态供应 NFS 存储类型的 PV。首先,你需要安装 NFS Provisioner。
安装 NFS Provisioner
K8s 内置了一些供应器插件,但是对于 NFS 存储类型,需要单独安装供应器插件。这里我选择使用 NFS 子目录外部供应器(NFS Subdir External Provisioner)。它通过使用你现有的、已经配置好的 NFS 服务器来支持通过 PVC 动态供应 PV(自动创建的 PV 名称为 “${namespace}-${pvcName}-${pvName}” )。
需要从项目 GitHub( 项目链接)中的 deploy 文件夹下载 3 个 YAML 文件。然后依次部署这 3 个 YAML 文件。
# 获取rbac.yaml文件
wget https://raw.githubusercontent.com/kubernetes-sigs/nfs-subdir-external-provisioner/master/deploy/rbac.yaml
# 获取class.yaml文件
wget https://raw.githubusercontent.com/kubernetes-sigs/nfs-subdir-external-provisioner/master/deploy/class.yaml
# 获取deployment.yaml文件
wget https://raw.githubusercontent.com/kubernetes-sigs/nfs-subdir-external-provisioner/master/deploy/deployment.yaml
在部署 “deployment.yaml” 文件之前,你需要使用编辑器修改文件,将镜像地址更改为国内可下载的镜像源 “chainguard/nfs-subdir-external-provisioner:latest-dev”,并将 NFS 服务器地址和共享目录修改为已部署的 NFS 服务端信息。如果镜像下载太慢或者无法下载,可以使用我提供的离线镜像包(nfs.tar, 下载地址),下载到所有节点,并在每个节点上使用 “docker load” 命令将离线包导入到节点本地,最后再执行 YAML 文件部署。
[root@k8s-master ~]# vi deployment.yaml
......
containers:
- name: nfs-client-provisioner
# image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
image: chainguard/nfs-subdir-external-provisioner:latest-dev
......
- name: NFS_SERVER
value: 192.168.1.13 # NFS 服务端 k8s-worker2 IP 地址
- name: NFS_PATH
value: /nfs/k8s/shared # NFS 服务端的共享目录
volumes:
- name: nfs-client-root
nfs:
server: 192.168.1.13 # NFS 服务端 k8s-worker2 IP 地址
path: /nfs/k8s/shared # NFS 服务端的共享目录
[root@k8s-master ~]# kubectl apply -f rbac.yaml # 授权访问apiser
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
[root@k8s-master ~]# kubectl apply -f deployment.yaml # 部署供应器
deployment.apps/nfs-client-provisioner created
[root@k8s-master ~]# kubectl apply -f class.yaml # 创建存储类
storageclass.storage.k8s.io/nfs-client created
部署完成后,检查插件和存储类是否已成功部署。结果显示,NFS 子目录外部供应器插件已成功安装,且 NFS 存储类也已创建。至此,准备工作完成,可以使用动态供应 PV 功能。
[root@k8s-master ~]# kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
nfs-client-provisioner 1/1 1 0 7m42s
[root@k8s-master ~]# kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-client k8s-sigs.io/nfs-subdir-external-provisioner Delete Immediate false 8m10s
使用动态供应
动态供应的过程如下:
- 先声明一个 PVC,指定了所需的存储容量、访问模式和需要使用的存储类 StorageClass。
- 然后 K8s 查找 PVC 中指定的 StorageClass(如果不存该存储类,则无法分配 PV,从而 PVC 也不可用)。
- 如果找到匹配的 StorageClass,会通过 StorageClass 配置的 Provisioner 来自动创建 PV 存储资源(存储类中会定义回收策略、存储类型、Provisioner 供应器插件)。
- 新创建的 PV 存储资源自动与 PVC 资源绑定,PVC 状态为 “Bound”,说明 PVC 已经可以用了。
- 在 Pod 资源中直接挂载 PVC,从而使用 PVC 绑定的动态分配的 PV 存储资源。
可以看出,动态供应的关键就是通过 StorageClass 中的 Provisioner 来动态创建 PV 资源。
现在我们就来动手创建一个 PVC ,它的 YAML 文件(my-pvc-dynamic.yaml)如下,增加 “storageClassName” 字段来定义要使用的存储类,这里我们使用上面创建好的 “nfs-client”。
# my-pvc-dynamic.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc-dynamic-100m # 自定义PVC名字
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteMany # 访问模式
resources:
requests:
storage: 100M # 定义要申请的空间大小
然后我们再定义一个 Deployment 来挂载使用 PVC。它的 YAML 文件(my-pvc-dynamic-deployment.yaml)如下。
# my-pvc-dynamic-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-pvc-deployment
spec:
replicas: 3
selector:
matchLabels:
app: pvc-busybox-pod
template:
metadata:
labels:
app: pvc-busybox-pod
spec:
containers:
- name: busybox-c
image: busybox
command: ["/bin/sh","-c","sleep 3600"]
volumeMounts:
- name: pvc-shared-data
mountPath: /tmp/data
volumes:
- name: pvc-shared-data
persistentVolumeClaim:
claimName: my-pvc-dynamic-100m # 部署好的动态供应的PVC名称
部署 PVC 和 Deployment 后,查看 PVC 的状态,发现 PVC 已成功创建并动态绑定了一个 PV,其标识为 “pvc-66eff73b-9a5d-42f0-905d-c30f43143247”。
[root@k8s-master ~]# kubectl apply -f my-pvc-dynamic.yaml
persistentvolumeclaim/my-pvc-dynamic-100m created
[root@k8s-master ~]# kubectl apply -f my-pvc-dynamic-deployment.yaml
deployment.apps/my-pvc-deployment created
[root@k8s-master ~]# kubectl get pvc # 查看PVC资源
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc-dynamic-100m Bound pvc-66eff73b-9a5d-42f0-905d-c30f43143247 100M RWX nfs-client 51s
kubectl get pv # 查看PV资源
注意:如果 PVC 没有绑定成功,可以使用 “kubectl describe pvc
” 命令来查看失败原因。如果提示 permission denied,则表示共享目录操作权限不够,可以在 NFS 服务器节点上通过 “chmod -R 777 <共享目录>” 命令扩大目录操作权限。
进入到 Deployment 的其中一个 Pod 中,创建一个文件 “aaa.txt”。
[root@k8s-master ~]# kubectl exec -it my-pvc-deployment-c775ccd6f-h9nl8 -c busybox-c -- sh
/ # cd /tmp/data/
/tmp/data # echo "Hello dynamic PVC!" > aaa.txt
然后去 k8s-worker2 节点的 NFS 共享目录 “/nfs/k8s/shared” 中查看,发现已经自动动态创建了一个文件夹,名称是 “default-my-pvc-dynamic-100m-pvc-66eff73b-9a5d-42f0-905d-c30f43143247”,同时里面可以看到 Pod 中创建的文件 “aaa.txt”。这也说明了创建成功。
[root@k8s-worker2 ~]# cd /nfs/k8s/shared/
[root@k8s-worker2 shared]# ls
default-my-pvc-dynamic-100m-pvc-66eff73b-9a5d-42f0-905d-c30f43143247
[root@k8s-worker2 shared]# cd default-my-pvc-dynamic-100m-pvc-66eff73b-9a5d-42f0-905d-c30f43143247
[root@k8s-worker2 default-my-pvc-dynamic-100m-pvc-66eff73b-9a5d-42f0-905d-c30f43143247]# cat aaa.txt
Hello dynamic PVC!
删除动态供应
删除 Deployment 和 PVC,再看一下结果,此时 PV 也已经删除。
[root@k8s-master ~]# kubectl delete -f my-pvc-dynamic-deployment.yaml
deployment.apps "my-pvc-deployment" deleted
[root@k8s-master ~]# kubectl delete -f my-pvc-dynamic.yaml
persistentvolumeclaim "my-pvc-dynamic-100m" deleted
[root@k8s-master ~]# kubectl get pv # 查看PV资源
No resources found
我们再来看一下共享目录中的文件夹和文件,可以发现动态供应 PV 对应的文件夹和里面的文件也都被自动删除了。这个也就是动态供应中默认的 Delete 回收策略的作用。
通过动手实验,我们看出动态供应 PV 可以更加智能和高效地管理存储资源,提高了存储资源的灵活性和可扩展性。实际项目中,这种方式也最为常用。
StatefulSet 资源
学会了使用动态供应 PV,K8s 中的存储就讲完了,最后,我们再来看一个用于部署有状态应用的资源对象——StatefulSet。由于 StatefulSet 的部署过程需要网络和存储的知识,所以我放到中级篇的最后介绍一下。
我先帮你回顾一下有状态应用,有状态应用需要在服务器端维护应用的状态信息,这可能包括用户会话、数据库连接、文件系统状态等。此外,有状态应用的多个应用之间往往存在依赖关系,例如主从或主备关系等。因此,有状态应用的部署和管理通常比无状态应用更加复杂,例如数据库 MySQL、缓存Redis、消息队列等。
通过 StatefulSet 部署有状态应用解决了三个关键问题。
- 应用顺序问题: 在部署多个 Pod 时,即使它们都基于同一个应用镜像,它们之间存在特定的依赖关系,例如数据库的主从关系(master/slave),StatefulSet 通常也是按照预设的顺序部署这些 Pod 来确保这些依赖关系得以正确建立,从而保证整个应用的正常运行。同样,StatefulSet 中的 Pod 在扩容、删除、替换时,也会严格遵循这种顺序。
- 网络标识问题: 由于每个 Pod 有不同的角色和功能,那么通过 Service 进行访问时,就无法使用负载均衡,而是必须确保请求被正确地转发到特定的 Pod。StatefulSet 解决了这个问题,它为每个 Pod 自动分配固定的网络标识(DNS 名称),即使 Pod 重启,其 DNS 名称也不会改变,从而确保了网络通信的一致性和可靠性。
- 持久化存储问题: 由于每个 Pod 角色不同,要存储的信息可能也不同,这时就不能使用共享存储了。为了解决这个问题,StatefulSet 允许为每个 Pod 定义独立的 PVC 和 PV。通过使用 PVC 模板,部署时可以为每个 Pod 创建并挂载对应的 PVC 和 PV。这样,每个 Pod 都拥有自己独立的存储空间,即使 Pod 在集群中被重新调度,其存储卷中的数据也不会丢失,从而保障了应用数据的持久性和一致性。
了解了这三个问题,你可能在想,是否可以通过按照一定顺序执行不同的 Pod 的 YAML 文件,来部署有状态应用不同角色的应用,这确实也是可行的,但考虑到这些 Pod 的镜像相同且配置可以复用,使用 StatefulSet 资源只需编写一个 YAML 文件,相比管理多个 Pod 的 YAML 文件,使用 StatefulSet 更为简单方便。
StatefulSet 在 CKA 考试中没有出现,在实际开发项目中也很少使用。这里我仅给出了一个运行 Redis 镜像的 StatefulSet 的 YAML 文件。你可以自己部署一下看看结果。
# my-sts-redis.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: my-sts-redis
spec:
replicas: 2 # 两个Pod副本
selector:
matchLabels:
app: my-sts-redis
template: # Pod模板
metadata:
labels:
app: my-sts-redis
spec:
containers:
- image: redis # Redis 镜像
name: redis
ports:
- containerPort: 6379
volumeMounts:
- name: my-sts-redis-pvc-100m # PVC 模板中的名称
mountPath: /tmp/data
serviceName: my-sts-redis-svc # 提前指定了 Service 名称
volumeClaimTemplates: # 定义 PVC 模板
- metadata:
name: my-sts-redis-pvc-100m
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
---
# Service 资源对象
apiVersion: v1
kind: Service
metadata:
name: my-sts-redis-svc
spec:
clusterIP: None
selector:
app: my-sts-redis
ports:
- port: 6379
targetPort: 6379
protocol: TCP
- serviceName: 由于 StatefulSet 不再通过 Service 负载均衡的方式访问 Pod,因此Service 主要是通过 Pod 的域名将请求直接转发到 Pod 上,所以在定义 StatefulSet 时需指定转发请求的 Service 名称。
- volumeClaimTemplates: PVC 资源模板,类似于 PVC。每个 Pod 都会使用这个模板创建一个 PVC,分别挂载自己的 PV 存储资源。模板生成的 PVC 的命名方式是:PVC 名字 + StatefulSet 名字 + 序号。
- clusterIP:在 Service 中需要定义 clusterIP 为 None,这样 K8s 集群不会为 Service 分配 ClusterIP,Service 也不会自动负载均衡访问到某个 Pod,而是需要通过 Pod 的 DNS 域名“
.<服务名>.<名字空间名>.svc.cluster.local”(简写 .<服务名>)来访问具体的 Pod。
部署这个文件,可以看到 StatefulSet 创建了两个有顺序的 Pod,可以分别通过 Pod 的 DNS 域名访问,并且通过动态供应,StatefulSet 自动为这两个 Pod 生成了自己的独立存储空间。
小结
今天,我给你介绍了 K8s 中 PV 的生命周期,包括 Provisioning(供应)、Binding(绑定)、Using(使用)、Releasing(释放)和 Recycling(回收)阶段。其中重点讲了两种 PV 的回收策略,Retain 是默认策略,删除 PVC 时,绑定的 PV 状态变为 Released,同时还保留着之前的数据;Delete 会同时清除 PV 和 PV 对应的存储资源中的数据,需要存储资源的插件支持,否则 PV 状态为 Failed。
接着,介绍了动态供应 PV 的使用方法,不同的存储资源类型,需要使用不同的供应器插件。对于 NFS 网络文件系统,我们可以使用 “NFS Subdir External Provisioner”。它通过使用你现有的、已经配置好的 NFS 服务器来支持通过 PVC 动态供应 PV。我带你安装了 NFS 的 Provisioner,然后创建 StorageClass 存储类,之后就可以根据 PVC 存储资源的要求来自动创建和绑定 PV ,从而在 Pod 中使用持久化存储空间。
最后,我给你展示了有状态应用部署的 StatefulSet 资源对象,通过 StatefulSet 可以固定 Pod名称和顺序,同时为每个 Pod 分配固定网络访问域名,以及给每个 Pod 提供独立存储空间,从而有效地解决部署有状态应用的问题。
思考题
这就是今天的全部内容,在 CKA 中也会考到相关的知识点,我给你留一道思考题。
PV 里指定了存储空间的 capacity 容量,如果我们在使用 PV 的时候,超过了 PV 指定的 capacity 容量,这个时候还能继续存放文件吗?
你可以动手实验一下,会让你对知识的理解更加深刻。