7872 字
39 分钟
第 11 篇:Kubernetes Volume、PV、PVC 与 StatefulSet

第 11 篇:Kubernetes Volume、PV、PVC 与 StatefulSet#

0. 本篇定位#

这是容器技术路线的第 11 篇。上一篇讨论 ConfigMap 与 Secret,把“配置和密钥不要写死在镜像里”这件事讲清楚;本篇继续往生产运行层推进,聚焦 Kubernetes 里的 Volume、PersistentVolume、PersistentVolumeClaim、StorageClass、StatefulSet,也就是后端服务一旦开始写文件、依赖磁盘、保存状态时必须面对的存储边界。

本文不把目标放在背 YAML 字段上,而是建立一条判断链:容器文件系统为什么不可靠,Volume 解决哪一层问题,PV/PVC 为什么要拆成资源和声明,StorageClass 如何把存储供给标准化,StatefulSet 为什么能给有状态副本稳定身份,以及排障时应该怎样沿着事件、对象状态、调度、挂载和应用日志逐层收集证据。

读完后,你应该能回答三个工程问题:普通 Spring Boot API 是否需要 StatefulSet;文件上传、报表导出、缓存目录和数据库数据目录分别应该放在哪里;当 PVC Pending、Pod 挂载失败、数据重建后丢失或多副本写坏数据时,应该先查哪一层证据,而不是凭经验重启服务。

1. 问题背景#

后端容器化最容易被误解成“把应用放进镜像,再换个启动命令”。这个理解对无状态 HTTP API 还能勉强跑通,但一旦服务需要写文件、保留会话、生成报表、存储上传附件、运行队列消费者或部署数据库,问题就会立刻暴露:容器文件系统不是业务数据的可靠归宿,Pod 也不是一台固定机器。

典型故障通常长这样:上传文件写进了容器目录,Pod 被驱逐后文件消失;把本地临时缓存误认为持久化目录,发布后缓存和业务数据混在一起;PVC 一直 Pending,团队在应用日志里找原因,却没有看 StorageClass、PV 容量、访问模式和调度事件;多个副本同时写一个只适合单写的卷,短期看请求成功,长期看数据已经损坏;删除测试环境 Helm Release 时顺手删掉 PVC,才发现没有备份恢复演练。

Kubernetes 的存储模型不是为了让 YAML 更复杂,而是为了把“谁需要存储、需要多大、如何供给、谁能挂载、删除后如何回收、故障时如何恢复”这些问题拆开治理。后端工程师如果只会写 volumeMounts,很容易把平台存储能力当成本地目录来用;如果能理解 Volume、PV、PVC、StorageClass 和 StatefulSet 的分工,就能在设计阶段提前划清应用、平台、存储后端和运维流程的责任边界。

2. 核心概念#

这一节先把对象边界讲清楚。Kubernetes 存储体系里最容易出错的地方,不是字段名字难记,而是把不同层级的对象混成了一个“磁盘”概念。Volume 是 Pod 视角的挂载目录,PV 是集群视角的持久存储资源,PVC 是应用视角的存储申请,StorageClass 是平台视角的供给策略,StatefulSet 是控制器视角的稳定副本身份。

2.1 Volume:Pod 内可见的文件系统入口#

Volume 首先是 Pod 级别的抽象:它定义“这个 Pod 里的容器可以在哪个路径看到哪类文件系统”。容器本身的可写层会随着容器重建而变化,Volume 则把特定目录挂载到容器内,让多个容器共享文件,或者让数据跨容器重启保留。注意这里说的是“跨容器重启”,不是一定跨 Pod 重建。

不同 Volume 类型的生命周期差异很大。emptyDir 在 Pod 被调度到节点时创建,Pod 从节点移除时删除,适合临时文件、缓存、构建中间产物和同 Pod 内容器共享目录。PVC 类型的 Volume 则把 Pod 连接到持久卷,数据是否保留取决于底层 PV、StorageClass、回收策略和存储系统。

所以,看到 volumeMounts 时不要直接等同于“持久化”。它只说明容器内哪个路径被挂载了。真正要判断数据是否可靠,需要继续看 volumes 引用的是 emptyDirconfigMapsecretpersistentVolumeClaim,还是某种 CSI、NFS、local volume。

2.2 PV、PVC、StorageClass:资源、声明和供给策略#

PersistentVolume,简称 PV,是集群中的存储资源。它可能来自云盘、分布式块存储、NFS、CSI 驱动或本地磁盘。PV 关心容量、访问模式、卷模式、回收策略、节点亲和性和底层存储句柄。它不直接表达某个业务应用“想要什么”,而是表达集群里“有什么存储资源”。

PersistentVolumeClaim,简称 PVC,是命名空间内的存储申请。应用不应该直接依赖某块具体云盘,而是声明“我需要 20Gi、ReadWriteOnce、某个 StorageClass 的文件系统卷”。控制平面会尝试把 PVC 绑定到合适的 PV,或者通过 StorageClass 动态创建一个 PV。Pod 再通过 PVC 挂载存储。

StorageClass 是动态供给策略。它把 provisioner、参数、回收策略、卷绑定时机、扩容能力等平台细节封装起来,让应用侧只引用一个类名。好的 StorageClass 命名应该表达工程含义,例如 ssd-rwo-retainstandard-rwo-delete,而不是只叫 fastdefault,否则排障时很难判断性能、拓扑、删除行为和备份预期。

2.3 StatefulSet:稳定身份,不是更高级的 Deployment#

Deployment 面向无状态副本:副本之间等价,Pod 名字可变,任意副本都可以承接请求。StatefulSet 面向有状态副本:每个 Pod 有稳定序号、稳定网络身份和稳定存储身份,例如 mysql-0mysql-1。这种稳定身份适合主从数据库、需要固定成员编号的集群、分片服务和对启动顺序敏感的组件。

StatefulSet 的关键不是“能挂 PVC”,Deployment 也能挂 PVC。关键在于 volumeClaimTemplates 会为每个副本创建独立 PVC,例如 data-mysql-0data-mysql-1,Pod 重建后仍回到自己的卷。再配合 Headless Service,每个副本可以拥有可预测 DNS 名称,便于集群成员发现和复制协议工作。

不要把 StatefulSet 当成解决所有稳定性的万能工具。普通后端 API、无状态网关、只读消费者和可以水平扩展的业务服务,通常仍应使用 Deployment。只有当“副本身份本身就是业务或协议的一部分”,或者“每个副本必须绑定自己的持久数据”时,StatefulSet 才值得引入。

3. 运行机制#

理解运行机制时,可以把它拆成三条链:Pod 内 Volume 挂载链,PVC 与 PV 绑定链,StatefulSet 副本身份链。排障时先判断故障落在哪条链,再进入具体对象,而不是一上来就改 YAML。

3.1 从容器目录到 Volume 挂载#

容器启动时看到的是镜像文件系统加上挂载点。volumeMounts.mountPath 会把某个 Volume 挂到容器内指定路径,这个路径下原本来自镜像的内容会被挂载覆盖。很多“镜像里明明有文件,启动后找不到”的问题,本质上是挂载路径覆盖了镜像目录。

emptyDir 的生命周期跟 Pod 绑定。容器进程崩溃并由 kubelet 重启时,Pod 还在同一节点上,emptyDir 数据仍可能存在;但 Pod 被删除、驱逐、重新调度或节点不可用时,emptyDir 就不是持久数据来源。它适合 /tmp、批处理临时目录、同 Pod sidecar 交换文件,不适合上传附件、数据库目录和审计日志。

PVC 类型 Volume 的生命周期跟 Pod 解耦。Pod 删除后 PVC 仍在,新的 Pod 可以重新挂载同一个 PVC。这里的“可以”还要受访问模式、节点拓扑、存储驱动和调度结果约束。比如很多块存储只支持单节点读写,多个 Pod 跨节点同时写会失败或带来数据损坏风险。

3.2 从 PVC 创建到 PV 绑定#

PVC 创建后会进入绑定流程。控制平面会寻找容量足够、访问模式匹配、StorageClass 匹配、卷模式匹配、标签选择器匹配的 PV。如果找不到合适 PV,并且 PVC 指定的 StorageClass 支持动态供给,就由对应 provisioner 创建底层卷和 PV,再把 PVC 绑定上去。

volumeBindingMode 决定绑定时机。默认的 Immediate 会在 PVC 创建后立即绑定或供给,适合没有明显拓扑限制的存储。WaitForFirstConsumer 会等到有 Pod 使用 PVC 时,再结合 Pod 的调度约束选择或创建卷,适合有可用区、节点、local volume 等拓扑限制的场景。PVC Pending 不一定是错误,有时是在等第一个消费者。

绑定成功后,调度器、kubelet、CSI 驱动和存储系统继续协作完成 attach、mount 和文件系统准备。也就是说,PVC Bound 只说明“声明和资源匹配成功”,不保证 Pod 已经成功挂载。挂载失败要继续看 Pod Events、节点 kubelet 日志、CSI controller/node 日志和存储后端状态。

3.3 StatefulSet 的创建、重建与删除语义#

StatefulSet 创建副本时会按序号生成 Pod,并通过 Headless Service 建立稳定网络身份。默认语义下,副本有明确的 ordinal,Pod 名称可预测。很多有状态组件依赖这个特性决定谁是主、谁是从、谁负责哪个分片。

volumeClaimTemplates 会为每个副本创建 PVC。Pod mysql-0 被删除后,控制器会重新创建 mysql-0,并重新使用它原来的 PVC。这就是 StatefulSet 能在 Pod 变化中保持存储身份的关键。它解决的是“同一个副本找回自己的数据”,不是“数据库自动高可用”。

删除 StatefulSet 时要特别谨慎。删除控制器、删除 Pod、删除 PVC 和删除底层 PV 是不同动作。很多团队以为删除 StatefulSet 会自动保护所有数据,或者以为删除 Helm Release 只影响工作负载,结果被 chart 的 PVC 管理方式、StorageClass 的回收策略和人工清理步骤影响。生产环境必须明确删除流程和恢复路径。

4. 最小可运行示例#

示例的目标不是覆盖所有生产能力,而是让对象关系最小闭环可见。真正落地时,还需要补充命名规范、资源配额、备份策略、监控告警、安全上下文和恢复演练。

4.1 临时目录:emptyDir 适合缓存,不适合业务数据#

下面这个 Pod 把 /cache 挂成 emptyDir。它适合保存临时计算结果、下载中的文件、同 Pod 内容器之间的交换文件。只要 Pod 还在,容器重启通常不会清空它;Pod 被删除或重新调度后,它就不应被视为可靠数据。

apiVersion: v1
kind: Pod
metadata:
name: report-worker
spec:
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "date > /cache/run.txt && sleep 3600"]
volumeMounts:
- name: cache
mountPath: /cache
volumes:
- name: cache
emptyDir:
sizeLimit: 1Gi

后端项目里常见的正确用法是:报表生成过程中的中间文件、图片压缩临时目录、批处理排序临时文件。常见误用是:把上传附件、用户头像、数据库数据文件、账务导出结果长期放在 emptyDir 或容器可写层里。

4.2 普通后端服务:Deployment 通过 PVC 挂载持久目录#

如果一个无状态 API 只是需要一个共享或持久文件目录,例如保存本地开发环境里的上传文件,可以先用 Deployment 加 PVC,而不是直接上 StatefulSet。Deployment 的副本依然应该被设计成可替换,PVC 只是外部依赖之一。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: upload-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: upload-api
spec:
replicas: 1
selector:
matchLabels:
app: upload-api
template:
metadata:
labels:
app: upload-api
spec:
containers:
- name: app
image: ghcr.io/example/upload-api:1.0.0
volumeMounts:
- name: upload-data
mountPath: /app/data/uploads
volumes:
- name: upload-data
persistentVolumeClaim:
claimName: upload-data

这个例子故意使用 replicas: 1。如果要扩成多副本,必须先确认存储是否支持多节点多写,应用是否有文件锁和并发写保护,上传文件是否更应该进入对象存储。不要因为 PVC 能挂载,就默认可以让多个副本同时写。

4.3 有状态副本:StatefulSet 为每个 Pod 创建独立 PVC#

下面是 StatefulSet 的最小骨架。Headless Service 提供稳定网络域,volumeClaimTemplates 为每个副本创建独立 PVC。真实数据库部署还需要初始化、复制、备份、探针、资源限制、反亲和、升级策略和故障切换机制。

apiVersion: v1
kind: Service
metadata:
name: demo-db
spec:
clusterIP: None
selector:
app: demo-db
ports:
- name: tcp
port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo-db
spec:
serviceName: demo-db
replicas: 2
selector:
matchLabels:
app: demo-db
template:
metadata:
labels:
app: demo-db
spec:
containers:
- name: db
image: postgres:16
ports:
- name: tcp
containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources:
requests:
storage: 50Gi

这个示例只说明对象关系,不代表推荐直接裸跑生产数据库。数据库是否进集群,需要单独评估团队能力、备份恢复、升级窗口、存储 SLA、监控告警、故障切换和责任边界。

5. 配置逐行拆解#

配置字段的价值在于表达工程约束。下面不做字段百科,而是挑最容易影响生产行为的字段:访问模式、容量、存储类、回收策略、绑定时机、挂载路径和安全上下文。

5.1 accessModes、volumeMode 与容量请求#

accessModes 描述卷如何被节点挂载。常见的 ReadWriteOnce 表示卷可以被单个节点读写;ReadOnlyMany 表示多个节点只读;ReadWriteMany 表示多个节点读写;ReadWriteOncePod 则限制同一时刻只能被一个 Pod 读写。它们不是应用层并发控制,也不保证数据库写入安全,只是存储系统支持的挂载能力声明。

resources.requests.storage 是 PVC 申请的容量。动态供给时,它会影响底层卷大小;静态绑定时,它参与匹配。容量不是随便写大就好,太小会触发磁盘写满和扩容流程,太大会造成成本浪费和调度困难。生产环境要把容量增长、告警阈值、扩容窗口和回滚预案一起设计。

volumeMode 常见值是 FilesystemBlock。多数后端服务使用文件系统卷。只有数据库、存储引擎或特定中间件明确需要裸块设备时才考虑 Block,否则会增加初始化、权限和排障复杂度。

5.2 storageClassName、reclaimPolicy 与 volumeBindingMode#

storageClassName 决定 PVC 使用哪类存储供给策略。为空字符串 "" 通常表示不使用动态供给,只匹配没有 StorageClass 的 PV;不写则可能使用集群默认 StorageClass。团队应避免依赖“默认值猜测”,至少在生产模板里显式指定。

persistentVolumeReclaimPolicy 决定 PVC 释放后 PV 和底层存储如何处理。Delete 通常会删除 PV 以及底层存储资产,适合可重建数据或临时环境;Retain 会保留底层数据,适合需要人工确认、恢复或审计的生产数据。动态创建的 PV 通常继承 StorageClass 的回收策略,因此 StorageClass 的设计会直接影响删除风险。

volumeBindingMode 决定 PV 绑定和动态供给发生在什么时候。拓扑敏感的存储建议优先考虑 WaitForFirstConsumer,让调度器结合 Pod 的节点选择、亲和性、污点容忍和资源需求再做绑定。否则可能出现 PVC 已绑定到某个可用区,但 Pod 被调度约束卡住的情况。

5.3 volumeMounts、subPath 与权限边界#

volumeMounts.mountPath 是应用看到数据的位置。这个路径必须和应用配置一致,例如 Spring Boot 上传目录、日志目录、报表输出目录或数据库数据目录。排障时要同时检查应用实际写入路径和 Kubernetes 挂载路径,很多“数据没持久化”就是路径不一致。

subPath 可以把同一个卷里的子目录挂到不同路径,但它也会带来更新语义和隔离边界问题。配置文件类挂载如果使用 subPath,往往不会获得自动更新语义;多个应用共享一个 PVC 再用 subPath 分目录,也容易把权限、备份和删除边界混在一起。生产环境应谨慎使用。

权限问题常落在 runAsUserfsGroup、只读根文件系统、挂载只读标记和存储后端权限上。应用日志里可能只看到 Permission denied,真正原因可能是容器用户不能写挂载目录,或 CSI 驱动创建的目录属主不符合应用运行用户。排障要把应用错误、Pod 安全上下文和节点挂载状态放在一起看。

6. 后端工程场景#

后端服务是否需要 Kubernetes 持久存储,取决于状态的类型、生命周期、并发写入模型和恢复要求。不要用“服务会写文件”作为唯一判断标准。

6.1 无状态 API 与临时文件#

普通 REST API、网关、BFF、鉴权服务和大多数 Spring Boot 业务服务,理想形态都是无状态。它们可以写临时文件,但临时文件应放在 emptyDir 或容器临时目录,并且应用逻辑必须允许 Pod 重建后丢失这些文件。

例如上传接口可以先把分片写入临时目录,校验完成后立即转存对象存储;报表接口可以在 emptyDir 里生成 Excel,响应完成后删除;图片处理服务可以用内存或临时目录做中间态。只要这些文件不是系统事实来源,就不应该为了它们引入 StatefulSet。

如果无状态 API 需要本地缓存,要明确缓存失效和预热机制。缓存目录丢失不应影响数据正确性,最多影响性能。把缓存误当数据,是容器化迁移中很常见的事故来源。

6.2 上传、报表、队列与共享文件#

上传附件和用户生成内容通常不建议长期放在 Pod 本地 PVC 里。更稳妥的模式是对象存储加数据库元数据,应用只保存对象 key、版本、校验和和访问策略。这样可以避免多副本文件一致性、跨节点挂载、备份窗口和容量扩展问题。

报表和批处理任务要区分中间产物与最终产物。中间产物适合 emptyDir,最终产物如果需要长期下载,应进入对象存储、归档存储或专门的文件服务。不要让业务用户依赖某个 Pod 内路径。

队列消费者和离线任务如果需要断点续跑,可以把 checkpoint 写入数据库、消息系统 offset、对象存储元数据或专门状态表,而不是默认写本地文件。只有当框架或组件明确要求本地持久目录时,才为它设计 PVC 和恢复流程。

6.3 数据库是否应该放进 Kubernetes#

数据库能不能进 Kubernetes,不是一个简单的是非题。Kubernetes 可以通过 StatefulSet、PVC、探针、反亲和和 Operator 管理数据库实例,但这不等于团队已经具备数据库生产运维能力。真正的门槛在备份恢复、主从切换、版本升级、存储性能、故障演练和责任边界。

适合入集群的情况包括:团队有成熟 Operator 或平台能力;数据库规模可控;已经有自动备份、恢复演练和监控告警;能接受 Kubernetes 节点、存储、网络与数据库运维耦合。研发测试环境、临时环境、小型内部工具也可以用集群内数据库提高交付效率。

不适合入集群的情况包括:核心交易库、强监管数据、极高 IOPS 或低延迟要求、团队没有存储和数据库值班能力、没有做过恢复演练。此时使用云厂商托管数据库或独立数据库集群,通常比“为了统一部署形态而入集群”更可靠。

7. 常见错误与排障#

存储故障排查要坚持证据链:先看对象状态和 Events,再看调度与挂载,再看 CSI 和节点,再看应用日志。不要只盯着应用报错,因为应用通常只能看到最终现象。

7.1 PVC Pending:先分清是等待还是失败#

PVC Pending 的第一步是执行 kubectl describe pvc <name>,看 Events、StorageClass、请求容量、访问模式和是否有绑定候选 PV。如果 StorageClass 不存在、默认类未设置、容量不足、访问模式不匹配或静态 PV 的 storageClassName 不一致,PVC 都可能无法绑定。

如果 StorageClass 使用 WaitForFirstConsumer,PVC Pending 可能是在等待 Pod 创建和调度。此时要继续看使用它的 Pod 是否存在、是否被节点选择器、亲和性、污点、资源不足卡住。不要在没看绑定模式前就误判为存储故障。

排障证据建议按顺序保存:kubectl get pvc,pv,sckubectl describe pvc,使用 PVC 的 Pod Events,StorageClass YAML,相关 namespace 的资源配额,以及最近一次发布或 Helm 变更。这样复盘时才能判断是应用声明错误、平台默认值变化,还是底层存储供给失败。

7.2 Pod 挂载失败、数据丢失与多写冲突#

Pod 挂载失败时,先看 kubectl describe pod <pod> 的 Events。常见原因包括卷还没 attach、节点不可达、CSI node 插件异常、权限不匹配、访问模式冲突、存储后端不可用。PVC Bound 不代表容器已经能写入目录。

数据“没有持久化”时,不要先怀疑 Kubernetes 丢数据,而要核对应用写入路径、mountPath、容器工作目录、环境变量配置和镜像默认数据目录。数据库镜像尤其容易因为数据目录配置错误,把数据写到未挂载路径。

多副本写冲突更危险,因为它可能不立即失败,而是以数据错乱、索引损坏、文件覆盖、锁失效的形式延迟暴露。凡是多个副本共享写同一个 PVC,都必须确认存储访问模式、文件系统语义和应用级并发控制。对大多数数据库而言,更正确的方式是每个副本独立 PVC,通过数据库复制协议同步。

7.3 StatefulSet 故障:身份、顺序与删除风险#

StatefulSet 故障要先确认三件事:Pod ordinal 是否符合预期,Headless Service 是否存在并匹配 selector,每个 Pod 是否绑定到自己的 PVC。网络身份问题常表现为成员发现失败,存储身份问题常表现为副本找不到自己的数据。

滚动更新卡住时,不要只重启 Pod。要看 readiness probe、启动顺序、应用复制状态、partition 策略、PodDisruptionBudget 和前一个副本是否真正可用。有状态组件的“Ready”不应只代表端口打开,还应代表数据恢复、复制追平或成员状态正常。

删除风险要写进操作手册。删除 Pod 通常可恢复,删除 StatefulSet 控制器不一定删除 PVC,删除 PVC 则可能触发 PV 回收策略,删除底层存储可能直接造成数据不可恢复。生产操作必须明确每一步会影响哪个对象,以及是否已有可用备份。

8. 生产化边界#

学习环境可以只关注跑通,生产环境必须关注生命周期闭环:容量从哪里来,谁审批,谁备份,如何恢复,如何扩容,如何删除,故障时谁负责。

8.1 备份恢复与 RPO/RTO#

PVC 不是备份。PV 也不是备份。存储副本、云盘快照、数据库逻辑备份、对象存储版本控制和跨地域复制解决的是不同层级的问题。生产系统必须明确 RPO 和 RTO:最多能丢多少数据,多久必须恢复服务。

备份策略要贴合数据类型。数据库通常需要逻辑备份、物理备份、WAL/binlog 归档和定期恢复演练;文件类数据要考虑对象版本、元数据一致性和删除保护;临时数据则不需要备份,但要保证丢失后可重建。

恢复演练比备份成功更重要。团队至少要验证:能否从备份恢复到新 PVC,应用能否指向恢复后的数据,权限和属主是否正确,恢复后数据校验如何做,旧 PVC 如何保留以便回滚。没有恢复演练的备份,只是心理安慰。

8.2 容量、拓扑与扩容窗口#

容量治理要从申请值开始。PVC 申请太小会频繁扩容,太大会造成成本和调度压力。生产模板应配置磁盘使用率告警、增长趋势、扩容审批和应急预案。对数据库,还要关注 IOPS、吞吐、延迟和 fsync,而不仅是 GiB。

拓扑约束决定可用性。云盘可能绑定可用区,local PV 绑定节点,NFS 或分布式存储有自己的性能和故障域。调度策略、反亲和、节点池和 StorageClass 必须一起设计,否则会出现“Pod 能调度但卷不可用”或“卷可用但 Pod 调度不了”。

扩容不是任意可逆操作。很多存储支持扩容但不支持缩容,文件系统扩容可能需要 Pod 重启或节点侧操作。扩容前要确认 StorageClass 是否允许扩容、应用是否能承受延迟、备份是否完成,以及扩容失败时如何处理。

8.3 安全、权限与运维流程#

存储安全不仅是 Secret。PVC 里的数据可能包含用户文件、导出报表、数据库文件和审计日志,必须考虑访问权限、加密、备份保留、删除保护和跨环境隔离。开发、测试、生产不应共享同一底层数据资产。

权限边界要在模板中固化。应用运行用户、fsGroup、只读挂载、目录属主、初始化脚本和存储驱动默认权限,都可能影响可写性。用人工 kubectl exec chmod 修一次不是治理,应该把权限声明回写到部署模板或镜像初始化逻辑。

运维流程要覆盖创建、变更、扩容、备份、恢复和删除。尤其是删除:删除应用不等于删除数据,删除 PVC 可能触发底层资产删除。生产环境建议对 PVC 删除、StorageClass 回收策略和 Helm 卸载行为设置显式评审或保护。

9. 什么时候用,什么时候不用#

选择存储方案时,先问状态属于谁、能否重建、是否需要多副本写、恢复目标是什么,再决定使用 emptyDir、PVC、对象存储、外部数据库还是 StatefulSet。

9.1 Deployment 加 PVC,还是 StatefulSet#

如果副本之间没有固定身份,只是单副本服务需要一个持久目录,Deployment 加 PVC 就足够。典型场景是内部工具、小型文件服务、开发测试环境里的上传目录。此时重点是限制副本数、明确备份策略和避免并发写。

如果每个副本都必须拥有独立数据,且副本身份对协议有意义,就考虑 StatefulSet。典型场景是数据库副本、分片节点、需要固定成员名的协调组件。此时重点是 volumeClaimTemplates、Headless Service、启动顺序、复制协议和故障切换。

如果只是无状态 API,不要为了“稳定”使用 StatefulSet。它会带来固定身份、存储生命周期和更新顺序等额外复杂度,却不能自动提升可用性。无状态服务的稳定性应来自多副本、探针、滚动发布、限流熔断和外部化状态。

9.2 PVC,还是对象存储、托管数据库和外部服务#

用户上传文件、图片、附件、报表归档和静态资源,优先考虑对象存储。对象存储天然适合多副本访问、生命周期策略、版本控制、跨区域复制和 CDN 分发。PVC 更像是给 Pod 提供文件系统,不是通用文件平台。

核心数据库优先考虑托管数据库或成熟数据库平台,除非团队明确选择并具备集群内数据库运维能力。StatefulSet 能提供稳定身份和存储绑定,但数据库高可用、备份恢复、升级兼容、性能调优仍然是数据库系统本身的问题。

日志、指标、链路追踪和审计数据不要简单写本地 PVC。日志应该进入日志系统,指标进入时序数据库,链路进入 tracing 后端,审计数据进入满足合规要求的存储。把可观测性数据压到业务 Pod 本地磁盘,会让故障时最需要的数据最先丢失。

9.3 最终决策清单#

引入持久存储前,至少回答这些问题:数据是否能重建,Pod 删除后是否必须保留,多个副本是否会同时写,是否需要跨可用区,谁负责备份,恢复要多久,删除 PVC 会发生什么,扩容是否可逆,应用写入路径是否和挂载路径一致。

如果这些问题答不清,不要急着把 YAML 写复杂。先把状态从应用里拆出来:能进数据库的进数据库,能进对象存储的进对象存储,能重建的放临时目录,必须本地持久的再用 PVC,必须稳定副本身份的再用 StatefulSet。

真正成熟的标准不是“会写多少种 Volume”,而是团队能否把存储选择沉淀成模板、评审清单、排障手册和恢复演练。Kubernetes 存储对象只是工具,工程结果应该是可预测、可恢复、可审计。

10. 下一篇衔接#

本篇解决的是 Kubernetes 存储边界:Volume 让 Pod 看到文件系统,PV/PVC 拆开资源和声明,StorageClass 统一供给策略,StatefulSet 给有状态副本稳定身份。到这里,后端服务已经从配置外置走到了数据和状态治理。

下一篇会进入 第 12 篇:Kubernetes 发布、回滚与健康检查。发布系统会继续追问:有状态工作负载如何滚动更新,什么时候不能并发替换,健康检查如何表达“数据已恢复”,回滚是否会影响 PVC 和数据库 schema。也就是说,存储不是孤立主题,它会直接影响发布策略和故障恢复。

如果你已经能区分 emptyDir、PVC、PV、StorageClass 和 StatefulSet 的职责,并能沿着 PVC Pending、挂载失败、数据丢失、多副本冲突的证据链排查,就可以继续进入发布与健康检查;如果还只能记住 YAML 字段,建议先回到“这个字段错了会出现什么现象”再复盘一遍。

11. 官方参考#

第 11 篇:Kubernetes Volume、PV、PVC 与 StatefulSet
https://jupiter-ws.cn/posts/backend/container/11_kubernetes_volume_pv_pvc_stateful_workloads/
作者
Jupiter
发布于
2026-07-05
许可协议
CC BY-NC-SA 4.0