首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Kubernetes持久化存储实战:PV、PVC与StorageClass全解
📅 2026/10/16 10:21:39
✍️ 爱科研究院
👁 阅读 3,247
我最早接触 Kubernetes 的时候对 PVPersistentVolume和 PVCPersistentVolumeClaim这套东西其实挺抵触的。存储不就是一个挂载吗为什么非要拆成两个资源对象还要再搭一个 StorageClass绕一大圈学习成本瞬间就上去了。后来在项目里因为 Pod 重建导致数据丢到不同节点、因为回收策略没弄对导致底层数据被清掉才真正明白这套抽象的用意——它不是给运维添麻烦而是把“用户怎么申请存储”和“平台怎么供给存储”这两件事彻底解耦。这篇文章就从这两个问题出发把 PV、PVC 以及 StorageClass 的完整链路讲清楚包括绑定规则、生命周期、访问模式、实操步骤和几个我踩过很多次的坑。无论你是刚开始接触 K8s还是已经写了一段时间 YAML 但没系统梳理过存储模型这篇文章都适合你参考。1. 搞懂 PV/PVC 之前先搞清楚它在解决什么问题1.1 直接把存储挂进 Pod 为什么不行很多新手会想Pod 里面挂一个hostPath或者emptyDir不就能存数据了吗确实能存但当你开始考虑 Pod 被删掉、被重新调度、被滚动更新的时候问题就来了。hostPath依赖节点上的目录Pod 一旦被调度到另一台节点旧节点上的数据对它就不可见了。更麻烦的是如果 Pod 是一个副本数为 3 的 Deployment这三个副本可能被调度到三台不同节点上它们各自挂载的是各自节点的本机目录写进去的数据彼此完全隔离根本不像“同一个卷”。emptyDir就更直接了跟 Pod 生命周期绑定Pod 一删除目录就没了等于没有持久化。从本质上看这两个卷类型都没有解决存储和节点、存储和 Pod 生命周期解耦的问题。你真正需要的是一份这样的数据目录Pod 重建不影响它节点换了也不影响它多个副本需要时可以共享读写它。这就是 PV/PVC 存在的意义。1.2 PV、PVC、StorageClass 三者到底怎么分工我用一个尽可能生活化的类比来帮助理解。把存储看作一个超市的仓库PV 是仓库里已经进货、贴着价格标签的商品PVC 是你手里的购物清单写着你要多少数量、需要什么规格StorageClass 则是超市的自动补货系统货架上没有合适商品时它按标准规格去进货、贴标签、放上货架。你只需要把购物清单交出去补货系统怎么干活、跟哪个供应商对接跟你无关。对应到 K8s 里PV是集群级别的资源由管理员预先创建或者由 StorageClass 动态创建。它描述的是“这块存储长什么样”容量是多大、访问模式是什么、底层是 NFS 还是云盘、路径是哪里。PVC是命名空间级别的资源由应用开发者创建。它只描述“我需要的存储是什么样”多大容量、什么访问模式、使用哪个 StorageClass。StorageClass是一个供给模板。PVC 找不到现成 PV 时K8s 会调用里面的 provisioner 去创建新的 PV也就是动态供给。这里有一个非常关键的特征PVC 和 PV 是一对一绑定的。一个 PVC 只消费一个 PV没有共享、没有叠加。所以如果你的应用需要三份不同数据的副本你需要三个 PVC而不是一个 PVC 挂在三个 Pod 上不同的应用和 StatefulSet 场景后面会单独说。2. 从创建到回收PV/PVC 的完整生命周期2.1 PVC 如何匹配到一个 PV绑定条件的拆解当你在集群里创建了一个 PVCK8s 的控制面会在所有 PV 里寻找满足条件的那个然后做绑定。匹配条件必须全部满足缺一个都不行。条件一是访问模式要匹配。PVC 要求的访问模式必须在 PV 支持的访问模式集合里。比如 PV 支持ReadWriteOnce和ReadWriteMany而 PVC 只声明了ReadWriteOnce那可以用如果 PVC 声明的是ReadWriteMany而这个 PV 只有ReadWriteOnce那这个 PV 就不匹配。条件二是容量要满足。PV 的容量必须大于等于PVC 请求的容量。注意这里不是等于PVC 要 2Gi一个 5Gi 的 PV 照样可以绑定。这也是很多人在 YAML 里写“刚好相等”的误区其实没这个必要。条件三是 StorageClass 要一致。如果 PVC 里配了storageClassName那它只会找storageClassName相同的 PV如果 PVC 不配而集群里有默认 StorageClass会被默认 StorageClass 接管如果 PVC 和 PV 都想走静态绑定需要显式把storageClassName置为否则可能被默认 StorageClass 干扰。条件四是标签选择器。PV 可以定义labelsPVC 可以通过selector指定匹配的标签。这个通常用于运维管理想把某个特定 PV 精确分给某个 PVC 的场景。2.2 状态流转Pending、Bound、Released写清楚状态流转排查问题会少走很多弯路。一个 PV 会经历这样几个阶段Available还没被任何 PVC 绑定可以随时匹配。Bound已经被某个 PVC 消费这时候想再绑定其他 PVC 是不行的除非解绑。ReleasedPVC 被删除PV 进入待回收状态。但注意Released 并不等于可以重新上架必须看回收策略。Failed自动回收失败时的最终状态这个状态通常需要人工介入。PVC 的状态则比较简单Pending还没找到 PV、Bound已绑定、Lost绑定的 PV 丢失。这里我想重点强调一下Release这个状态。很多人第一次删 PVC 的时候会惊讶PVC 没了但 PV 还在那里状态变成 Released而且这个 PV 不能马上再绑定给新的 PVC。为什么因为 PV 里还写着claimRef也就是它过去被谁用过。控制面为了保证数据安全不会自动让你把一块可能还残留数据的卷直接分配出去。你得先决定怎么处理这些数据再手动把 claimRef 清掉PV 才会重新变成 Available。这是一个特点不是 bug。2.3 三种回收策略的真实区别persistentVolumeReclaimPolicy是 PV 上最重要的一个字段它决定 PVC 删除后 PV 及底层存储的数据怎么处置。默认值不是固定不变的很多 StorageClass 会自定义但静态创建 PV 时默认是Retain。Retain保留底层数据PV 进入 Released。需要管理员手动处理要么直接删掉这个 PV 重新建、要么手工清理数据后清 claimRef 再复用。数据安全级别最高误操作成本最低但运维成本也最高。Delete删除 PVC 时K8s 会调用底层存储插件把存储资源一起删掉。云厂商的云盘、动态供给的 StorageClass 多数用这个策略。方便但千万别在没备份的情况下删除 PVC数据会瞬间消失。Recycle早期版本提供的自动清理再复用功能实现方式是删除卷内所有文件然后状态回到 Available。现在基本废弃了不建议在现代版本里依赖它。我自己的习惯是涉及数据库、重要日志、用户上传文件全部用 Retain临时测试数据、可以随时重建的中间结果用 Delete。宁可多做几步清理动作也不要因为省事把数据搞没了。3. 访问模式不是随便选的AccessModes 与底层存储选型3.1 三种访问模式的实际含义K8s 支持三种常见访问模式缩写分别是 RWO、ROX、RWX。很多人以为ReadWriteOnce是“只允许一个 Pod 读写”这个理解在大多数场景下是对的但说法不严谨。严格讲RWO 的意思是某个节点上的 Pod 可以以读写方式挂载该卷也就是同一节点上的多个 Pod 是可以同时挂载同一个 RWO 卷的但跨节点不行。在实际排查中遇到 RWO 卷被两个不同节点的 Pod 同时挂载导致数据读写异常是非常常见的。比如默认云盘的ReadWriteOnce两个副本通过 Deployment 调度到不同节点都想挂同一块盘这时候第二个 Pod 会一直 ContainerCreating卡在 volume mount 阶段。ReadOnlyManyROX就是允许被多个节点同时只读挂载。ReadWriteManyRWX则允许所有节点同时以读写方式挂载这是难度最高的模式不是所有存储后端都支持。如果你的后端是某个云厂商的普通云盘基本只有 RWO如果要做到 RWX 跨节点共享就得考虑 NFS、CephFS 这类共享文件系统。3.2 不同存储后端怎么选一张对照表存储后端支持访问模式特点适用场景hostPath/local PVRWO性能最好、无网络开销单节点测试、需要极高磁盘 IO 的有状态组件NFSRWO、ROX、RWX实现简单、兼容好、性能受网络影响共享文件、多副本读写、测试环境云厂商云盘RWO性能稳定、快照方便、有成本数据库、单副本有状态服务CephFSRWO、ROX、RWX分布式、能力全面、部署复杂大规模共享存储、生产环境多读多写选型逻辑其实就一句话单副本有状态服务优先选块存储云盘/local PV因为块存储延迟低、吞吐高多副本需要共享数据才上文件存储NFS/CephFS。别上来就 NFS 一把梭如果应用是数据库跑在 NFS 上的性能通常很难看。3.3 StorageClass 动态供给与 WaitForFirstConsumer静态 PV 适合存储资源固定、集群规模小的场景。集群大了以后管理员不可能每个应用都提前建好 PV所以动态供给成了主流。动态供给就是在 StorageClass 里约好 provisioner 和参数PVC 创建后系统自动创建 PV。一个典型的 StorageClass 长这样apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-storage provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: archiveOnDelete: false reclaimPolicy: Delete volumeBindingMode: Immediate allowVolumeExpansion: truevolumeBindingMode是一个值得关注的字段。Immediate 模式下PVC 创建后立刻创建 PVWaitForFirstConsumer模式下要等 Pod 调度到节点后才会创建 PV。后者最大的好处是 PV 创建时已经知道 Pod 落在哪台节点可以做到本地卷的“延迟绑定”避免 PV 创建在没被调度的节点上导致数据倾斜。4. 实操记录从零搭建一套 NFS 持久化存储方案4.1 为什么我选 NFS 做演示K8s 里的持久化存储概念很多如果只看文档很容易被各种 Volume 类型吓到。我做演示的时候最喜欢用 NFS原因是它架构简单——一台服务器做共享目录集群所有节点挂载访问访问模式支持最全可以实现 RWX部署排查比 Ceph 这类分布式存储好操作得多适合先理解完整链路再学习更复杂的生产方案。你不需要把 NFS 当作生产环境的唯一答案但把它作为理解 PV/PVC 的载体非常合适。开始之前先在一台节点上安装 NFS 服务端并创建一个共享目录# 在 NFS 服务器上执行 sudo apt update sudo apt install -y nfs-kernel-server nfs-common sudo mkdir -p /data/k8s sudo chown nobody:nogroup /data/k8s sudo tee -a /etc/exports EOF /data/k8s *(rw,sync,no_subtree_check,no_root_squash) EOF sudo systemctl restart nfs-kernel-server exportfs -rv然后在所有 K8s 节点安装客户端工具并测试挂载sudo apt install -y nfs-common showmount -e NFS服务器IP我这里单独设置了no_root_squash后面权限那节会专门讲这是一个非常容易踩的选项。4.2 创建静态 PV 与 PVC我先用静态供给的方式写一个 PV目的就是把第一条链路走通。假设 NFS 服务器的 IP 是 192.168.1.20apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs-001 labels: usage: elk-storage spec: capacity: storage: 5Gi volumeMode: Filesystem accessModes: - ReadWriteOnce - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs: path: /data/k8s/pv001 server: 192.168.1.20注意我把storageClassName设成了空字符串这是为了防止集群里有默认 StorageClass 时 PVC 走动态供给而不是绑到我手工创建的 PV。对应地PVC 里也必须写成storageClassName: apiVersion: v1 kind: PersistentVolumeClaim metadata: name:>kubectl apply -f pv.yaml kubectl apply -f pvc.yaml kubectl get pv,pvc正常情况下你会看到 PV 的状态从 Available 变成 BoundPVC 同时变成 Bound。这时候 PVC 就相当于拿到了一块 5Gi 的 NFS 卷等待被 Pod 消费。4.3 用 Deployment 验证数据能跨 Pod 存活接下来创建一个 Deployment用 busybox 每隔一段时间往挂载目录写一条带时间戳的日志apiVersion: apps/v1 kind: Deployment metadata: name: storage-demo spec: replicas: 1 selector: matchLabels: app: storage-demo template: metadata: labels: app: storage-demo spec: containers: - name: app image: busybox:1.36 command: [/bin/sh, -c, while true; do echo $(date) /data/access.log; sleep 30; done] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName:>kubectl exec -it pod-name -- tail -f /data/access.log然后手动删除这个 Pod等 Deployment 重新拉起来一个新的 Pod再去看/data/access.log你会发现之前写入的内容还在。这就是持久化存储和普通临时存储的核心分界线。Pod 可以死节点可以坏但数据不随容器生命周期消失。4.4 换成动态供给再跑一遍静态 PV 适合理解概念但真实项目里我更推荐动态供给。把上面的 StorageClass 部署到集群然后创建 PVC 的时候指定storageClassName: nfs-storage。创建 PVC 后不需要提前建 PV控制面会自动调用 provisioner 在 NFS 服务器上生成一个子目录再把它包装成 PV 并绑定给 PVC。kubectl apply -f sc-nfs.yaml kubectl apply -f pvc-dynamic.yaml kubectl get pvc>kubectl describe pvc pvc-name最常见的事件是no persistent volumes available for this claim and no storage class is set。翻译一下就是集群里没有满足条件的 PVPVC 也没指定 StorageClass系统不知道该从哪里冒一块存储出来。解决要分清情况如果你预期走静态供给检查 PV 是否被别的 PVC 占用检查访问模式和容量是否匹配检查storageClassName是否因为默认 StorageClass 干扰。如果你预期走动态供给检查 StorageClass 是否存在、provisioner 对应的 Pod 是否正常运行。比如 NFS provisioner 崩了PVC 就会一直 Pending。用下面几个命令快速定位kubectl get sc kubectl get pv kubectl get pods -n kube-system | grep nfs kubectl logs -n kube-system nfs-provisioner-pod还有一个容易被忽略的情况PV 是WaitForFirstConsumer模式PVC 没有 Pod 引用时也会一直 Pending。这不是故障你创建一个使用该 PVC 的 PodPV 自然就生成了。所以看到 Pending 先看绑定模式别急着删资源。5.2 PV 进入 Released为什么不能再直接复用第二号高频坑来自回收策略我在第 2.2 节提过PVC 删除后 PV 进入 Released不是 Available。如果你直接新建 PVC 想去绑原来的 PV完全绑不上。原因就是 PV 里留有claimRef它记录了上一个 PVC 的 namespace 和名字。解决办法分两步而且删除字段要谨慎先查看引用信息kubectl get pv pv-nfs-001 -o jsonpath{.spec.claimRef}如果确认不需要保留原 PVC 的绑定记录可以安全地将其置空kubectl patch pv pv-nfs-001 -p {spec:{claimRef: null}}执行后再次kubectl get pv状态会从 Released 变回 Available。但这里有一个非常重大的提醒PV 里面的数据还在原路径上重新绑定后新的 PVC 看到的可能是旧数据。如果这个问题在数据库场景里出现那会是很严重的隐患。所以在清理 claimRef 之前一定要确认旧的底层数据要么已备份、要么本来就可以丢弃。5.3 挂载成功但容器里写不进去权限问题的完整链路这个坑我踩的次数最多。NFS 挂载成功、PVC 也是 Bound容器启动后往目录里写数据却报Permission denied。常见的根因有两个。第一个是 NFS 服务端的 root squash 配置。没有配置no_root_squash时客户端以 root 身份写入会被映射成 nobody 用户如果目录属主不是 nobody就会拒绝写入。生产环境里当然不要随便开no_root_squash但要在 NFS 共享目录上把属主设置好比如sudo mkdir -p /data/k8s sudo chown nobody:nogroup /data/k8s第二个是容器以非 root 用户运行时进程 UID 和卷的属主对不上。K8s 的安全上下文里可以通过fsGroup让 K8s 把卷的组权限统一改成指定 GID然后再启动容器。示例如下spec: securityContext: fsGroup: 1000fsGroup会让卷内所有内容的 group 变成 1000并且进程只要在这个组里就能正常读写。如果我维护的是 MySQL、Elasticsearch 这类容器镜像这类权限问题几乎是入门的必经之路。排查的顺序建议是先kubectl exec进容器用id看当前 UID/GID再到 NFS 服务器看共享目录的属主最后检查/etc/exports里是否加了奇怪的 squash 参数。按这个链路走基本十分钟内能定位。6. 再往前走一步StatefulSet 里如何自动生成 PVC6.1 为什么 Deployment 挂 PVC 不够用Deployment 的管理方式是“所有副本共享同一个 Pod 模板”所以复制出来的多个 Pod 如果都挂同一个 PVC就产生了一个隐含假设这块存储可以被多副本同时读写。如果后端是 RWO 云盘一个 PVC 根本挂不上两个不同节点的 PodDeployment 直接卡住。对于数据库、消息队列这类“每个副本都必须有独立数据目录”的有状态应用正确做法是用 StatefulSet。它给每个副本编号并且可以配合volumeClaimTemplates自动为每个副本生成独立 PVC。6.2 volumeClaimTemplates 的实际用法和测试apiVersion: apps/v1 kind: StatefulSet metadata: name: stateful-demo spec: serviceName: stateful-demo replicas: 3 selector: matchLabels: app: stateful-demo template: metadata: labels: app: stateful-demo spec: containers: - name: app image: busybox:1.36 command: [/bin/sh, -c, echo hello /data/$(hostname).txt; sleep 3600] volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] storageClassName: nfs-storage resources: requests: storage: 1Gi创建后你会发现集群里多了三个 PVC>kubectl get pv,pvc -A kubectl get sc kubectl describe pvc pvc-name kubectl describe pv pv-name kubectl get events --sort-by.metadata.creationTimestamp | tail -50每次遇到疑似存储的问题这组命令都能让我快速从“现象不可见”进入“原因可见”的状态。整个 K8s 生态里存储是最不能凭感觉乱写参数的部分多用 describe 看事件多理解生命周期比背多少文档都管用。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/16 10:21:39
缠论DLL源码拆解:笔段中枢算法工程化与实盘信号落地
2026/10/16 10:21:39
STM32F429 HAL+DMA多通道ADC高速采样实战指南
2026/10/16 10:21:39
ATSHA204加密芯片实战:STM32 I2C通信、SHA-256认证与避坑指南
2026/10/16 13:37:54
在线拍卖系统架构设计:Node.js+Socket.IO+Redis高并发实践
2026/10/16 13:37:54
官方 Crates 停摆的 300 天:gpui 生态靠 fork 撑起了一片天,接下来轮到谁?
2026/10/16 13:37:54
VSG并网稳定性分析:从虚拟惯量建模到弱电网参数设计
2026/10/16 13:37:54
FCOS源码深度解析:逐像素预测如何实现anchor-free目标检测
2026/10/16 13:37:54
牛客刷题第5期复盘:动态规划、滑动窗口、二叉树递归的模板沉淀
2026/10/16 13:32:53
Spring Boot爱心商城系统毕设源码:从数据库设计到JWT鉴权实战
2026/10/16 0:04:49
HCIA-AI V3.5 模拟题库从零搭建:知识点标签、组卷算法与错题分析
2026/10/16 0:04:49
游戏项目Shader清理实战:静态扫描与动态验证双轨并行
2026/10/16 0:04:49
比特币白皮书深度拆解:从双重支付到共识机制的核心设计
2026/10/16 1:17:36
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/16 1:17:35
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/16 1:17:35
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/16 1:17:35
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/15 5:07:59
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/15 8:10:01
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)