大白话说明白K8S的PV / PVC / StorageClass(理论+实践)

#K8S#PVC#PV#StorageClass
2026年 5月 21日

本文主要通过大白话说明白PV、PVC的概念和原理,再说说StorageClass的作用,最后通过实践加深理解。

先来个一句话总结:PV、PVC是K8S用来做存储管理的资源对象,它们让存储资源的使用变得可控,从而保障系统的稳定性、可靠性。StorageClass则是为了减少人工的工作量而去自动化创建PV的组件。所有Pod使用存储只有一个原则:*先规划* → *后申请* → *再使用*

一、理论

1、PV概念

PV是对K8S存储资源的抽象,PV一般由运维人员创建和配置,供容器申请使用。

没有PV之前,服务器的磁盘没有分区的概念,有了PV之后,相当于通过PV对服务器的磁盘进行分区。

2、PVC概念

Pod可以在Kubernetes 中创建和管理的、最小的可部署的计算单元

Namespace(命名空间)是Kubernetes 中的一个抽象概念,用于在同一个物理集群中创建多个虚拟的集群环境。 它为资源对象提供作用域,使得不同Namespace 中的资源可以使用相同的名称而不会冲突,实现逻辑分组和隔离。

PVC 是Pod对存储资源的一个申请,主要包括存储空间申请、访问模式等。创建PV后,Pod就可以通过PVC向PV申请磁盘空间了。类似于某个应用程序向操作系统的D盘申请1G的使用空间。

PVC 创建成功之后,Pod 就可以以存储卷(Volume)的方式使用 PVC 的存储资源了。Pod 在使用 PVC 时必须与PVC在同一个Namespace下。

3、PV / PVC的关系

PV相当于对磁盘的分区,PVC相当于APP(应用程序)向某个分区申请多少空间。比如说安装WPS程序时,一般会告知我们安装它需要多少存储空间,让你选择在某个磁盘下安装。如果将来某个分区磁盘满了,也不会影响别的分区磁盘的使用。

一旦 PV 与PVC绑定,Pod就可以使用这个 PVC 了。如果在系统中没有满足 PVC 要求的 PV,PVC则一直处于 Pending 状态,直到系统里产生了一个合适的 PV。

img

4、StorageClass概念

K8S有两种存储资源的供应模式:静态模式和动态模式,资源供应的最终目的就是将适合的PV与PVC绑定:

  • 静态模式:管理员预先创建许多各种各样的PV,等待PVC申请使用。
  • 动态模式:管理员无须预先创建PV,而是通过StorageClass自动完成PV的创建以及与PVC的绑定。

StorageClass就是动态模式,根据PVC的需求动态创建合适的PV资源,从而实现存储卷的按需创建。

一般某个商业性的应用程序,会用到大量的Pod,如果每个Pod都需要使用存储资源,那么就需要人工时不时的去创建PV,这也是个麻烦事儿。解决方法就是使用动态模式:当Pod通过PVC申请存储资源时,直接通过StorageClass去动态的创建对应大小的PV,然后与PVC绑定,所以基本上PV → PVC是一对一的关系。

5、Provisioner概念

在创建 PVC 时需要指定 StorageClass,PVC 选择到对应的StorageClass后,与其关联的 Provisioner 组件来动态创建 PV 资源。

那Provisioner是个啥呢?其实就一个存储驱动,类似操作系统里的磁盘驱动。

StorageClass 资源对象的定义主要包括:名称、Provisioner、存储的相关参数配置、回收策略。StorageClass一旦被创建,则无法修改,只能删除重新创建。

PV和PVC的生命周期,包括4个阶段:资源供应(Provisioning)、资源绑定(Binding)、资源使用(Using)、资源回收(Reclaiming)。首先旧的有资源供应,说白了就是得有存储驱动,然后才能创建、绑定和使用、回收。

6、使用PV / PVC前后对比

6.1、通过描述对比

在没有使用PV、PVC之前,各个Pod都可以任意的向存储资源里(比如NFS)写数据,随便一个Pod都可以往磁盘上插一杠子,长期下去磁盘的管理会越来越混乱,然后导致数据使用超限,磁盘爆掉,最后导致磁盘上的所有应用全部挂掉。

为了解决这个问题,引入了PV、PVC的概念,达到限制Pod写入存储数据大小的目的,从而更好地保障了系统的可用性、稳定性。

有了PVC、PV之后,所有Pod使用存储资源,保持一个原则:先规划 → 后申请 → 再使用。

那你肯定有一个疑问,“StorageClass是自动化创建PV,跟原本的无序不可控是一样的效果啊,都可以随便占用存储资源啊”。

其实不然,使用StorageClass只是自动化了创建PV的流程,但依旧执行的是一个存储可控的流程。每个Pod使用多少存储空间是固定的,Pod没有办法超额使用存储空间,更不会影响到别的应用,要出故障也只是某个Pod自己出故障。

6.2、通过**图片对比**

没有使用PV、PVC之前的情况,如下面2张图:

img

img

有了PV、PVC之后的情况,如下图:

img

二、实践

1. 集群基础信息确认

首先确认本地 kubeconfig 已经连接到目标集群:

kubectl config current-context

输出:

kubernetes-admin@kubernetes

查看节点状态:

kubectl get nodes

输出:

NAME         STATUS   ROLES           AGE   VERSION
k8s-master   Ready    control-plane   67d   v1.28.2
node1        Ready    <none>          67d   v1.28.2
node2        Ready    <none>          67d   v1.28.2

说明本地可以正常访问远程 Kubernetes 集群。

2. 检查业务服务

查看命名空间:

kubectl get ns

发现已有:

default
dubbo-samples-shop
dubbo-system
logging

查看 Dubbo 相关 Deployment:

kubectl get deploy -A | grep -i dubbo

发现主要服务包括:

default              dubbo-admin-prototype
dubbo-system         dubbo-admin
dubbo-samples-shop   shop-comment-v1
dubbo-samples-shop   shop-detail-v1
dubbo-samples-shop   shop-frontend
dubbo-samples-shop   shop-order-v1
dubbo-samples-shop   shop-user

其中重点调试对象为:

dubbo-system/dubbo-admin

3. 检查 logging 命名空间

查看 logging namespace 下的资源:

kubectl get all -n logging

最开始发现只残留了一个旧 Job:

job.batch/loki-minio-post-job

随后确认当前集群中没有 Helm release:

helm list -A

输出只有表头,没有任何 release。

说明当前集群里没有正在由 Helm 管理的 Loki/Grafana/Alloy 等组件,loki-minio-post-job 只是历史残留。

清理旧 Job:

kubectl -n logging delete job loki-minio-post-job

继续检查 PVC:

kubectl get pvc -n logging

发现残留旧 PVC:

export-0-loki-minio-0   Pending
export-1-loki-minio-0   Pending

这些 PVC 长期处于 Pending,说明之前部署 MinIO/Loki 时动态存储没有成功。

删除旧 PVC:

kubectl -n logging delete pvc export-0-loki-minio-0 export-1-loki-minio-0

4. 发现集群没有 StorageClass

检查 StorageClass:

kubectl get sc

结果发现集群没有可用 StorageClass。

这会导致 Loki/MinIO 这类需要 PVC 的组件无法正常创建存储卷,PVC 会一直处于 Pending

因此决定先部署一个轻量的 local-path-provisioner。

5.给k8s集群设置StorageClass,安装 local-path-provisioner

使用 Rancher local-path-provisioner:

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.36/deploy/local-path-storage.yaml

由于机器资源有限,对 provisioner 设置较小的 requests/limits:

安装后马上执行:

kubectl -n local-path-storage set resources deployment/local-path-provisioner \
  --containers=local-path-provisioner \
  --requests=cpu=10m,memory=32Mi \
  --limits=cpu=100m,memory=128Mi

这个配置很保守:

requests:
  cpu: 10m
  memory: 32Mi
limits:
  cpu: 100m
  memory: 128Mi

对 provisioner 来说一般够用。

查看状态:

kubectl -n local-path-storage get deploy,rs,pod -o wide

最初发现 Pod 状态为:

ImagePullBackOff

6. 处理 local-path-provisioner 镜像拉取失败

查看 Pod 详情:

kubectl -n local-path-storage describe pod <local-path-provisioner-pod>

发现错误:

Failed to pull image "docker.io/rancher/local-path-provisioner:v0.0.36"
dial tcp registry-1.docker.io:443: i/o timeout

说明服务器节点无法正常访问 Docker Hub。

由于没有服务器 SSH 权限,不能直接 ctr import 镜像,因此选择切换镜像源。

将 local-path-provisioner 镜像改为 DaoCloud 镜像源:

kubectl -n local-path-storage set image deployment/local-path-provisioner \
  local-path-provisioner=m.daocloud.io/docker.io/rancher/local-path-provisioner:v0.0.36

等待滚动更新:

kubectl -n local-path-storage rollout status deploy/local-path-provisioner --timeout=180s

查看 Pod:

kubectl -n local-path-storage get pod -o wide

最终 provisioner 成功运行:

local-path-provisioner-85b4c4c598-8r8qx   1/1   Running

检查是否成功

kubectl get sc
kubectl -n local-path-storage get pod

7.然后设置成默认 StorageClass:

local-path 设置为默认 StorageClass:

kubectl patch storageclass local-path \
  -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

理想情况:

local-path (default)

img

PS:注意一个坑

local-path 默认是:

volumeBindingMode: WaitForFirstConsumer

也就是说,PVC 可能会先 Pending,直到有 Pod 真正使用它时才绑定 PV。这不是一定出问题,官方默认 manifest 里就是这个行为。

PSS:另一个关键点:PVC 的 storage 大小不等于硬限制

Rancher 官方 README 也提到,local-path-provisioner 当前不支持真正的 volume capacity limit,容量限制会被忽略。也就是说你写:

resources:
  requests:
    storage: 10Gi

更多是调度/声明意义,不能严格防止它把节点磁盘写爆

8. 做一个 PVC 测试

先创建测试 PVC:

cat > test-pvc.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-local-path-pvc
  namespace: logging
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
EOF

kubectl apply -f test-pvc.yaml

查看:

kubectl get pvc -n logging

注意:因为 local-pathWaitForFirstConsumer,这个 PVC 可能暂时是 Pending,这是正常的。它需要有 Pod 使用它之后才会绑定。

9. 创建一个测试 Pod:

cat > test-pvc-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: test-local-path-pod
  namespace: logging
spec:
  containers:
    - name: busybox
      image: busybox:1.36
      command: ["sh", "-c", "echo hello > /data/test.txt && sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: test-local-path-pvc
EOF

kubectl apply -f test-pvc-pod.yaml

如果 busybox 也拉不到镜像,说明后面 Loki/Grafana 镜像也要继续换镜像源。

查看:

kubectl get pod -n logging
kubectl get pvc -n logging
kubectl get pv

此时发现:

test-local-path-pod   Pending
test-local-path-pvc   Pending
No resources found

10. 排查 PVC 为什么没有 Bound

查看 PVC:

kubectl -n logging describe pvc test-local-path-pvc

可以看到 provisioner 已经开始处理 PVC:

External provisioner is provisioning volume for claim "logging/test-local-path-pvc"

查看 local-path-provisioner 日志:

kubectl -n local-path-storage logs deploy/local-path-provisioner --tail=100

日志显示它正在创建本地目录,并创建 helper pod:

Creating volume ... at node2:/opt/local-path-provisioner/...
create the helper pod helper-pod-create-pvc-... into local-path-storage

查看事件:

kubectl get events -A --sort-by=.lastTimestamp | tail -50

发现 helper pod 拉取默认 busybox 镜像失败:

Failed to pull image "docker.io/library/busybox"
dial tcp registry-1.docker.io:443: i/o timeout

也就是说,local-path-provisioner 本身已经运行,但它创建 PV 时依赖的 helper pod 仍然使用 Docker Hub 的 busybox,导致 PVC 无法完成绑定。

11. 修改 local-path-provisioner 的 helper 镜像

查看 ConfigMap:

kubectl -n local-path-storage get cm local-path-config -o yaml

编辑 ConfigMap:

kubectl -n local-path-storage edit cm local-path-config

找到 helperPod.yaml 中的:

image: busybox

改为当前集群可访问的镜像源:

image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/busybox:1.37.0

因为集群事件中已经显示该镜像曾成功运行过:

swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/busybox:1.37.0

12. 重启 provisioner 并重新测试

重启 local-path-provisioner:

kubectl -n local-path-storage rollout restart deploy/local-path-provisioner
kubectl -n local-path-storage rollout status deploy/local-path-provisioner --timeout=180s

清理失败的测试资源:

kubectl -n local-path-storage delete pod helper-pod-create-pvc-80b8ad84-cb27-4bda-a3ae-99b38f747af7 --ignore-not-found
kubectl -n logging delete pod test-local-path-pod --ignore-not-found
kubectl -n logging delete pvc test-local-path-pvc --ignore-not-found

重新创建 PVC 和 Pod:

kubectl apply -f test-pvc.yaml
kubectl apply -f test-pvc-pod.yaml

再次检查:

kubectl get pod -n logging
kubectl get pvc -n logging
kubectl get pv

最终期望结果:

test-local-path-pod   1/1   Running
test-local-path-pvc   Bound
pvc-xxxx              Bound

说明 local-path-provisioner 已经可以正常为 PVC 动态创建 PV。

13. 测试完成后清理

确认测试成功后,删除测试 Pod 和 PVC:

kubectl -n logging delete pod test-local-path-pod --ignore-not-found
kubectl -n logging delete pvc test-local-path-pvc --ignore-not-found

至此,我们实践过程全部结束。

三、总结

本文主要讲解了PV、PVC、StorageClass的理论和实战。

一句话总结:PV、PVC是K8S用来做存储管理的资源对象,它们让存储资源的使用变得可控,从而保障系统的稳定性、可靠性。StorageClass则是为了减少人工的工作量而去自动化创建PV的组件。所有Pod使用存储只有一个原则:先规划后申请再使用

参考文献:《Kubernetes权威指南》

编辑