尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
K8s持久化存储实战:PV/PVC/StorageClass与StatefulSet绑定逻辑
最近在给一个模拟项目做 StatefulSet 部署数据库的存储配置绕不开 K8s 里的 PV 和 PVC。很多刚接触 K8s 的朋友会觉得这两兄弟抽象、绕实际上只要把它们的生命周期和绑定逻辑捋顺持久化存储这块就能拿捏得比较稳。这篇我会从为什么需要这层抽象开始拆解 PV、PVC、StorageClass 之间的关系再带一组可直接套用的 YAML 实战最后把我在踩坑中整理的排查思路一并放出来。1. 为什么 K8s 需要 PV 和 PVC 这套抽象1.1 没有持久化Pod 重启等于数据归零先回想一下 K8s 里 Pod 的基本假设Pod 里的容器是“多变的、短命的”。滚动更新、节点故障、资源驱逐都会导致 Pod 被销毁或重建。如果数据直接写在容器层那 Pod 一没数据就跟着没了。即便是写在宿主机的emptyDir里Pod 被调度到另一台节点时数据也留不在新节点上。数据库、消息队列、日志系统这类有状态应用显然不能容忍这种数据与 Pod 强绑定的模式。1.2 存储与 Pod 解耦的两层设计K8s 的解法就是把“存储资源”和“存储使用请求”分开管理。存储资源由集群管理员提前准备叫做 PVPersistentVolume持久化存储卷它描述的是实际的存储后端比如某台分布式存储、某块云硬盘、NFS 共享目录等。存储使用请求由业务开发者提交叫做 PVCPersistentVolumeClaim持久化存储卷声明它只表达业务需要什么样的存储多大容量、什么访问模式、是否要求快速读取。你可以把 PV 理解成“现成的房源”PVC 是“租房需求”。业务方不需要自己去买物理设备只需要告诉 Kubernetes 我需要一个三室一厅、朝南、有电梯的房子调度器会根据需求找到匹配的 PV 并完成绑定。如果找不到符合条件的房子PVC 会一直处于 Pending 状态直到管理员补充房源或者触发动态供给。1.3 从工作量和安全性两个角度看抽象的好处很明显没有这套抽象之前每个应用团队都要自己去找存储管理员申请磁盘拿到 IP 和路径然后写进 Deployment 的配置里。存储变更时应用团队还得改配置、重新发布。这个流程慢且容易出错。PV/PVC 把“存储细节”和“业务使用”彻底隔离业务团队只需要知道 PVC 的名字和容量不需要关心底层是 Ceph 还是云盘还是 NFS。管理员则在 PV 层控制存储插件的接入和访问权限避免开发人员直接接触存储后端。安全边界更清晰升级存储后端也不需要业务侧改动。所以PV/PVC 不是给 K8s 增加复杂度而是把有状态应用真正落到 K8s 上的重要基石。理解了这一点后面所有细节都顺理成章。2. PV、PVC、StorageClass 三者如何协作2.1 静态供给与动态供给两条路径K8s 的持久化存储有两条典型路径静态供给管理员手动创建一份或多份 PV预先分配容量并指定存储后端的具体信息。开发者提交 PVC 后K8s 寻找与 PVC 匹配的 PV完成一对一绑定。这条路适合存储后端不支持动态创建卷的场景比如企业内部使用的 NFS 共享。动态供给管理员定义 StorageClass存储类PVC 里引用这个 StorageClassK8s 会调用存储插件在底层存储系统中实时创建卷然后自动生成 PV 并绑定 PVC。这条路适合云平台如云服务商提供的块存储或支持 CSI 接口的存储系统。动态供给最大优点是“按需分配”省去管理员预置 PV 的工作。2.2 PV 的关键字段和访问模式PV 的完整定义里有几个字段需要重点关注capacity声明的存储容量比如 20Gi。PVC 请求的容量必须小于等于 PV 的容量绑定才能成功。volumeModeFilesystem 或 Block默认是 Filesystem。数据库大多用 Filesystem某些高性能场景会选择 Block 模式的块设备。accessModes访问模式决定卷能被多少个节点以何种方式挂载。常用有ReadWriteOnce单个节点读写、ReadOnlyMany多个节点只读、ReadWriteMany多个节点读写。persistentVolumeReclaimPolicy回收策略包括Retain保留、Recycle已废弃、Delete删除底层存储资源。storageClassName关联的存储类名称。如果值为空则属于未绑定 StorageClass 的 PV。后端插件信息比如 NFS 的 server 和 path云盘类的磁盘 IDCSI 驱动的参数等。光看字段可能不直观我给你画一个 NFS 类型的 PV 示例这是静态供给中很常见的写法apiVersion: v1 kind: PersistentVolume metadata: name: pv-nfs-demo spec: capacity: storage: 20Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs: server: 10.0.0.10 path: /data/k8s-demo这里storageClassName: 表示这个 PV 不绑定任何 StorageClass。PVC 如果希望绑定这个 PV也需要把storageClassName设为空字符串两者才会匹配。2.3 PVC 的关键字段和绑定规则PVC 的 spec 通常包含这些内容accessModes与 PV 一致才能绑定。K8s 不会把 ReadWriteOnce 的 PVC 绑定到 ReadOnlyMany 的 PV 上。resources.requests.storage申请容量必须小于或等于 PV 容量。storageClassName如果当前环境配置了默认 StorageClass可以省略但显式声明更稳妥。selector通过 label 选择器匹配指定 PV。如果你的 PVC 只想绑定某几个 PV可以给 PV 打 label然后在 PVC 里用 matchLabels 筛选。PVC 提交后K8s 会触发控制循环来寻找匹配的 PV 并将两者绑定。绑定成功后PVC 的status.phase会变成 Bound注明的 volumeName 就是绑定的 PV 名称。如果找不到匹配的 PVPVC 会一直处于 Pending并且事件里会有waiting for a volume to be created, either by external provisioner or manually这类提示。2.4 StorageClass 如何介入动态供给StorageClass 的定义包含了卷的供应者、参数和回收策略。云服务商的存储插件通常会在 StorageClass 的 provisioner 字段指定比如磁盘、分布式文件存储。参数则可以设置类型、性能档位、是否加密等。下面是一个简单的 StorageClass 示例以某个内部模拟的 CSI 驱动为例实际参数取决于后端系统apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: csi.example.com/block-storage parameters: type: ssd sizeRange: 10-100Gi reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer注意volumeBindingMode这个字段之前默认是Immediate表示 PVC 创建后立刻动态创建卷并绑定 PV。但假如后端存储被绑定在某个具体节点例如本地盘场景PVC 刚创建、Pod 还没被调度时控制器根本不知道卷该创建在哪台节点。这时候就要用WaitForFirstConsumer等 Pod 调度完成后再根据 Pod 所处的节点去动态创建卷。数据库这类依赖节点本地化存储的场景这个参数非常重要。2.5 生命周期里的四种事件供应、绑定、使用、回收供应静态路径下管理员创建 PV动态路径下 StorageClass 调用插件创建卷并生成 PV。绑定PVC 和 PV 之间建立一对一关系。一个 PV 只能绑定一个 PVC一旦绑定其他 PVC 无法再使用。使用Pod 在 volumes 中通过 PVC 名称引用卷K8s 将 PV 挂载到容器的某个路径。回收删除 PVC 后PV 会根据 reclaimPolicy 执行后续动作。Retain 表示 PV 被保留但状态变为 Released需要管理员手动清理并重新创建Delete 表示 PV 和底层卷会被一并删除。清楚这套生命周期排查问题才能从“现象”定位到“阶段”。3. 实操从 NFS 静态供给到动态供给落库3.1 搭建一个最小可用的 NFS 存储模拟环境生产环境通常会有独立存储团队提供 NFS 或对象存储但测试环境里我们可以快速用一个节点上的目录模拟 NFS。以常见 Linux 环境为例安装 NFS 服务端并导出/data/k8s-demo目录# 安装并启动 NFS 服务 yum install -y nfs-utils mkdir -p /data/k8s-demo chown nobody:nobody /data/k8s-demo echo /data/k8s-demo *(rw,sync,no_root_squash) /etc/exports systemctl enable --now nfs-server exportfs -avK8s 的每个工作中节点也需要安装 nfs-utils否则拉取 NFS 类型的 PV 时会提示mount: bad option。这一点容易漏很多朋友 PV 创建了、PVC 也绑定了但 Pod 起不来一查 Kubelet 日志才发现 Node 上没装 NFS 客户端。3.2 实战静态供给创建 PV、PVC、Deployment 全流程第一步创建 PV。这里我使用上面的pv-nfs-demo.yaml注意storageClassName留空表示与企业默认 StorageClass 无关要求 PVC 也留空。kubectl apply -f pv-nfs-demo.yaml kubectl get pv这时候 PV 的 STATUS 应该是 Available因为还没有 PVC 绑定。第二步创建 PVC。PVC 的storageClassName同样留空apiVersion: v1 kind: PersistentVolumeClaim metadata: name: myapp-data spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi storageClassName: 接着 apply 并检查状态kubectl apply -f pvc.yaml kubectl get pvc正常情况下PVC 会很快变成 BoundK8s 会将myapp-data绑定到pv-nfs-demo。这里的容量匹配逻辑是“5Gi 请求小于等于 20Gi 容量”accessModes 都是 ReadWriteManystorageClassName 都为空所以匹配成功。第三步在 Deployment 中引用 PVC。写一个简单的应用挂载到/usr/share/nginx/html下的目录apiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 1 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: containers: - name: nginx image: nginx:1.25 volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data persistentVolumeClaim: claimName: myapp-dataapply 后进入容器随便写一个文件然后删除 Pod 再重建你会发现文件还在。这就是持久化存储的意义。3.3 动态供给StorageClass PVC 自动建卷动态供给的前提是集群已经安装了 CSI 驱动或云厂商的存储插件。我这里用一个常见的模拟 CSI 驱动来演示实际项目中替换成对应的 provisioner 即可。创建 StorageClassapiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: dynamic-fast provisioner: csi.example.com/block-storage parameters: type: gp3 reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer allowVolumeExpansion: true然后创建 PVC引用这个 StorageClassapiVersion: v1 kind: PersistentVolumeClaim metadata: name: myapp-dynamic-data spec: accessModes: - ReadWriteOnce storageClassName: dynamic-fast resources: requests: storage: 10Gi提交后由于volumeBindingMode是WaitForFirstConsumerPVC 可能会先处于 Pending。等 Deployment 或其他 Pod 引用这个 PVC 并被调度到某个节点后存储插件才真正创建卷。这中间如果一直不创建 PodPVC 会一直是 Pending别以为出故障了。我见过不少新手在这个环节反复删掉重建其实只要把引用 PVC 的 Deployment 跑起来PVC 就会自动转入 Bound。动态供给的好处是不用手动管理 PV。PVC 删除后如果 reclaimPolicy 是 Delete底层存储卷会一并清理不会留下不可见的孤岛存储资源。3.4 扩容与快照后续可能用到的两个场景AllowVolumeExpansion 开启后容量可以动态扩容。扩容步骤很简单修改 PVC 的请求容量并 applykubectl edit pvc myapp-dynamic-data # 修改 storage: 10Gi 为 20Gi但前提是底层存储驱动支持扩容修改之后 PVC 的status.capacity会逐步更新。期间建议不要写大量数据因为部分后端在扩容过程中会执行挂起或文件系统扩展操作。快照则是通过 VolumeSnapshot 资源实现的。PVC 是状态数据的载体快照是它的备份点。日常做数据库备份时可以在低峰期创建一个 VolumeSnapshot然后基于快照恢复出新的 PVC。由于每家 CSI 驱动的快照能力差异很大这里不展开写具体参数等你们用到备份恢复时再深入排查。4. 状态检查、常见问题与踩坑经验4.1 用 kubectl 快速判断 PV/PVC 健康度的几个命令检查集群里的 PV 状态kubectl get pv kubectl describe pv pv-name检查 PVC 状态kubectl get pvc -A kubectl describe pvc pvc-name -n namespace查看 StorageClasskubectl get sc kubectl describe sc sc-namedescribe 的输出通常会把重要事件列出来比如FailedBinding、Provisioning、VolumeFailedMount等。我排查问题时的标准流程是先看 PVC 的 phase 和 events再顺着事件里提到的 PV 名称去看 PV 的 status最后看 Pod 的事件和 kubelet 日志。如果事件里没有具体原因还要查看 provisioner 或 CSI controller 的日志尤其是动态供给场景。4.2 PVC 一直 Pending但 PV 明明存在这个问题集中在静态供给场景。可能的原因有PVC 的容量超过 PV 容量。例如 PV 是 20GiPVC 请求 50Gi不匹配。访问模式不匹配。PV 是 ReadWriteManyPVC 是 ReadWriteOnce除非 PV 本来就支持 ReadWriteOnce否则绑定不上。storageClassName 不匹配。PV 的 storageClassName 是PVC 的 storageClassName 是standard两边对不上。PV 已经 Bound 给另一个 PVC。一个 PV 不能同时绑定多个 PVC查看kubectl get pv里 STATUS 是否为 Bound。selector 不匹配。PVC 指定了 selector但 PV 的 label 不满足条件。实际操作中最常见的是 storageClassName 不一致。很多公共教程在旧版本里不写这个字段但在配置了默认 StorageClass 的集群中PVC 不写会使用默认 StorageClass而 PV 的 storageClassName 可能是空最终匹配失败。建议在 PV 和 PVC 中明确把 storageClassName 写成相同的值不要依赖隐式默认。4.3 Pod 一直 ContainerCreating卷挂载失败PV 和 PVC 虽然是 Bound但 Pod 无法启动。原因大多数出在“底层存储不可用”或“节点缺少客户端支持”NFS 类型工作节点没有安装 nfs-utils或者 NFS 服务端防火墙未放行端口挂载时报mount: wrong fs type。CephFS/云盘节点缺少对应驱动或者存储侧限制了访问 IP。设备路径冲突某些云盘驱动要求 PV 名称、磁盘 ID 全局唯一重复使用时会被拒绝。权限问题NFS 导出的目录权限不对容器进程无法读写导致应用启动时报 permission denied。排查这类问题通常要看kubectl describe pod的 Events里面会显示挂载报错的具体信息。如果 Events 不够详细进入节点查看 kubelet 日志日志位置因发行版而异一般是/var/log/messages或/var/log/syslog。4.4 PV 删除后状态一直是 Released静态供给并且 reclaimPolicy 设置为 Retain 时PVC 删除后 PV 会进入 Released 状态。Released 意味着 PV 已经释放了绑定但底层存储数据仍然保存着等待管理员处理。管理员需要先清理或备份底层存储数据然后删除 PV再重新创建同名的 PV才能让它回到 Available。这里容易踩的坑是直接删除 Released 状态的 PV 会提示成功但如果直接重新创建一个同名 PV而底层数据目录还保留着上一轮的数据新的 PVC 绑定后立刻会看到旧数据这在生产环境中可能导致数据泄漏或错误初始化。规范做法是清理底层存储资源后再重新创建 PV。4.5 误删 PVC 导致数据被连带删除动态供给下如果 StorageClass 的 reclaimPolicy 是 Delete那么删除 PVC 时底层卷会被一起删除数据就没了。很多同学以为 PVC 删除只是断开绑定并不会影响数据实际上云盘的 Delete 策略是真删除。为了避免这种事故关键业务所在的 StorageClass 建议改成 Retain。但这会带来另一个问题动态供给删除 PVC 后PV 会保留并处于 Released管理员需要手动处理底层卷。批量测试环境可以用 Delete核心生产库建议 Retain或者做好定时快照。我在一个模拟项目里遇到过 PVC 被误删、整库数据直接消失的情况从快照恢复又花了不少时间。后来规范流程是所有和生产数据相关的 StorageClass 必须显式设置 Retain并且定期做 VolumeSnapshot。4.6 多节点读写时提示锁冲突或数据不一致选择 ReadWriteMany 只是让多个节点可以同时挂载但存储后端和文件系统的语义决定它是否适合并发写。NFS 支持多节点读写但应用层如果用了某些默认的锁机制也可能出现锁冲突尤其是数据库场景。真正生产级的数据库通常建议单节点读写通过主从架构实现高可用而不是让多个节点同时写同一份 PV。使用块存储加文件系统确保 I/O 性能和一致性。使用 CSI 快照做主备切换时的数据保护。如果你一定要多个 Pod 共享同一份 PV建议使用对象存储或支持并发语义的分布式文件存储不要用普通块设备。4.7 如何从 Bound 退订并重新分配 PV你可能会需要在业务下线后把 PV 释放出来给别的 PVC 用。对于 Retain 策略先删除 PVCPV 会变成 Released手动清理底层数据删除旧 PV重新创建新 PV状态恢复 Available。对于 Delete 策略删除 PVC 后底层卷直接删除再创建新的 PVC 会自动动态供给新 PV。值得一提的还有 PV 的“重新绑定”场景动态供给下如果删除 PVC 但底层卷由于 reclaimPolicy 是 Retain 而被保留管理员想恢复这个卷可以手动创建一个 PV 指向原来的 volume handle但必须确保 PV 的所有字段与原卷一致否则挂载时容易出错。5. StatefulSet 与 PV/PVC 配合使用的几个细节5.1 volumeClaimTemplates 自动生成 PVCStatefulSet 里最常用的持久化配置就是 volumeClaimTemplates。它定义了创建 Pod 时自动生成 PVC 的模板每个副本都能拿到一个独立的 PVCPVC 名称会带上 Pod 的序号。apiVersion: apps/v1 kind: StatefulSet metadata: name: db spec: serviceName: db replicas: 3 selector: matchLabels: app: db template: metadata: labels: app: db spec: containers: - name: db image: mysql:8.0 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce storageClassName: fast-ssd resources: requests: storage: 20GiStatefulSet 控制器创建 Pod 的顺序是 db-0、db-1、db-2。对应的 PVC 也会自动生成>apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: db-snapshot-001 spec: volumeSnapshotClassName: csi-snapclass source: persistentVolumeClaimName:>apiVersion: v1 kind: PersistentVolumeClaim metadata: name:>
RELATED

相关推荐

PCA9422可编程PMIC与TM4C1299上电时序设计实战

PCA9422可编程PMIC与TM4C1299上电时序设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/10 2:34:18
NYU-DLSP20 课程笔记:基于能量的结构化预测——因子图、高效推理与图变换网络(Graph Transformer Net)

NYU-DLSP20 课程笔记:基于能量的结构化预测——因子图、高效推理与图变换网络(Graph Transformer Net)

示例工程 【免费下载链接】NYU-DLSP20 NYU Deep Learning Spring 2020 项目地址: https://gitcode.com/gh_mirrors/pyt/pytorch-Deep-Learning 点击查看 免费下载 本文基于 NYU Deep Learning Spring 2020(NYU-DLSP20)第 14 周理论课 Part A…

📅 2026/10/10 2:34:18
BFE mod_doh 模块配置指南:mod_doh.conf 全参数详解与 DoH 请求转发实现

BFE mod_doh 模块配置指南:mod_doh.conf 全参数详解与 DoH 请求转发实现

后端网络/通信云原生 【免费下载链接】bfe A modern layer 7 load balancer from baidu 项目地址: https://gitcode.com/gh_mirrors/bf/bfe 点击查看 免费下载 mod_doh 是 BFE(Baidu Front End,现代七层负载均衡器)内置的 DoH&am…

📅 2026/10/10 2:34:18
MORE NEWS

更多资讯

📰

SpringBoot+Vue私人诊所管理系统:协同过滤推荐算法实战解析

这两年我陆陆续续帮几个做基层医疗系统的朋友看过代码,也做过一些私人诊所的信息化改造,发现一个挺有意思的现象:很多诊所老板以为管理系统就是“记个账、排个班”,但真正用了半年之后,最让他们离不开的反而是“推荐”…

📰

MagPie模型路由工具:Agent多模型统一管理与自动分发实践

这个项目叫 MagPie,本质上是一个 Agent 模型路由工具。它的核心思路不是再训练一个多大的模型,而是把市面上已有的各种模型能力统一管起来,根据任务类型自动选择最合适的模型去处理。对于经常在 Agent、工作流、自动化脚本里反复切换模型的人…

📰

用Shell脚本实现轻量级基础设施即代码(IaC)实践

做了这么多年运维,我一直觉得“基础设施即代码”这件事,不应该只有大厂那套玩法。很多小团队、轻量项目,根本不需要立刻上Terraform、Ansible这些重型工具,直接用Shell脚本也能把IaC做得明明白白。这次分享的这套实践,…

📰

本地AI项目部署实战:环境准备、API接口与批量任务全流程解析

高效启动本地 AI 项目:从环境准备到接口联调的一次完整实测打开这篇文章的读者,大概率不是来看概念介绍的,而是想知道三件事:这个项目怎么跑起来、跑起来之后能干什么、遇到问题怎么排查。这次我们就围绕一个本地 AI 工具类项目的…

📰

OpenHarmony真机Flutter应用错误处理与异常管理实战指南

在OpenHarmony真机上跑Flutter,最磨人的不是写页面,而是排错。原因很简单:你在模拟器里跑得好好的逻辑,一旦上了真机,摄像头权限、传感器驱动、系统省电策略、通知开关,任何一环出问题,整个App就…

📰

人类阅读与大语言模型如何应对概念中断和指称中断?

这次我们来看一个研究性项目:Distinct dynamics of conceptual and referential disruptions in human reading and large language model processing,翻译过来是“人类阅读与大语言模型处理中概念与指称中断的不同动态”。它不是一个可以一键部署的模型…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬