如果你刚接手K8s集群面对一堆Pending的PVC不知道从哪查起如果你给MySQL配了持久化存储Pod一重启发现数据全没了如果你还在手动一个个创建PV维护得想骂人——那这篇文档就是为你准备的。我在这行干了10年K8s存储这块踩过的坑足够写一本书这篇算是精华版。前置条件已部署K8s集群我用的是v1.28更高版本也能用kubectl已配置好能连集群了解基本的Pod和Deployment概念一、先搞懂这三个东西到底是什么用最土的话说我在一开始也被PV、PVC、StorageClass这几个名词绕晕过。用大白话解释PVPersistentVolume—— 就像一个仓库管理员提前准备好的存储空间。仓库有多大、在哪个位置、是什么类型都是PV里定义的。PVCPersistentVolumeClaim—— 就是你提交的租仓库申请单写上“我要5GB空间”。k8s拿着这个申请单去匹配仓库。StorageClass—— 仓库的分类标准。高性能SSD仓库、普通HDD仓库、便宜大碗仓库分门别类。有了它你就可以直接说“我要一个高性能SSD仓库”系统自动帮你创建好。PV的生命周期这块面试常考实战也容易掉坑PV有4个状态——Available空闲待用、Bound已被某PVC绑定占用、ReleasedPVC删了但PV还没清理完、Failed回收失败了。回收策略三种Retain保留数据给你手动清理的机会、Recycle之前会帮你rm -rf清理但现在已经废弃了别用了、DeletePVC一删PV和底层存储一块删。提醒Recycle策略在K8s v1.20里标记为废弃了别在生产里用了。二、两种玩法静态供给 vs 动态供给K8s的存储支持两种玩法看你的场景选择。静态供给管理员手动创建一批PV用户提交PVC时系统从这批PV里找匹配的绑定。适合小规模环境或者有合规要求比如数据必须放在指定NFS服务器上的场景。缺点就是手动维护PV太麻烦规模大了根本维护不过来。动态供给配置好StorageClass用户提交PVC时系统自动创建PV。这是生产环境的主流做法。我强烈推荐用动态供给除非你有特殊原因必须用静态。三、实战让静态供给跑起来先把静态供给搞明白这是理解动态供给的基础。3.1 创建PVapiVersion: v1 kind: PersistentVolume metadata: name: my-static-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain # 重要删PVC时保留数据 nfs: server: 192.168.1.100 path: /data/nfs-shareaccessModes三种模式ReadWriteOnce单个节点可读写最常用云盘类存储标配ReadOnlyMany多个节点可同时读ReadWriteMany多个节点可同时读写需要NFS这类共享存储支持3.2 创建PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-static-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi绑定规则PVC申请的容量必须≤ PV提供的容量访问模式必须匹配。如果找不到匹配的PVPVC会一直Pending。3.3 在Pod中使用apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: app image: nginx volumeMounts: - name: storage mountPath: /data volumes: - name: storage persistentVolumeClaim: claimName: my-static-pvc四、进阶动态供给StorageClass这才是生产就绪的配置动态供给是我在生产环境里最常用的方式省心太多了。4.1 云环境下的StorageClass以AWS为例apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: kubernetes.io/aws-ebs parameters: type: gp3 # gp3便宜gp2老但兼容性好 fsType: ext4 reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer # 这个很重要后面详细说 allowVolumeExpansion: true # 允许在线扩容WaitForFirstConsumer是啥意思就是PV的创建和绑定要等到真正有Pod用这个PVC时才执行。这样PV会在Pod被调度的节点所在的可用区创建避免跨可用区挂载失败。4.2 自建NFS的动态供给我的最爱因为便宜如果你没有云厂商的CSI插件可以用NFS配合nfs-subdir-external-provisioner来实现动态供给。# 1. 创建StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: pathPattern: ${.PVC.namespace}/${.PVC.name} # 子目录按命名空间/PVC名组织 onDelete: delete # PVC删除时删目录 reclaimPolicy: Delete # 2. 部署NFS Provisioner用Helm最简单 helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --set nfs.server192.168.1.100 \ --set nfs.path/data/nfs-k8s然后用下面的PVC就能自动创建PV了apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data spec: storageClassName: nfs-storage accessModes: - ReadWriteMany resources: requests: storage: 10Gi4.3 Local SSD高性能存储数据库必备对于数据库这类需要高IOPS的场景Local PV是最佳选择。测试数据显示NVMe SSD作为Local Volume时单盘可提供超过50万IOPS。但有个坑Local PV绑定在特定节点上节点故障时数据就不可用了。# StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-ssd provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer # Local PV必须用这个模式 # PV手动创建绑定到指定节点 apiVersion: v1 kind: PersistentVolume metadata: name: local-pv-node1 spec: capacity: storage: 100Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-ssd local: path: /mnt/disks/ssd1 nodeAffinity: # 必须指定节点 required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node1五、PVC Pending了怎么办教你三板斧排查90%的问题能搞定PVC Pending是所有K8s运维最常见的报错我在这上面栽过的跟头能绕地球一圈。第一步看事件kubectl describe pvc pvc-name -n namespace事件里会写清楚原因。第二步检查StorageClass是否存在kubectl get sc # 确认PVC里写的storageClassName存在 kubectl get sc storageclass-name -o yamlStorageClass找不到是最常见的原因之一。第三步检查动态供给器是否正常kubectl get pods -n kube-system | grep -E provisioner|csi kubectl logs -n kube-system provisioner-pod如果动态供给器挂了PV就创建不出来。还有一个隐藏坑容量不足PVC请求的容量超过PV可用容量或者ResourceQuota限制了namespace的总存储上限。检查一下kubectl get resourcequota -n namespace六、PVC删不掉、Terminating卡住的骚操作这是最让人抓狂的问题之一我遇到过好几次。原因1还有Pod在用这个PVC# 找出使用该PVC的Pod kubectl get pods --all-namespaces -o json | jq .items[] | select(.spec.volumes[].persistentVolumeClaim.claimName your-pvc-name)找到后删掉或用deployment scale成0。原因2Finalizers卡住了# 强制移除finalizer kubectl patch pvc pvc-name -p {metadata:{finalizers:null}} --typemerge这个操作要谨慎相当于绕过了K8s的正常保护机制确保真的没有业务在使用这个PVC再干。原因3StorageClass设置了RetainPVC删了PV还在但不影响使用。如果想自动删除改StorageClass或PV的reclaimPolicy为Delete。七、一个坑从静态迁移到动态存储类切换老旧集群升级、换了云厂商都需要做StorageClass迁移。手动搞太容易出错好在有工具。推荐工具pvmigrate# 将使用default这个StorageClass的所有PVC迁移到fast-ssd pvmigrate --source-sc default --dest-sc fast-ssdpvmigrate会帮你验证两边的StorageClass存在 → 找出现有PVC → 创建新PVC → 停掉使用这些PVC的Pod → 用rsync拷贝数据 → 切换PVC关联 → 恢复Pod。局限只支持StatefulSet和DeploymentDaemonSet和Job不支持。另外如果迁移中途断了恢复不太完美建议在维护窗口操作。八、安全与权限最小权限原则很多人忽略RBAC结果莫名其妙报Permission denied。# 给Pod配置ServiceAccount只给必要的PVC操作权限 apiVersion: v1 kind: ServiceAccount metadata: name: app-sa --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: pvc-manager rules: - apiGroups: [] resources: [persistentvolumeclaims] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: app-pvc-access subjects: - kind: ServiceAccount name: app-sa roleRef: kind: Role name: pvc-manager apiGroup: rbac.authorization.k8s.iofsGroup和runAsUser确保Pod以非root用户运行并且用fsGroup设置存储卷的访问权限spec: securityContext: runAsUser: 1000 runAsGroup: 3000 fsGroup: 2000 # 重要设置存储卷的组权限九、彩蛋几个藏得很深的坑subPath的硬链接风险使用subPath挂载卷的子目录时如果宿主机上有人创建了指向敏感文件的符号链接可能导致容器逃逸。我一般不推荐用subPath挂载外部存储卷中的根目录如果非要用确保以低权限用户运行并且监控容器行为。挂载系统目录会导致容器异常千万别把存储卷挂载到容器的/、/var/run等系统目录下会导致容器启动失败。我亲眼见过同事把PVC挂到/上整个Pod直接崩溃排查了半小时才发现。动态PV的回收策略默认是Delete很容易丢数据如果你用动态供给创建了PV但没有在StorageClass里把reclaimPolicy改成Retain哪天不小心删了PVCPV和底层数据直接无了。救都救不回来。所以我建议生产环境的StorageClass一律把reclaimPolicy设成Retain等数据确认不需要了再手动清理。WaitForFirstConsumer的威力在使用EBS这类zone级别存储时如果不设置WaitForFirstConsumerPVC会立即创建PV但可能落在和Pod调度不同的可用区导致Pod无法挂载。这个坑我踩过两次才长记性。PVC删除后PV的Released状态无法直接复用PV处于Released状态时虽然数据还在但不能直接绑定给新的PVC。你需要手动编辑PV删掉claimRef字段让它回到Available状态才能重新绑定。或者更简单的方法是删除PV自己重建。收个尾K8s存储说难不难说简单也真不简单。核心就是三件事搞清楚静态和动态的区别、把StorageClass配置好、懂得排查Pending的原因。上面这些配置和命令我都是在生产环境里反复试过、踩过坑才总结出来的希望能帮到正在跟存储死磕的你。三句话总结PV是仓库PVC是申请单StorageClass是仓库类型模板生产环境用动态供给 WaitForFirstConsumer别手动创建PVPV的回收策略设成Retain别用Delete除非你知道自己在干嘛如果你在用Local PV做数据库存储或者遇到过什么我上面没提到的奇葩报错欢迎在评论区分享一下——我挺好奇你们是怎么踩坑的。
K8s存储三剑客避坑指南:PV/PVC/StorageClass生产级配置实战
发布时间:2026/6/3 17:49:14
如果你刚接手K8s集群面对一堆Pending的PVC不知道从哪查起如果你给MySQL配了持久化存储Pod一重启发现数据全没了如果你还在手动一个个创建PV维护得想骂人——那这篇文档就是为你准备的。我在这行干了10年K8s存储这块踩过的坑足够写一本书这篇算是精华版。前置条件已部署K8s集群我用的是v1.28更高版本也能用kubectl已配置好能连集群了解基本的Pod和Deployment概念一、先搞懂这三个东西到底是什么用最土的话说我在一开始也被PV、PVC、StorageClass这几个名词绕晕过。用大白话解释PVPersistentVolume—— 就像一个仓库管理员提前准备好的存储空间。仓库有多大、在哪个位置、是什么类型都是PV里定义的。PVCPersistentVolumeClaim—— 就是你提交的租仓库申请单写上“我要5GB空间”。k8s拿着这个申请单去匹配仓库。StorageClass—— 仓库的分类标准。高性能SSD仓库、普通HDD仓库、便宜大碗仓库分门别类。有了它你就可以直接说“我要一个高性能SSD仓库”系统自动帮你创建好。PV的生命周期这块面试常考实战也容易掉坑PV有4个状态——Available空闲待用、Bound已被某PVC绑定占用、ReleasedPVC删了但PV还没清理完、Failed回收失败了。回收策略三种Retain保留数据给你手动清理的机会、Recycle之前会帮你rm -rf清理但现在已经废弃了别用了、DeletePVC一删PV和底层存储一块删。提醒Recycle策略在K8s v1.20里标记为废弃了别在生产里用了。二、两种玩法静态供给 vs 动态供给K8s的存储支持两种玩法看你的场景选择。静态供给管理员手动创建一批PV用户提交PVC时系统从这批PV里找匹配的绑定。适合小规模环境或者有合规要求比如数据必须放在指定NFS服务器上的场景。缺点就是手动维护PV太麻烦规模大了根本维护不过来。动态供给配置好StorageClass用户提交PVC时系统自动创建PV。这是生产环境的主流做法。我强烈推荐用动态供给除非你有特殊原因必须用静态。三、实战让静态供给跑起来先把静态供给搞明白这是理解动态供给的基础。3.1 创建PVapiVersion: v1 kind: PersistentVolume metadata: name: my-static-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain # 重要删PVC时保留数据 nfs: server: 192.168.1.100 path: /data/nfs-shareaccessModes三种模式ReadWriteOnce单个节点可读写最常用云盘类存储标配ReadOnlyMany多个节点可同时读ReadWriteMany多个节点可同时读写需要NFS这类共享存储支持3.2 创建PVCapiVersion: v1 kind: PersistentVolumeClaim metadata: name: my-static-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi绑定规则PVC申请的容量必须≤ PV提供的容量访问模式必须匹配。如果找不到匹配的PVPVC会一直Pending。3.3 在Pod中使用apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: app image: nginx volumeMounts: - name: storage mountPath: /data volumes: - name: storage persistentVolumeClaim: claimName: my-static-pvc四、进阶动态供给StorageClass这才是生产就绪的配置动态供给是我在生产环境里最常用的方式省心太多了。4.1 云环境下的StorageClass以AWS为例apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: kubernetes.io/aws-ebs parameters: type: gp3 # gp3便宜gp2老但兼容性好 fsType: ext4 reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer # 这个很重要后面详细说 allowVolumeExpansion: true # 允许在线扩容WaitForFirstConsumer是啥意思就是PV的创建和绑定要等到真正有Pod用这个PVC时才执行。这样PV会在Pod被调度的节点所在的可用区创建避免跨可用区挂载失败。4.2 自建NFS的动态供给我的最爱因为便宜如果你没有云厂商的CSI插件可以用NFS配合nfs-subdir-external-provisioner来实现动态供给。# 1. 创建StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: pathPattern: ${.PVC.namespace}/${.PVC.name} # 子目录按命名空间/PVC名组织 onDelete: delete # PVC删除时删目录 reclaimPolicy: Delete # 2. 部署NFS Provisioner用Helm最简单 helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/ helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --set nfs.server192.168.1.100 \ --set nfs.path/data/nfs-k8s然后用下面的PVC就能自动创建PV了apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data spec: storageClassName: nfs-storage accessModes: - ReadWriteMany resources: requests: storage: 10Gi4.3 Local SSD高性能存储数据库必备对于数据库这类需要高IOPS的场景Local PV是最佳选择。测试数据显示NVMe SSD作为Local Volume时单盘可提供超过50万IOPS。但有个坑Local PV绑定在特定节点上节点故障时数据就不可用了。# StorageClass apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-ssd provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer # Local PV必须用这个模式 # PV手动创建绑定到指定节点 apiVersion: v1 kind: PersistentVolume metadata: name: local-pv-node1 spec: capacity: storage: 100Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-ssd local: path: /mnt/disks/ssd1 nodeAffinity: # 必须指定节点 required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node1五、PVC Pending了怎么办教你三板斧排查90%的问题能搞定PVC Pending是所有K8s运维最常见的报错我在这上面栽过的跟头能绕地球一圈。第一步看事件kubectl describe pvc pvc-name -n namespace事件里会写清楚原因。第二步检查StorageClass是否存在kubectl get sc # 确认PVC里写的storageClassName存在 kubectl get sc storageclass-name -o yamlStorageClass找不到是最常见的原因之一。第三步检查动态供给器是否正常kubectl get pods -n kube-system | grep -E provisioner|csi kubectl logs -n kube-system provisioner-pod如果动态供给器挂了PV就创建不出来。还有一个隐藏坑容量不足PVC请求的容量超过PV可用容量或者ResourceQuota限制了namespace的总存储上限。检查一下kubectl get resourcequota -n namespace六、PVC删不掉、Terminating卡住的骚操作这是最让人抓狂的问题之一我遇到过好几次。原因1还有Pod在用这个PVC# 找出使用该PVC的Pod kubectl get pods --all-namespaces -o json | jq .items[] | select(.spec.volumes[].persistentVolumeClaim.claimName your-pvc-name)找到后删掉或用deployment scale成0。原因2Finalizers卡住了# 强制移除finalizer kubectl patch pvc pvc-name -p {metadata:{finalizers:null}} --typemerge这个操作要谨慎相当于绕过了K8s的正常保护机制确保真的没有业务在使用这个PVC再干。原因3StorageClass设置了RetainPVC删了PV还在但不影响使用。如果想自动删除改StorageClass或PV的reclaimPolicy为Delete。七、一个坑从静态迁移到动态存储类切换老旧集群升级、换了云厂商都需要做StorageClass迁移。手动搞太容易出错好在有工具。推荐工具pvmigrate# 将使用default这个StorageClass的所有PVC迁移到fast-ssd pvmigrate --source-sc default --dest-sc fast-ssdpvmigrate会帮你验证两边的StorageClass存在 → 找出现有PVC → 创建新PVC → 停掉使用这些PVC的Pod → 用rsync拷贝数据 → 切换PVC关联 → 恢复Pod。局限只支持StatefulSet和DeploymentDaemonSet和Job不支持。另外如果迁移中途断了恢复不太完美建议在维护窗口操作。八、安全与权限最小权限原则很多人忽略RBAC结果莫名其妙报Permission denied。# 给Pod配置ServiceAccount只给必要的PVC操作权限 apiVersion: v1 kind: ServiceAccount metadata: name: app-sa --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: pvc-manager rules: - apiGroups: [] resources: [persistentvolumeclaims] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: app-pvc-access subjects: - kind: ServiceAccount name: app-sa roleRef: kind: Role name: pvc-manager apiGroup: rbac.authorization.k8s.iofsGroup和runAsUser确保Pod以非root用户运行并且用fsGroup设置存储卷的访问权限spec: securityContext: runAsUser: 1000 runAsGroup: 3000 fsGroup: 2000 # 重要设置存储卷的组权限九、彩蛋几个藏得很深的坑subPath的硬链接风险使用subPath挂载卷的子目录时如果宿主机上有人创建了指向敏感文件的符号链接可能导致容器逃逸。我一般不推荐用subPath挂载外部存储卷中的根目录如果非要用确保以低权限用户运行并且监控容器行为。挂载系统目录会导致容器异常千万别把存储卷挂载到容器的/、/var/run等系统目录下会导致容器启动失败。我亲眼见过同事把PVC挂到/上整个Pod直接崩溃排查了半小时才发现。动态PV的回收策略默认是Delete很容易丢数据如果你用动态供给创建了PV但没有在StorageClass里把reclaimPolicy改成Retain哪天不小心删了PVCPV和底层数据直接无了。救都救不回来。所以我建议生产环境的StorageClass一律把reclaimPolicy设成Retain等数据确认不需要了再手动清理。WaitForFirstConsumer的威力在使用EBS这类zone级别存储时如果不设置WaitForFirstConsumerPVC会立即创建PV但可能落在和Pod调度不同的可用区导致Pod无法挂载。这个坑我踩过两次才长记性。PVC删除后PV的Released状态无法直接复用PV处于Released状态时虽然数据还在但不能直接绑定给新的PVC。你需要手动编辑PV删掉claimRef字段让它回到Available状态才能重新绑定。或者更简单的方法是删除PV自己重建。收个尾K8s存储说难不难说简单也真不简单。核心就是三件事搞清楚静态和动态的区别、把StorageClass配置好、懂得排查Pending的原因。上面这些配置和命令我都是在生产环境里反复试过、踩过坑才总结出来的希望能帮到正在跟存储死磕的你。三句话总结PV是仓库PVC是申请单StorageClass是仓库类型模板生产环境用动态供给 WaitForFirstConsumer别手动创建PVPV的回收策略设成Retain别用Delete除非你知道自己在干嘛如果你在用Local PV做数据库存储或者遇到过什么我上面没提到的奇葩报错欢迎在评论区分享一下——我挺好奇你们是怎么踩坑的。