(十九)PV、PVC、StorageClass
前面学习 NFS 的时候,Pod 需要直接写 NFS Server 地址和共享目录
nfs:
server: 192.168.64.2
path: /srv/k8s-share
这种方式可以正常使用,但 Pod 自己知道底层使用的是 NFS,也知道 NFS Server 在哪里。如果以后服务器地址、共享目录或者存储类型发生变化,就需要修改 Pod 的 YAML
因此 Kubernetes 提供了 PV 和 PVC,把应用和具体存储隔开
- PV:描述真正的存储在哪里、容量多大、支持什么访问方式
- PVC:描述应用需要一块什么样的存储
- Pod:只使用 PVC,不需要知道后面是 NFS、云硬盘还是其他存储
整体关系可以理解成:
- Pod -> PVC -> PV -> NFS
PV 和 PVC
PV 全称是 PersistentVolume,可以理解成 Kubernetes 中的一块持久化存储资源,是已经存在的、真实的存储资源,不需要额外申请,原来是直接写进 Pod,现在可以交给 PV 管理。比如前面使用的:
192.168.64.2:/srv/k8s-share
PVC 全称是 PersistentVolumeClaim,可以理解成应用对存储的申请,需要申请,比如:
- 我需要一块至少 2Gi,并且支持 ReadWriteMany,类型是 manual-nfs,的存储
Kubernetes 会根据 PVC 的要求寻找合适的 PV,然后把它们绑定起来
PV
提供 5Gi、RWX、manual-nfs
↑
│ 匹配
↓
PVC
申请 2Gi、RWX、manual-nfs
PV 是集群级资源,不属于某个 Namespace;而 PVC 属于 Namespace,并且 Pod 只能引用和自己位于同一个 Namespace 中的 PVC
使用 NFS 创建 PV 和 PVC
这里继续使用上一章已经搭建好的 NFS
NFS Server:192.168.64.2
共享目录:/srv/k8s-share
本章要将原有的路径从:
- Pod -> NFS 修改改成:
- Pod -> PVC -> PV -> NFS
创建 PV
先创建一个 PV,把 NFS Server 和共享目录交给 PV 管理
cat > nfs-pv.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv # PV 在集群中的 名称
spec:
capacity:
storage: 5Gi # PV 对 Kubernetes 声明自己提供 5Gi 存储
volumeMode: Filesystem # 卷以 文件系统 方式挂载
accessModes:
- ReadWriteMany # 允许多个节点以读写方式使用
persistentVolumeReclaimPolicy: Retain # PVC 删除以后保留这块存储的数据
storageClassName: manual-nfs # 给这块 PV 标记一个存储类别
mountOptions:
- hard # NFS 请求失败时无限重试,直到服务器恢复
- nfsvers=4.1
nfs:
server: 192.168.64.2 # NFS Server 主机 IP
path: /srv/k8s-share # 共享目录
EOF
kubectl apply -f nfs-pv.yaml
kubectl get pv

capacity.storage: 5Gi:表示这个 PV 对 Kubernetes 声明自己提供 5Gi 存储accessModes: ReadWriteMany:允许多个节点以读写方式使用persistentVolumeReclaimPolicy: Retain:PVC 删除以后保留这块存储的数据storageClassName: manual-nfs:给这块 PV 标记一个存储类别nfs.server和nfs.path:真正的存储位置
也就是说,这个 PV 可以理解成:
- 一个叫 nfs-pv 的 PV 声明了一块 5Gi、支持多节点读写(ReadWriteMany)、名为 manual-nfs 的存储资源,它的真实数据挂载在 NFS 服务器的 192.168.64.2:/srv/k8s-share 目录上
这里的 5Gi 主要提供给 Kubernetes 进行存储匹配,普通 NFS 共享目录不会因为这里写了 5Gi ,就真的只能存 5Gi。如果需要严格限制 NFS 目录容量,还需要在后端存储中另外配置配额
创建 PVC
现在 PV 已经存在,但 Pod 还不会直接使用 PV,接下来创建一个 PVC,表示应用需要什么样的存储
cat > nfs-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-data # PVC 在集群中的 名称
spec:
accessModes:
- ReadWriteMany # 允许多个节点以读写方式使用
storageClassName: manual-nfs # 指定要匹配的存储类别,必须与目标 PV 相同
resources:
requests:
storage: 2Gi # 声明需要 2Gi 存储
EOF
kubectl apply -f nfs-pvc.yaml
kubectl get pvc
kubectl get pv

这个 PVC 表示的是:
- 一个叫 web-data 的 PVC 向 Kubernetes 申请一块至少 2Gi、支持多节点读写(ReadWriteMany)、类型为 manual-nfs 的存储资源
所以 Kubernetes 可以把它们绑定:
PVC web-data
↓
PV nfs-pv
查看时一般会看到 PVC 和 PV 都进入 Bound
这里需要注意,PVC 虽然只申请了 2Gi,PV 提供了 5Gi,但绑定以后整个 PV 都会交给这个 PVC,多出来的 3Gi 不会再分给另一个 PVC。也就是说,一个 PV 与 一个 PVC,它们是一对一绑定的
Pod 怎么使用 PVC
现在已经完成 PVC(web_data)-> PV(nfs-pv)-> NFS Server 的连接了,接下来创建一个 Pod 与他们连接
cat > pvc-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: pvc-demo
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: web-data # 引用下面 volumes 中名为 web-data 的卷
mountPath: /usr/share/nginx/html # 挂载到容器内这个路径
volumes:
- name: web-data # 卷名称,提供给 volumeMounts 引用
persistentVolumeClaim:
claimName: web-data # # 引用已存在的 PVC
EOF
kubectl apply -f pvc-pod.yaml
kubectl get pod pvc-demo -o wide
kubectl exec pvc-demo -- sh -c 'echo "hello pvc" > /usr/share/nginx/html/index.html'
kubectl exec pvc-demo -- cat /usr/share/nginx/html/index.html

volumes:
- name: web-data
nfs:
server: 192.168.64.2
path: /srv/k8s-share
现在写成:
volumes:
- name: web-data
persistentVolumeClaim:
claimName: web-data
Pod 只需要知道我要使用 web-data 这个 PVC 即可,后面的关系由 Kubernetes 自行处理:
Pod
↓
PVC:web-data
↓
PV:nfs-pv
↓
192.168.64.2:/srv/k8s-share
所以应用的 Pod 配置中已经没有 NFS Server 地址,也没有 NFS 共享目录。以后底层存储发生变化,只要 PVC 仍然能够获得符合要求的存储,Pod 这一层就不需要直接关心具体存储实现
静态制备和动态制备
前面的实验是我们自己创建:
- PV -> PVC
这种方式叫静态制备,Kubernetes 还支持动态制备,两种方式主要区别就在于 PV 是谁创建的
静态制备
静态制备就是管理员提前创建 PV
管理员创建 PV
↓
用户创建 PVC
↓
Kubernetes 匹配
↓
PV 和 PVC 绑定
我们前面的 NFS 实验就是静态制备,因为使用的命令是:
kubectl apply -f nfs-pv.yaml
我们以手动创建 PV 的方式很适合用来学习 PV 和 PVC,因为可以直接看到实际 存储、PV、PVC 和 Pod 之间的关系
动态制备
如果每一个 PVC 都需要管理员提前创建 PV,实际使用起来会很麻烦,所以 Kubernetes 还可以配合 StorageClass 和 存储驱动 动态创建 PV,整体过程变成:
创建 PVC
↓
StorageClass
↓
存储驱动
↓
创建真实存储
↓
自动创建 PV
↓
与 PVC 绑定
应用只需要创建 PVC,PV 和后面的存储由集群自动准备,这种方式在现代 Kubernetes 集群和云环境中比较常见
StorageClass
StorageClass 是 Kubernetes 中用于定义动态存储供给策略的资源对象,它算是一种规则,用于表示 哪种存储、怎么创建、怎么回收,例如一个集群中可能有:
- fast -> SSD 云硬盘
- normal -> 普通云硬盘
- nfs -> NFS 存储
我们可以看看当前集群的:
kubectl get storageclass
kubectl get sc
类似的输出比如:
root@master:~# get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 509d
local-storage (default) kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 509d
nfs-csi nfs.csi.k8s.io Retain Immediate true 509d
root@master:~# get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 509d
local-storage (default) kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 509d
nfs-csi nfs.csi.k8s.io Retain Immediate true 509d
可以看到,在输出中会存在这几个字段,也就是 StorageClass 的常见配置:
provisioner:指定由哪个存储驱动负责创建 PV,这是最核心的字段parameters:传递给存储驱动的参数,不同驱动支持的参数不同volumeBindingMode:决定 PVC 什么时候与 PV 绑定,其中常见的有两种Immediate:PVC 创建后立即进行 PV 创建或绑定WaitForFirstConsumer:等到真正有 Pod 使用这个 PVC 时,再结合 Pod 的调度位置创建或绑定 PV
reclaimPolicy:决定动态创建的 PV 在 PVC 删除后如何处理Delete:PVC 删除后,对应的动态 PV 和后端存储通常也会被删除Retain:PVC 删除后保留 PV 和底层数据,需要管理员后续处理
allowVolumeExpansion:决定是否允许通过修改 PVC 容量进行扩容
一个 StorageClass 的结构大致如下,当然不同配置结构并不一致:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
annotations:
storageclass.kubernetes.io/is-default-class: "true" # 设置为默认 StorageClass
provisioner: kubernetes.io/no-provisioner # 不动态创建 PV,需要提前准备 Local PV
reclaimPolicy: Delete # PV 的回收策略
volumeBindingMode: WaitForFirstConsumer # 等 Pod 使用 PVC 时再选择并绑定合适的 PV
这里的 provisioner: kubernetes.io/no-provisioner 表示这个 StorageClass 没有动态存储供给程序。也就是说,创建 PVC 时不会自动生成 PV,需要提前准备好符合条件的 Local PV,然后由 Kubernetes 完成匹配和绑定
提前准备 Local PV
↓
PVC 指定 local-storage
↓
Pod 使用 PVC
↓
Kubernetes 根据 Pod 的调度位置选择合适的 Local PV
↓
PVC 与 PV 绑定
而 volumeBindingMode: WaitForFirstConsumer 则表示不会在 PVC 创建后立即完成绑定,而是等到真正有 Pod 使用这个 PVC时,再结合 Pod 可以调度到哪个节点来选择 PV。这种方式尤其适合 Local PV,因为本地存储通常属于某一台具体节点
使用 StorageClass 创建 PVC
现在假设集群中已经存在:
StorageClass:fast
而且对应的存储驱动已经正常运行,这时候应用就不需要自己创建 PV,只需要创建 PVC
cat > database-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: database-data
spec:
storageClassName: fast
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
EOF
kubectl apply -f database-pvc.yaml
kubectl get pvc database-data
kubectl get pv

- 我需要一块 20Gi、ReadWriteOnce、fast 类型 的存储
接下来 Kubernetes 会根据:
storageClassName: fast
找到 fast StorageClass,然后:
PVC database-data
↓
StorageClass fast
↓
对应的 Provisioner
↓
创建真正的存储
↓
自动创建 PV
↓
绑定 database-data
所以这里和前面的 NFS 实验最大的区别就是:前面需要手动创建 PV、再创建 PVC;现在可以直接创建 PVC,然后自动得到 PV。这就是 StorageClass 在动态制备中的作用
默认 StorageClass
如果 PVC 没有写 storageClassName:,那么一个 StorageClass 就会被设置成默认 StorageClass 。通常会使用集群中的默认 StorageClass进行动态制备,比如
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
这里还需要讲一下,不写 storageClassName 和 写了 storageClassName: "" 的含义是不一样的:
- 不写 storageClassName 时,集群存在默认 StorageClass 的情况下,PVC 通常会使用默认 StorageClass
- 写成空字符串时,则表示这个 PVC不使用 StorageClass,通常用于绑定没有 StorageClass 的静态 PV
PV 和 PVC 扩展
这里额外讲一下 PV 和 PVC 中几个经常遇到的配置
访问模式
PV 和 PVC 中都会看到 accessModes 字段,常见有四种:
- ReadWriteOnce(RWO):一个节点可以 以读写方式挂载
- ReadOnlyMany(ROX):多个节点可以 以只读方式挂载
- ReadWriteMany(RWX):多个节点可以 以读写方式挂载
- ReadWriteOncePod(RWOP):只允许集群中的一个 Pod 以读写方式挂载
这里需要特别注意 ReadWriteOnce,它表示 一个节点读写,而不是 只能有一个 Pod 使用。同一个节点上的多个 Pod,在存储实现允许的情况下仍然可能共同使用这个卷
访问模式主要描述存储支持怎样的挂载方式,同时也是 PV 和 PVC 匹配时的重要条件。换句话说 访问模式说的是这个存储能被怎么挂载 —— 是只能挂到一个节点,还是能同时挂到多个节点。PVC 申请存储时,Kubernetes 会拿这个条件去筛 PV,对不上就绑不了
它也不能代替 Linux 文件权限或者应用自己的并发控制。换句话说就是它只是一种访问方式,如果你用 root 用户在 Pod A 写了一个 600 权限的文件、而在 Pod B 用普通用户去读,那 Pod B 就是读不了;以及 Pod A 与 Pod B 都能同时写文件 C 时,那这个文件 C 就会变得混乱不安全,这时候就需要进行并发控制加锁之类的操作...
PV 状态
查看:
kubectl get pv
kubectl get pvc -A
kubectl describe pv nfs-pv
PV 常见状态有:
Available:当前还没有绑定 PVCBound:已经和 PVC 绑定Released:原来的 PVC 已删除,但 PV 还没有完成后续处理Failed:自动回收过程中发生错误
我们前面的过程大概就是:
- 创建 PV -> Available
- 创建 PVC -> Bound
回收策略
PV 中的 persistentVolumeReclaimPolicy: 字段,可以控制 PVC 删除以后,这块 PV 和后端存储怎么处理
常见的是 Retain 和 Delete
Retain
前面的 NFS PV 使用:
persistentVolumeReclaimPolicy: Retain
删除 PVC 后,PV 背后的数据会保留下来,PV 通常会进入:
Released
这时管理员需要自己检查、备份、清理数据,再决定后续怎么处理
对于手工管理的 NFS 实验或者重要数据,使用 Retain 可以降低误删除数据的风险
Delete
如果使用:
persistentVolumeReclaimPolicy: Delete
那 PVC 删除以后,Kubernetes 会尝试删除 PV,并让存储驱动删除真正的后端存储
动态创建的云硬盘、CSI Volume 经常使用这种策略,所以删除 PVC 之前需要先确认回收策略,否则真实数据可能跟着一起删除
以前 Kubernetes 还有一个:
Recycle
会简单清空卷中的文件然后重新使用,但已经废了,就不过多描述了
PVC 扩容
如果 StorageClass 配置了:
allowVolumeExpansion: true
并且对应存储驱动支持扩容,就可以增加 PVC 请求的容量,比如从原来的 20Gi 扩容到 30Gi,可以通过一下命令进行扩容
kubectl patch pvc database-data -p '{"spec":{"resources":{"requests":{"storage":"30Gi"}}}}'
虽然 PVC 支持增加容量,但不能通过把 30Gi 再改成 20Gi 的方式直接缩容。以及扩容以后文件系统是否需要额外处理,取决于具体卷类型和存储驱动
总结
这篇主要学习的是 Kubernetes 怎么把 Pod 和 具体存储 进行隔离
前面直接使用 NFS 时 Pod 自己知道 NFS Server 地址和目录
- Pod -> NFS
在加入 PV 和 PVC 后 Pod 只需要知道自己要使用哪个 PVC,具体存储在哪里由 PV 管理
- Pod -> PVC -> PV -> NFS
静态制备时,PV 由管理员提前创建;动态制备时,StorageClass 和存储驱动会根据 PVC 自动创建真实存储和 PV
我认为,能了解这些就足够了。
愿诸君顺遂