如果你刚接手K8s集群,面对一堆Pending的PVC不知道从哪查起;如果你给MySQL配了持久化存储,Pod一重启发现数据全没了;如果你还在手动一个个创建PV,维护得想骂人——那这篇文档就是为你准备的。我在这行干了10年,K8s存储这块踩过的坑足够写一本书,这篇算是精华版。

前置条件

  • 已部署K8s集群(我用的是v1.28,更高版本也能用)
  • kubectl已配置好,能连集群
  • 了解基本的Pod和Deployment概念

一、先搞懂这三个东西到底是什么(用最土的话说)

我在一开始也被PV、PVC、StorageClass这几个名词绕晕过。用大白话解释:

  • PV(PersistentVolume) —— 就像一个仓库,管理员提前准备好的存储空间。仓库有多大、在哪个位置、是什么类型,都是PV里定义的。
  • PVC(PersistentVolumeClaim) —— 就是你提交的租仓库申请单,写上“我要5GB空间”。k8s拿着这个申请单去匹配仓库。
  • StorageClass —— 仓库的分类标准。高性能SSD仓库、普通HDD仓库、便宜大碗仓库,分门别类。有了它,你就可以直接说“我要一个高性能SSD仓库”,系统自动帮你创建好。

PV的生命周期(这块面试常考,实战也容易掉坑) :PV有4个状态——Available(空闲待用)、Bound(已被某PVC绑定占用)、Released(PVC删了但PV还没清理完)、Failed(回收失败了)。

回收策略三种:Retain(保留数据,给你手动清理的机会)、Recycle(之前会帮你rm -rf清理,但现在已经废弃了,别用了)、Delete(PVC一删,PV和底层存储一块删)。

提醒:Recycle策略在K8s v1.20+里标记为废弃了,别在生产里用了。

二、两种玩法:静态供给 vs 动态供给

K8s的存储支持两种玩法,看你的场景选择。

静态供给:管理员手动创建一批PV,用户提交PVC时系统从这批PV里找匹配的绑定。适合小规模环境,或者有合规要求(比如数据必须放在指定NFS服务器上)的场景。缺点就是手动维护PV太麻烦,规模大了根本维护不过来。

动态供给:配置好StorageClass,用户提交PVC时系统自动创建PV。这是生产环境的主流做法。我强烈推荐用动态供给,除非你有特殊原因必须用静态。

三、实战:让静态供给跑起来

先把静态供给搞明白,这是理解动态供给的基础。

3.1 创建PV
apiVersion: 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-share

accessModes三种模式:

  • ReadWriteOnce:单个节点可读写(最常用,云盘类存储标配)
  • ReadOnlyMany:多个节点可同时读
  • ReadWriteMany:多个节点可同时读写(需要NFS这类共享存储支持)
3.2 创建PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-static-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

绑定规则:PVC申请的容量必须≤ PV提供的容量,访问模式必须匹配。如果找不到匹配的PV,PVC会一直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.server=192.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: 10Gi
4.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 yaml

StorageClass找不到是最常见的原因之一。

第三步:检查动态供给器是否正常

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。

原因2:Finalizers卡住了

# 强制移除finalizer
kubectl patch pvc <pvc-name> -p '{"metadata":{"finalizers":null}}' --type=merge

这个操作要谨慎,相当于绕过了K8s的正常保护机制,确保真的没有业务在使用这个PVC再干。

原因3:StorageClass设置了Retain,PVC删了PV还在,但不影响使用。如果想自动删除,改StorageClass或PV的reclaimPolicy为Delete。

七、一个坑:从静态迁移到动态(存储类切换)

老旧集群升级、换了云厂商,都需要做StorageClass迁移。手动搞太容易出错,好在有工具。

推荐工具:pvmigrate

# 将使用default这个StorageClass的所有PVC迁移到fast-ssd
pvmigrate --source-sc default --dest-sc fast-ssd

pvmigrate会帮你:验证两边的StorageClass存在 → 找出现有PVC → 创建新PVC → 停掉使用这些PVC的Pod → 用rsync拷贝数据 → 切换PVC关联 → 恢复Pod。

局限:只支持StatefulSet和Deployment,DaemonSet和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.io

fsGroup和runAsUser:确保Pod以非root用户运行,并且用fsGroup设置存储卷的访问权限:

spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000   # 重要:设置存储卷的组权限

九、彩蛋:几个藏得很深的坑

  1. subPath的硬链接风险:使用subPath挂载卷的子目录时,如果宿主机上有人创建了指向敏感文件的符号链接,可能导致容器逃逸。我一般不推荐用subPath挂载外部存储卷中的根目录,如果非要用,确保以低权限用户运行,并且监控容器行为。
  2. 挂载系统目录会导致容器异常:千万别把存储卷挂载到容器的//var/run等系统目录下,会导致容器启动失败。我亲眼见过同事把PVC挂到/上,整个Pod直接崩溃,排查了半小时才发现。
  3. 动态PV的回收策略默认是Delete,很容易丢数据:如果你用动态供给创建了PV,但没有在StorageClass里把reclaimPolicy改成Retain,哪天不小心删了PVC,PV和底层数据直接无了。救都救不回来。所以我建议:生产环境的StorageClass一律把reclaimPolicy设成Retain,等数据确认不需要了再手动清理。
  4. WaitForFirstConsumer的威力:在使用EBS这类zone级别存储时,如果不设置WaitForFirstConsumer,PVC会立即创建PV,但可能落在和Pod调度不同的可用区,导致Pod无法挂载。这个坑我踩过两次才长记性。
  5. PVC删除后PV的Released状态无法直接复用:PV处于Released状态时,虽然数据还在,但不能直接绑定给新的PVC。你需要手动编辑PV,删掉claimRef字段,让它回到Available状态,才能重新绑定。或者,更简单的方法是删除PV自己重建。

收个尾

K8s存储说难不难,说简单也真不简单。核心就是三件事:搞清楚静态和动态的区别、把StorageClass配置好、懂得排查Pending的原因。上面这些配置和命令,我都是在生产环境里反复试过、踩过坑才总结出来的,希望能帮到正在跟存储死磕的你。

三句话总结

  • PV是仓库,PVC是申请单,StorageClass是仓库类型模板
  • 生产环境用动态供给 + WaitForFirstConsumer,别手动创建PV
  • PV的回收策略设成Retain,别用Delete,除非你知道自己在干嘛

如果你在用Local PV做数据库存储,或者遇到过什么我上面没提到的奇葩报错,欢迎在评论区分享一下——我挺好奇你们是怎么踩坑的。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐