首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
K8s持久化存储实战:StatefulSet与PV/PVC/StorageClass原理与操作
📅 2026/10/14 14:52:36
✍️ 爱科研究院
👁 阅读 3,247
一直在折腾 Kubernetes 的人多半都会在持久存储这块栽过跟头。我遇到过最憋屈的一次一个有状态的服务跑得好好的节点忽然 NotReady等 Pod 被调度到另一台机器后数据全不见了。当时第一反应是怀疑服务镜像出了问题查了半天才发现问题出在 volumes 配置上——挂的是 emptyDirPod 一重建整个文件系统就跟着消失了。后来我花了一整个周末把持久存储 PersistentVolume 这条线彻底捋了一遍从 PV、PVC、StorageClass 到 StatefulSet 全部串起来才终于搞明白 K8s 里「存储」到底是怎么运作的。这篇是这个 Kubernetes 系列笔记的第 14 篇重点讲清楚两件事持久化存储的原理以及如何配合 StatefulSet 把有状态服务稳稳地跑起来。目标是让你看完以后删 Pod 不再心惊胆战。1. 有状态服务遇上 K8s数据跟着 Pod 走就是最大的坑1.1 Pod 天生是“临时工”重启、漂移与数据丢失的关系要理解 PersistentVolume先得理解 Pod 的宿命。一个 Pod 在 K8s 里从来不是“永久居民”它本质上是运行在任意工作节点上的几个容器组合随时可能因为这些原因消失节点重启、关机、被驱逐Pod 被重新调度到别的机器Deployment / StatefulSet 滚动更新旧 Pod 被替换内存或磁盘压力导致节点驱逐Pod 被强制终止人为误删或者 HPA 缩容把副本数降下来。每次被重新调度意味着旧 Pod 会被删除然后在一个可能是完全不同的节点上新建一个 Pod。你如果只是像传统服务器一样把数据写在容器内部或者挂在普通临时卷上那数据就跟着 Pod 一起“火化”了。这里有个容易混淆的细节如果只是容器崩溃被 Kubelet 自动重启因为 Pod 对象还活着emptyDir 的数据其实不会丢真正致命的是 Pod 被删除后重建。一旦 Pod 对象不存在了挂载在 Pod 生命周期内的临时卷也会被清理掉。所以有状态服务的第一条生存法则就是数据不能放在 Pod 文件系统内也不能放在 Pod 生命周期相关的临时卷里。它必须放在一个独立于 Pod 之外的“外部资产”上Pod 只是这台资产的租客租客随便换房子和里面的家具得留着。1.2 网络身份也会变为什么有状态服务需要“三稳”存储问题之外还有一个同样烦人的问题IP 和主机名会变。无状态服务无所谓客户端通过 Service 负载均衡访问实例换了一百遍也不影响但像数据库、消息队列这类服务客户端往往需要稳定地址副本之间也需要知道彼此是谁。我曾经试过用一个普通 Deployment 部署单机数据库数据问题解决以后又碰到了客户端连接串里的地址失效问题。Pod 每次重建 IP 都不一样连接池里的老地址全废了。这引出有状态服务在 K8s 里必须满足的“三个稳定”稳定的网络标识Pod 名字固定、DNS 名固定即使重建也不变稳定的存储标识每个副本拥有独立的持久卷卷和副本一一对应副本没了卷还在稳定的实例顺序比如第 0 个节点必须永远叫 0不能随机改名。第一点由 StatefulSet 的稳定 Pod 名和 Headless Service 解决第二点就是我们这篇文章的主角——PersistentVolume 体系以及 StatefulSet 里的 volumeClaimTemplates 机制。1.3 为什么不直接在宿主机器上挂个本地目录有同学会问我在 Pod 的 volumeMounts 里挂宿主机的某个固定目录不就行了数据存在宿主机磁盘上Pod 重建后挂同一个目录不也能拿到数据吗这就是典型的“单机思维”移植到分布式集群时会踩的坑。hostPath 卷确实能把宿主机目录挂进 Pod但它有非常明显的局限Pod 被重新调度到另一台节点时新节点上根本没有那个目录就算你给节点打标签、用 nodeSelector 强制绑回原节点一旦原节点彻底坏了数据还是跟着坏磁盘一起完蛋多副本同时挂同一个 hostPath 目录谁先写谁后写完全不可控数据互相覆盖的风险太高hostPath 没有容量配额的概念Pod 可以把宿主机的磁盘写满。所以 K8s 才搞出一整套抽象让存储成为与节点、与 Pod 生命周期解耦的独立资源。这就是 PersistentVolume 体系存在的根本原因。2. 组合拳拆解PV、PVC、StorageClass 各自管什么2.1 PV 是资产PVC 是需求单一次搞懂两者的关系很多新手上来就背概念背完就忘。我的理解方式比较简单用生活里的房子来类比PersistentVolumePV是集群里的一套房是真实的存储资产。它可以来自云上的云盘、机房里的 SAN/NAS、本地的磁盘不管底层是什么PV 把它包装成一个统一的“卷”对象告诉集群我这里有一套 20 GiB、支持 ReadWriteOnce 的房子。PersistentVolumeClaimPVC是你提交给集群的租房需求单“我需要一套 20 GiB、能读写的房子最好是快速 SSD 那类。”你不需要关心房子具体在哪也不需要关心房东是谁你只写需求。StorageClassSC是中介。它知道怎么按需求单去调用底层存储厂商的接口当场给你造出一套符合要求的房子动态供给如果没有 SC你得手动把现成的房子登记成 PV静态供给。所以整体流程是用户创建 PVC → K8s 找匹配的 PV → 绑定 → Pod 的 volume 字段引用这个 PVC → 挂载进容器 → 容器读写数据。关键认知PV 是集群级别的资源不属于任何命名空间PVC 是命名空间级别的资源。一个 PV 一旦被某一个 PVC 绑定这个 PV 在释放之前就不能再绑定别的 PVC 了。2.2 绑定的逻辑与生命周期五阶段K8s 找一个 PV 来绑定 PVC不是随便撞运气它会按这几个维度匹配capacityPV 的容量要大于等于 PVC 申请的容量accessModesPV 支持的访问模式必须包含 PVC 请求的模式storageClassName如果 PVC 指定了 SCPV 的 SC 必须一致volumeMode文件系统类型还是裸块设备二者不能混用。匹配成功之后PV 和 PVC 的关系就进入了绑定状态。你可以通过kubectl get pv看到 PV 的状态它主要有五个阶段状态含义你能做什么AvailablePV 还没被 PVC 绑定空闲可分配创建 PVC 时可能被选中Bound已有 PVC 与之绑定Pod 可以通过 PVC 使用它ReleasedPVC 被删了但底层存储还没回收数据可能还在需要人工处理Failed自动回收失败需要排查底层存储问题Pending不是 PV 状态但 PVC 常见动态供给还没完成或还没找到匹配的 PV这里值得特别注意 Released。我见过不少同学以为删掉 PVC 数据就自动清除了其实不一定。如果 PV 的 reclaimPolicy 是 RetainPVC 删除后 PV 会变成 Released底层数据依然保留运维人员可以手动处理。用得好是救命稻草用不好就是成本黑洞——云盘还在计费呢。2.3 StorageClass 动态供给让“按需分配”成为默认操作早期 K8s 版本里集群管理员必须先手动创建一批 PV用户才能去申请。这种做法在云环境里很不现实谁也不知道用户到底要多大、多快、什么类型的存储。于是 StorageClass 和动态供给出现了。举个例子一个最简单的 StorageClass 定义长这样apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-demo provisioner: example.com/local-path reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumerprovisioner告诉 K8s 用哪个存储插件来创建 PVreclaimPolicy动态创建的 PV 被释放后底层卷怎么处理volumeBindingMode立即绑定还是等第一个 Pod 调度后再绑定。创建 PVC 时只要在 spec.storageClassName 里填local-demo存储插件就会自动调用底层接口创建一块真实存储并生成一个 PV 与 PVC 绑定。整个过程自动化程度很高用户完全不感知底层实现。我的建议很简单能够动态供给就不要走静态供给。静态 PV 适合一些特殊的、一次性的、管理员手工生产的存储资源日常有状态服务一律走 StorageClass PVC省心也可靠。2.4 accessModes 与回收策略选错会让你半夜爬起来accessModes 描述的是卷能够被哪些节点以什么方式挂载它有三种ReadWriteOnceRWO卷只能被单个节点以读写方式挂载ReadOnlyManyROX卷可以被多个节点同时以只读方式挂载ReadWriteManyRWX卷可以被多个节点同时以读写方式挂载。选型时最容易踩的坑是把 accessModes 当成“权限控制”来理解。它不是权限校验而是卷自身能力的描述。有些存储后端比如一些网络盘底层根本不支持 RWX你就算写 ReadWriteMany 也不会成功。而 RWO 也不代表“网络安全”它只保证同一时刻只有一个节点能挂载多个 Pod 如果被调度到同一个节点上依然可以同时读写同一个 RWO 卷。回收策略上生产环境要谨慎选择策略删除 PVC 后的行为适用场景RetainPV 变 Released底层数据保留需人工处理数据重要误删后可恢复Delete底层存储直接被释放通常意味着数据销毁测试环境、可重建数据我个人给新项目的默认建议是测试环境用 Delete方便清理生产环境用 Retain宁可多存一点废弃卷也不要一时手滑把数据彻底抹掉。3. StatefulSet 的存储契约副本的身份就是数据的门牌号3.1 为什么 Deployment 管不了有状态服务Deployment 的设计目标是“无状态的多副本”。它管理的 Pod 名字是随机生成的比如web-5c78d6f6b7-xyz12扩容缩容可以任意并行进行。这种设计假设是每个 Pod 都等价调走哪个都行数据要么不存在要么存在共享集群组件里。但有状态服务恰恰相反数据库主从谁主谁从是有顺序的分片集群里每个分片有自己的数据消息队列里每个分区的落盘路径不能乱。这时候你就需要 StatefulSet。StatefulSet 给每个副本一个“固定编号”的 Pod 名从 0 开始递增app-0、app-1、app-2。这个编号不只是名字它就是副本身份证。更重要的是StatefulSet 可以通过 volumeClaimTemplates 让每个副本自动拥有一块独立的持久存储。3.2 稳定标识 存储绑定Pod 名和 PVC 名的对应关系这里有一个我当初觉得“哇原来如此”的点StatefulSet 创建的 PVC名字是这样拼接的volumeClaimTemplate 的名字-StatefulSet 的名字-序号假设你定义了一个叫data的 volumeClaimTemplateStatefulSet 叫app那么Podapp-0对应的 PVC 叫>spec: serviceName: app-headless replicas: 2 volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] storageClassName: local-demo resources: requests: storage: 1Gi template: spec: containers: - name: app image: busybox:1.36 volumeMounts: - name: data mountPath: /dataStatefulSet 控制器创建 Pod 前会先检查对应 Pod 的 PVC 是否存在。如果 PVC>kind create cluster --name pv-demo kubectl cluster-info如果你本机没有 kind用 minikube 也完全可以。接下来我们需要创建一个支持动态供给的 StorageClass。生产环境里这一步通常由云厂商或存储管理员提前准备本地实验就可以用一个“本地路径”的 provisioner 来模拟。创建 StorageClassapiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-demo provisioner: example.com/local-path reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumerWaitForFirstConsumer是这里的关键PVC 创建后不会立刻绑定 PV而是等第一个 Pod 被调度到某个节点后才在那个节点上创建 PV。这对于本地路径这种节点相关性很强的存储来说非常必要。4.2 编写 StatefulSet 配置从模板到验证的完整链路为了避开任何业务软件的干扰我习惯用一个最简单的自定义“占位容器”来演示持久化原理容器启动时往/data/instance.txt写入一条带时间戳的话然后一直在后台 sleep。我们只要看这条记录在 Pod 重建后还在不在就说明存储持久化是否生效了。下面是完整的 StatefulSetapiVersion: apps/v1 kind: StatefulSet metadata: name: app spec: serviceName: app-headless replicas: 2 volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] storageClassName: local-demo resources: requests: storage: 1Gi template: metadata: labels: app: demo-app spec: containers: - name: writer image: busybox:1.36 command: - /bin/sh - -c - | if [ ! -f /data/instance.txt ]; then echo created at $(date) on $(hostname) /data/instance.txt fi sleep 3600 volumeMounts: - name: data mountPath: /data代码逻辑不复杂如果/data/instance.txt不存在就写入创建时间和主机名如果存在就什么都不做。这样一个 Pod 无论被重建多少次只要 PVC 数据还在文件里的内容就保持不变。4.3 创建与观察Pod 顺序启动PVC 自动生成把上面的 YAML 保存为app.yaml然后应用kubectl apply -f app.yaml观察 Pod 启动顺序kubectl get pods -w你会看到app-0先进入 Running之后app-1才创建。StatefulSet 的有序创建策略就是这样前者没有 Ready 之前后者不会开始。再查看 PVCkubectl get pvc输出里应该能看到>kubectl exec app-0 -- cat /data/instance.txt你会看到类似created at ... on app-0的内容。4.4 核心验证删除 Pod 后数据依然存在接下来做最“惊险”的验证。直接删掉app-0kubectl delete pod app-0因为 StatefulSet 会保证副本数所以很快会有一个全新的app-0Pod 被创建。等它 Running 后再执行kubectl exec app-0 -- cat /data/instance.txt你会发现文件里的内容还是原来的那行文字。Pod 的 UID 已经变了容器也换了新的一生但数据还在。这里面的链条很清楚Pod 删了 → StatefulSet 重建同名的 Pod → 同名 Pod 绑定了同名的 PVC → PVC 后面对应的 PV 和底层存储没动 → 数据完完整整。这就是 StatefulSet 配合 PersistentVolume 解决有状态服务持久化问题的核心机制。4.5 再验证一步PVC 被删数据才真正消失为了加深理解我们再做个对照实验。这一次手动删除 PVCkubectl delete pvc>kubectl delete pod app-0重建完成后进入 Pod 查看文件kubectl exec app-0 -- cat /data/instance.txt你会发现文件不存在了数据变成了初始化的那一条新记录。这个现象告诉我们只要 PVC 在数据就在PVC 没了之前的数据基本也就没了。所以任何生产环境里的备份、快照、容灾方案都要基于“PVC 也是可以被误删的”这个前提去设计。5. 真实项目中反复踩过的暗坑从扩容到回收策略5.1 PVC 扩容先看看 StorageClass 有没有开启开关PVC 创建之后存储空间不够了常规思路是改 capacity 字段。但这有一个前置条件StorageClass 必须声明allowVolumeExpansion: true。没开这个开关的话你改了 PVC 容量也只会卡住或者被 API 拒绝。开了之后扩容的生效方式也和底层存储有关。部分存储支持在线扩容PVC 状态会自动进入FileSystemResizePending随后文件系统扩容有些存储需要重新挂载才生效。我在实际测试中就遇到过PVC 从 1Gi 改成 2Gi 之后状态一直没变化排查发现用的是不支持扩容的本地路径插件最后只能重建存储。另外注意一点PVC 容量只允许调大不允许调小。这个限制是 Kubernetes 机制层面的相当于防呆设计——任何存储系统对缩容数据迁移都没有做到足够安全干脆直接禁止。5.2 Retain 策略下删除 PVC数据其实还在测试环境用 Delete 策略图省事但生产环境很多人会用 Retain。Retain 会带来一个很有意思的运维场景PVC 被删后PV 变成 Released底层数据还在但这个 PV 不能直接被新的 PVC 绑定。如果你想恢复这组数据大致流程是删除旧的 PVC如果还在先删掉把 PV 的 status 清一下通常修改 PV 里的claimRef让它重新变成 Available重新创建 PVC指定和这个 PV 相同的 storageClassName、容量和 accessModes确认 PV 自动与 PVC 绑定。这个流程看着简单实际操作里因为 PV 的 claimRef、uid 等字段处理不对经常半天绑不上。我的切身建议是生产环境不要依赖“Retain 之后手动恢复”这种操作把它当成最后一条保险绳就好正常的数据恢复路径应该是备份系统。另外还有一个成本问题Retain 策略下PVC 删了你以为数据删了实际上云端卷还在持续计费直到管理员手动把底层卷释放掉。所以要么定期巡检 Released 状态的 PV要么就明确这个策略只用于少量核心数据。5.3 多副本共享同一个 PVC 的并发写坑StatefulSet 的默认形态是每个副本一个 PVC数据相互隔离。但有些人为了图省事会把同一个 PVC 挂到多个 Pod 里这种操作并不是不行而是容错率很低。如果同一个 PVC 被多个节点上的 Pod 同时挂载读写必须使用支持 RWX 的存储并且底层要保证一致性。普通的本地路径卷、一些只支持 RWO 的云盘都不行。即使存储支持 RWX多个副本同时读写同一个目录如果没有应用层的锁机制还是会出现文件互相覆盖的问题而且这种问题很难排查因为它不是必现是并发下概率性出现。如果你想部署一个数据库集群正确的做法应该是每个副本通过 StatefulSet 挂独立的 PVC副本之间的数据同步交给应用自己实现比如主从复制、分片协议而不是指望所有副本共享一堆文件。5.4 本地卷的“调度陷阱”Pod 漂移过去数据却漂不过去前文实验里的 local-demo StorageClass 只是演示原理用的它代表的是节点本地盘。这类存储有一个致命的绑定问题PV 是在某个具体节点上创建的Pod 被调度到其他节点后新节点上没有这个 PV 对应的本地目录Pod 就起不来或者拿到一个空卷。我自己就翻过车某次模拟节点维护把工作负载从一台机器驱逐到另一台机器结果有状态服务的 Pod 在目标节点上卡了很久原因就是本地路径卷和节点强绑定PVC 无法在别的节点上找到数据。所以请务必记住单节点实验、开发环境可以用本地卷方便多节点生产环境请使用真正的网络存储方案比如分布式存储、云盘挂载到多节点的方案等保证卷不绑定在单个节点上。如果项目确实受限于条件只能用在本地盘那也要给 StatefulSet 配置节点亲和性让有状态副本始终调度到原节点并且做好单节点故障的应急预案。5.5 StatefulSet 删了不等于数据删了手动清理流程最后讲一个最容易被忽略的运维习惯。有很多人以为kubectl delete sts app就算把应用和数据一起删掉了实际情况是 PVC 会保留底层数据也继续存在。要彻底清理顺序应该是# 1. 先删 StatefulSetPod 随之消失但 PVC 还在 kubectl delete sts app # 2. 确认所有 Pod 终止后再删 PVC kubectl delete pvc>
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/14 14:52:36
Python数据自动处理全流程实战:从采集、清洗到报表无人值守
2026/10/14 14:47:34
从失败吸蜂器到AI智能体:基于Ollama与Flask的害虫巢穴识别实战
2026/10/14 14:47:34
GenAI机器人安全框架Safeworld:从物理伤害到心理信任的工程拆解
2026/10/14 15:49:13
单细胞转录组数据整合:从批次效应校正到多工具选型与参数调优实战
2026/10/14 15:49:13
可穿戴传感器时序数据增强的5种物理可信方法
2026/10/14 15:49:13
LightGBM-MATLAB接口编译与调用全指南
2026/10/14 15:49:13
openrig 装配指南:Claude Code 与 Codex 本地环境搭建、tmux 会话托管及本地模型接入
2026/10/14 15:49:13
项目范围管理核心框架与WBS分解实战
2026/10/14 15:44:07
Unity Projector阴影投射:原理、配置与性能优化实战
2026/10/14 0:02:01
Oracle 19c Windows客户端安装配置与连接排错指南
2026/10/14 0:02:01
React Native截图优化:解决Android兼容性问题
2026/10/14 0:02:01
金属锈蚀速率计算模型与防护方案优化实践
2026/10/13 12:25:09
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/13 18:15:04
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/14 14:52:59
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/12 19:53:23
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/11 21:41:11
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/11 23:43:10
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)