AI时代的Kubernetes教程36招-第15招-PV、PVC、StorageClass、CSI和VolumeSnapshot怎么保证数据不丢?
建立K8s存储完整链路:PV、PVC、StorageClass、CSI与VolumeSnapshot
把 hostPath 当共享存储,等于把数据押在调度运气上。
[读者] 正在用 K8s 跑数字人平台的数据与平台工程师
[痛点] Pod 一漂移数据就丢,PVC 长期 Pending,扩容删库两头怕
[现在读] 多 Node 生产要把存储链路讲清楚,才敢让状态服务长期驻留
[读完] 能独立设计 PV/PVC/StorageClass/CSI/快照的链路、边界与回收策略
[旧方案] Deployment + hostPath + emptyDir 混着用
|
v
[新需求] PostgreSQL、知识库、模型缓存都要跑在多 Node 集群
|
v
[冲突] 本地盘不跟随 Pod 漂移,PVC 又绑不住拓扑与回收策略
|
v
[后果] 数据错乱、副本各看各的、误删 PVC 直接抹掉生产库
我是老李,做数字人平台的基础设施,集群从 3 个 Node 长到 20 多个 Node,上面跑着三块状态数据:数字人驱动的 PostgreSQL、向量知识库、模型缓存。
半年前我们的「存储方案」只有两个词:hostPath 和 emptyDir。理由朴素——单机时代它跑得挺好,数据落在宿主机磁盘上,Pod 重启还在,于是它就被当成共享存储用了。
直到业务要求这三块数据都必须跑在多 Node 生产集群上,问题才真正显形。
那么,本地盘到底在哪一步开始骗人?
PV、PVC、StorageClass、CSI、快照这五样东西,各自负责链路里的哪一段?
如果不做区分,全部塞进一个默认 StorageClass,又会发生什么?
01、故事
最早的三节点集群里,PostgreSQL 用的是 Deployment + hostPath,副本数 1。YAML 里写死 path: /data/pg,运维觉得这和裸机装数据库没区别。
第一次出事是节点维护。Pod 被驱逐,ReplicaSet 在新节点重建。新节点上 hostPath 目录是空的,PostgreSQL 的初始化逻辑把空目录当新实例,建了一个全新的库。监控看着进程健康、端口可连,连接池也还活着,直到第一次写入失败才报警——表已经没了。
第二次出事更隐蔽。为了高可用,我们把副本数改成 2。调度器把两个 Pod 放在了同一台 Node 上,因为另一台资源不够。两个 PostgreSQL 实例同时挂 /data/pg,一个在写 WAL,一个在读,postmaster.pid 互相覆盖。日志里是文件锁相关的报错,但我们花了两个小时才意识到:这不是数据库的问题,是存储语义的问题。
任务是让这三块状态数据在多 Node 集群上稳定驻留一年,并且能被备份、能被恢复、能被审计。
那一刻我想明白了一件事:hostPath 描述的是「节点上的一个目录」,而不是「一份跟着业务走的存储」。它不会因为换了节点而迁移,也不会因为起了两个副本就变成两块盘。
我们需要一份能被调度器理解、能被控制器兑现、能被审计和回收的存储契约。这才是 PV、PVC、StorageClass、CSI 这套东西存在的理由。
本地盘不随人走
副本共享即互踩
语义错则全盘错
先改契约再改码
02、问题
旧方案的失效点分三个层次。
业务层:PostgreSQL 存的是驱动与话术配置,知识库存的是向量与文档,模型缓存是可重建的大文件。前两者丢了要重建数天,模型缓存丢了只是变慢。旧方案对三者一视同仁,都是 hostPath,等于给最贵的数据配了最不可靠的盘。
技术层:hostPath 有三条硬伤。第一,绑定到节点本地文件系统,Pod 漂移后数据不可见;第二,没有容量声明、没有访问模式、没有回收策略,K8s 无法参与生命周期管理;第三,安全边界差,hostPath 在 Pod Security Admission 的 restricted 档位下直接被禁止。
管理层:删 Pod 不心疼,删 PVC 才心疼。动态 Provisioning 造出的 PV 默认继承 reclaimPolicy: Delete,PVC 一删,后端卷跟着删。没有快照就没有后悔药。
可验证的完成标准,我列了五条:
- 删掉 Pod 后重建,数据仍在,且不依赖它落在哪个节点;
kubectl get pvc全部 Bound,kubectl describe pvc无反复重试事件;- 三类数据使用不同 StorageClass,扩容能力、回收策略、绑定模式显式声明;
- 关键库有 VolumeSnapshot,并能从快照恢复出一个新 PVC;
- 任何一次
kubectl delete pvc都不会静默抹掉生产数据。
失效要分三层看
业务技术加管理
标准写成可验项
否则争论无终局
03、原理
本篇只需要四个原理。
第一,声明与实现分离。 PVC 表达需求(容量、访问模式、类名、卷模式),PV 描述资产(后端卷标识、容量、访问模式、回收策略、拓扑)。这是两份不同视角的对象,绑定控制器负责撮合。需求变了改 PVC,资产变了改 PV。把两者混在一起谈「存储怎么配」,永远谈不清。
第二,绑定是 1:1 且不可逆的。 一个 PVC 绑定一个 PV,绑定后 spec.volumeName 被写入,不能改绑。所以「先随便建一个 PVC,以后再换」这条路不通,只能新建 PVC 再迁移数据。
第三,StorageClass 定义的是「怎么造卷」,不是「卷本身」。 动态 Provisioning 里,provisioner 指向 CSI 驱动,parameters 是驱动私有参数,reclaimPolicy、volumeBindingMode、allowVolumeExpansion、allowedTopologies 是集群级策略。带 storageclass.kubernetes.io/is-default-class: 'true' 注解的类,会被没有写 storageClassName 的 PVC 隐式使用——这是很多人「莫名其妙用错盘」的源头。反过来,storageClassName: '' 表示明确不使用任何类,只走静态绑定。
第四,CSI 是接口规范,不是实现。 控制面由一组 sidecar 组成:external-provisioner 造卷删卷,external-attacher 负责挂载与卸载,external-resizer 负责扩容,external-snapshotter 负责快照,node-driver-registrar 把驱动注册到 kubelet。它们通过 Unix Domain Socket 与驱动通信,集群本身不关心后端是 Ceph、NFS 还是云盘。
反直觉判断:volumeBindingMode: WaitForFirstConsumer 看起来是「延迟绑定」,实质是「把拓扑决策提前交给调度器」。如果用 Immediate,卷会在 PVC 创建瞬间被造在某个拓扑域(比如某个可用区),调度器之后只能被动接受——Pod 反而更容易卡在 Pending。延迟的是动作,提前的是决策。
另一个反直觉判断:回收策略写在 StorageClass 上,但生效结果落在 PV 上。改 StorageClass 不会改变已有 PV 的行为,想救历史数据,只能逐个 patch PV。
声明实现两分离
绑定一次不可逆
类定造法非造卷
拓扑先决后造卷
04、架构
[输入] 业务声明(容量/访问模式/类名/卷模式)
|
v
[模块] PVC + StorageClass + CSI 控制面 sidecar
|
v
[数据/状态] PV 对象 + 后端卷 + VolumeSnapshotContent
|
v
[处理] 调度选拓扑 → provisioner 造卷 → attacher 挂载 → kubelet 挂到 Pod
|
v
[输出] Pod 内的文件系统或块设备
边界说明:
- PVC 只管需求,不管资产,它不关心后端是什么。
- StorageClass 只管「怎么造」,不负责「造出来之后活多久」——回收策略虽然写在类里,但会被复制进 PV。
- CSI 只管「把卷挂上去」,不管业务是否需要 RWX。访问模式是 K8s 侧契约,最终能不能多节点同时挂,取决于后端能力和驱动的
NodeStageVolume、NodePublishVolume实现。 - 快照属于另一条链:VolumeSnapshot → VolumeSnapshotContent → 后端快照对象,由
external-snapshotter驱动,和 PV/PVC 是并列而非包含关系。
收益:存储从「节点属性」变成「集群资源」,可声明、可调度、可审计、可回收。
代价:多了一层抽象和至少一个额外控制器组件;出问题时排查面从「一个目录」变成「对象 + 事件 + sidecar 日志」三处。
适用条件:只要是多 Node 集群、只要数据需要跟随业务而不是跟随机器,这套抽象就是必需的。单机开发环境可以退回 hostPath,但那条路不该出现在生产清单里。
五层各管一段路
越层代劳必出错
收益旁边写代价
边界清楚才敢扩
05、实战一次
先确认环境:
kubectl version --short
kubectl get nodes
kubectl get storageclass
kubectl get csidrivers
示例输出:
Client Version: v1.29.x
Server Version: v1.29.x
NAME STATUS ROLES AGE VERSION
node-1 Ready worker 30d v1.29.x
node-2 Ready worker 30d v1.29.x
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
standard (default) rancher.io/local-path Delete WaitForFirstConsumer
如果 kubectl get csidrivers 返回空,说明集群没有注册 CSI 驱动,动态 Provisioning 与快照都无法使用。这一步必须先确认,否则后面所有现象都会被误判成「K8s 坏了」。
V1:静态 PV + PVC,不依赖任何驱动,先让链路跑通。
先建一个本地卷 PV(deploy/storage/v1/pv-local.yaml):
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-pg-local
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
storageClassName: manual
local:
path: /data/pg
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-1
再建对应的 PVC(deploy/storage/v1/pvc-pg.yaml):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-pg
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
应用并验证:
kubectl apply -f deploy/storage/v1/pv-local.yaml
kubectl apply -f deploy/storage/v1/pvc-pg.yaml
kubectl get pv pv-pg-local -o wide
kubectl get pvc pvc-pg
未在当前环境实测,以下为预期结果。
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS
pv-pg-local 20Gi RWO Retain Bound default/pvc-pg manual
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
pvc-pg Bound pv-pg-local 20Gi RWO manual
V1 的关键观察: PVC 里的 storageClassName: manual 必须和 PV 完全一致才能绑定;PV 的 nodeAffinity 把这份数据钉死在 node-1。这不是缺陷,是本地卷的诚实表达——它本来就不跟随 Pod。
V1 的遗留问题: 这个 PVC 只能绑到 node-1,Pod 也只能调度到 node-1。如果 node-1 下线,Pod 无法在其他节点启动。这正是动态 Provisioning 要解决的事情:让卷跟着调度走,而不是让调度迁就卷。
先跑最小一条路
再谈动态与快照
能跑通的不省略
未验处须标未验
06、排查
链一:PVC 长期 Pending。
现象:kubectl get pvc 显示 pvc-kb 状态 Pending,十分钟不变。
怀疑:绑定控制器找不到可用 PV,或 StorageClass 不存在、无默认类。
检查:
kubectl describe pvc pvc-kb
kubectl get storageclass
kubectl get events --field-selector involvedObject.name=pvc-kb
证据:事件里出现
Warning ProvisioningFailed storageclass.storage.k8s.io 'fast-ssd' not found
根因:PVC 写了 storageClassName: fast-ssd,但集群里只有 standard,同时没有默认类注解,于是空类名的 PVC 也一起挂住。
修复:要么建出 fast-ssd,要么把 PVC 的类名改成真实存在的类。注意 PVC 的 storageClassName 在绑定时是可变字段,但一旦 Bound 再改会导致不可预期行为,尚未绑定的 PVC 改类名最干净。
错误尝试: 第一时间删掉 Pending 的 PVC 重建,期望刷新一下就好。这是错的——Pending 的 PVC 没有产生任何后端卷,重建只是重复同一个错误;而且如果集群里存在一个宽松的默认类,重建时会从「等待显式类」变成「立刻用默认类造卷」,问题被掩盖,等你在生产上误删时才爆发。正确做法是先看事件,确定是类不存在、容量不满足,还是访问模式不匹配。
链二:Pod 卡在 ContainerCreating。
现象:kubectl describe pod 显示 FailedAttachVolume 或 FailedMount,Pod 进不了 Running。
怀疑:卷拓扑域与节点不匹配,或 RWO 卷被多节点同时使用。
检查:
kubectl describe pod pg-0
kubectl get pv pv-xxx -o jsonpath='{.spec.nodeAffinity}'
kubectl get volumeattachment
证据:
Warning FailedAttachVolume Volume is already exclusively attached to one node and can't be attached to another
根因:StorageClass 用了 ReadWriteOnce 加 Immediate 绑定,卷被造在 zone-a,而 Pod 落在 zone-b;或者同一个 RWO 卷被两个副本引用。
修复:把 volumeBindingMode 改成 WaitForFirstConsumer,让调度器先定节点再定卷;如果业务确实需要多副本共享,必须换成后端能力支持 ReadWriteMany 的 StorageClass,而不是把 RWO 硬塞给多副本。
链三:删了 PVC,生产库没了。
现象:一次清理测试环境的 kubectl delete pvc 执行后,PostgreSQL 数据目录被清空。
怀疑:PVC 保护终结点没生效?还是 PV 被回收了?
检查:
kubectl get pv -o custom-columns=NAME:.metadata.name,POLICY:.spec.persistentVolumeReclaimPolicy,STATUS:.status.phase
kubectl get sc standard -o jsonpath='{.reclaimPolicy}'
证据:PV 的回收策略是 Delete;PVC 上的 kubernetes.io/pvc-protection 终结点只保护仍被 Pod 使用的 PVC,Pod 已停时它不阻止删除。
根因:默认 StorageClass 的 reclaimPolicy 是 Delete,动态 Provisioning 造出的 PV 继承了它。测试环境快速迭代的习惯,被带到了生产。
修复:为有状态数据单独建 StorageClass,显式写 reclaimPolicy: Retain;已有 PV 逐个 patch 成 Retain;删除操作前先确认没有业务在用这个 PVC。
现象先于猜测看
事件里藏着真话
删了重建是歧路
根因不落不罢休
07、优化
基于第 06 章的三条证据,V2 的修改点如下。
根因 1:所有数据共用一个默认类。
修改:按数据特征拆成三个 StorageClass(deploy/storage/v2/)。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: sc-pg-ssd
provisioner: csi.example.com
parameters:
type: ssd
iops: '3000'
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values:
- zone-a
- zone-b
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: sc-kb-shared
provisioner: csi.example.com
parameters:
protocol: nfs
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: sc-cache-local
provisioner: csi.example.com
parameters:
type: standard
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
原因:PostgreSQL 要求低延迟、强一致、单写,用块存储 RWO 加高 IOPS,Retain 保证误删 PVC 后卷还在,WaitForFirstConsumer 避免造在 Pod 到不了的可用区;知识库需要多副本同时读、需要快照做版本回滚,用支持 ReadWriteMany 的文件存储,Immediate 可以接受,因为共享文件系统不绑单可用区;模型缓存可重建、体积大、成本敏感,用普通盘加 Delete 回收,丢了就重新拉。
新行为:
- PVC 必须显式写类名,默认类不再承接生产数据;
- 误删 PVC 后 PV 进入
Released,数据仍在,可手工回收或重新绑定; - PVC 可以扩容但不能缩容,这是 K8s 侧的硬约束,别指望改小。
验证步骤(未在当前环境实测,以下为验证方法而非结果):
kubectl get pvc -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,SC:.spec.storageClassName,STATUS:.status.phase
kubectl get pv -o custom-columns=NAME:.metadata.name,POLICY:.spec.persistentVolumeReclaimPolicy,STATUS:.status.phase
预期:三类 PVC 各自 Bound 到对应类的 PV,且 sc-pg-ssd 与 sc-kb-shared 的 PV 回收策略为 Retain。
根因 2:RWO 卷被多副本共享。
修改:PostgreSQL 改用 StatefulSet,每个副本一份自己的卷。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg
spec:
serviceName: pg
replicas: 3
selector:
matchLabels:
app: pg
template:
metadata:
labels:
app: pg
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: sc-pg-ssd
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
新行为:三个副本产生三个独立 PVC(data-pg-0、data-pg-1、data-pg-2),互不争抢,副本漂移时各自的卷跟着走。
根因 3:没有快照,回不去。
修改:为知识库补快照链路,前提是集群已安装 external-snapshotter 与对应 CSI 驱动的快照能力。
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: vsc-kb
driver: csi.example.com
deletionPolicy: Retain
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: snap-kb-20240101
spec:
volumeSnapshotClassName: vsc-kb
source:
persistentVolumeClaimName: pvc-kb
新行为:快照对象与 PVC 解耦,deletionPolicy: Retain 保证删掉 VolumeSnapshot 时后端快照不被连带删除。恢复时用快照造一个新 PVC,挂到验证 Pod 上确认数据,而不是直接覆盖生产卷。
根因一处改一处
策略写进类里定
新行为须能验证
不编数据不吹牛
08、演进
[同一输入] 一份需要跟 Pod 走的状态数据
|
+--[V1] Deployment + hostPath / 静态 local PV
| 行为:数据落在节点本地,Pod 漂移即失联
| 代价:无容量声明、无回收策略、无法扩容、不可迁移
|
+--[V2] PVC + 专用 StorageClass + CSI 驱动 + 快照
| 行为:卷随调度创建,策略显式声明,可扩容可快照
| 代价:多一层抽象,依赖 CSI 驱动与 sidecar 健康
|
[Trade-off]
得到:数据与节点解耦、生命周期可审计、误删可挽回
失去:单机时代「目录即数据」的简单心智,以及一部分启动确定性
适用边界:多 Node 集群、需要备份或扩容的状态服务
正确性:V1 在单节点、单副本场景下可用,一旦出现漂移或多副本就失效;V2 的正确性来自显式契约,且能被 kubectl get 验证。
稳定性:V1 的故障面是节点磁盘,V2 的故障面多了 CSI 驱动与 sidecar。后者更复杂,但可观测、可重试、可替换驱动。
复杂度:V1 一条 YAML;V2 需要 StorageClass、PVC 模板、快照类三份声明,还需要集群侧装驱动。新增的复杂度必须换来明确能力,否则就是过度设计。
成本:本地盘便宜,网络存储贵。所以模型缓存这类可重建数据,不该和 PostgreSQL 用同一个类——这是成本优化,也是风险隔离。
适用范围:开发环境仍然可以用 hostPath 或本地卷;生产多 Node 集群里,ReadWriteOnce 的块存储是数据库的默认选择,共享文件存储留给多读场景,可重建数据用最便宜的一档。
遗留问题:跨可用区的容量池不均衡、快照恢复的 RTO 未做实测、Retain 的 PV 需要人工回收流程。这些本次没有解决,但必须写下来。
同一份需求两解
得稳定性失灵活
取舍写清再上路
遗留问题不隐藏
09、洞见
9.1 存储问题本质是契约问题,不是设备问题
ReadWriteOnce 不是「一块盘只能一个客户端」,它是「这个卷同一时刻只能被挂到一台节点上」。这是 K8s 侧的契约,和后端能不能多挂无关。契约写错,后端再强也救不了。
反直觉判断:把 RWO 卷挂给两个副本,Pod 并不会立刻报错——它们会一起启动、一起写,然后在文件锁或 WAL 层炸掉。错误发生得比配置错误晚得多,定位成本极高。宁可让它在 attach 阶段就失败。
契约先行设备后
错误晚发更难查
让它在早处失败
别让它在深处炸
9.2 WaitForFirstConsumer 是提前决策,不是延迟执行
反直觉判断:名字里的 WaitFor 让人以为是性能折中,实际上它改的是决策顺序。Immediate 让 provisioner 先选拓扑、调度器后迁就;WaitForFirstConsumer 让调度器先定节点、provisioner 再就近造卷。前者在按可用区分区的后端上会产生大量节点亲和冲突,后者几乎不会。
名为等待实为决
顺序一变堵自开
拓扑连着调度器
分开设计必打架
9.3 回收策略是这个体系里最贵的一个字段
删除 PVC 是一个语义轻巧的操作,后果却可能不可逆。PVC 保护终结点只保护仍被 Pod 使用的 PVC,它不保护「没人在用但数据重要」的卷。能真正拦住你的只有 reclaimPolicy: Retain 和快照。
所以 Retain 的代价不是存储费用,而是 PV 会长期停留在 Released 状态、需要人工回收。这笔人工成本必须提前认下来,否则运维会偷偷把它改回 Delete。
一个字段定生死
保护终结点有限
人工成本先认下
否则必然被改回
9.4 CSI 的价值是让存储能力可以被替换
CSI 把造卷、挂载、扩容、快照拆成标准 gRPC 接口,K8s 只跟接口打交道。好处是换后端不用改业务 YAML,代价是排障时要多看一层:kubectl describe 看 K8s 侧,sidecar 日志和驱动日志看 CSI 侧,两边时间线要对齐。
反直觉判断:集群里 kubectl get csidrivers 为空时,不要怀疑 StorageClass 写错了——没有驱动时,任何 provisioner 名字都不会被响应,PVC 只会一直 Pending。先确认驱动存在,再谈参数。
接口统一换后端
代价多查一层日志
先确认驱动存在
再怀疑参数写错
10、系统落地
原来有什么:一个三节点集群,PostgreSQL 用 Deployment 加 hostPath,知识库和模型缓存直接挂 emptyDir;没有 StorageClass 概念,没有快照,删 PVC 就等于删数据。
本篇新增了什么:deploy/storage/ 目录下的三类 StorageClass 声明、StatefulSet 的 volumeClaimTemplates、VolumeSnapshotClass 与快照 YAML,以及一条可验证的排查路径——PVC 事件到 PV 策略,再到挂载事件与 sidecar 日志。
现在能做什么:为任意一个新的状态服务选择存储档位——数据库用 RWO 块存储加 Retain 加 WaitForFirstConsumer,共享读用 RWX 文件存储,可重建缓存用 Delete 加低成本盘;能用快照造新 PVC 做恢复演练;能在误删 PVC 后从 Released 状态的 PV 里把数据捞回来。
还缺什么:缺少自动化的 PV 回收流程,Released 状态的 PV 会堆积;缺少快照保留策略,快照数量会无限增长;缺少跨可用区的容量水位监控;缺少对 CSI 驱动升级的灰度方案。
下一步如何演进:把选存储档位变成一条准入约束——通过策略禁止生产命名空间里出现无 storageClassName 的 PVC 和 hostPath 卷;再把快照演练变成定期任务,用 CronJob 造快照并恢复验证,让恢复路径始终处于可用状态。
原有之物先盘清
本篇新增补一段
现在能做哪几步
还缺什么要写明
11、小结
Q1 → 本地盘在 Pod 漂移的那一刻开始骗人:它不迁移,也不会因为多副本而变成多块盘
Q2 → PV 描述资产,PVC 表达需求,StorageClass 定造法,CSI 是接口层,VolumeSnapshot 是独立的快照链
Q3 → 全部塞进一个默认 StorageClass,会继承 Delete 回收策略与 Immediate 绑定,误删即丢数据、跨区即卡调度
状态 → 具备为三类数据分别设计存储档位、并用对象状态与事件验证整条链路的可交付能力
三问三答闭环成
交付能力可复现
状态如实不夸大
下一程自分明
12、作业
12.1 理解题:PV 和 PVC 在绑定之后各自还能改什么?
参考答案:绑定后 PVC 的 spec.volumeName 被写入,不能改绑;spec.resources.requests.storage 只能增加不能减少,且需要 StorageClass 开启 allowVolumeExpansion;spec.accessModes 与 volumeMode 基本不可变。PV 侧可以改 persistentVolumeReclaimPolicy,例如从 Delete 改成 Retain,这对已经存在的卷是有效的事后补救手段,但改 StorageClass 不会影响已有 PV。
12.2 实战题:为一份需要多副本同时读取的知识库数据写存储声明,并说明为什么这样选。
参考答案:使用支持 ReadWriteMany 的 StorageClass,通常基于 NFS、CephFS 等共享文件系统;volumeBindingMode 可以设为 Immediate,reclaimPolicy 设为 Retain,另配 VolumeSnapshotClass 做版本回滚。理由是多副本同时读需要共享语义,RWO 只能挂到一个节点;知识库更新频率低但价值高,所以回收策略必须保守。
12.3 排障题:PVC 一直 Pending,kubectl describe pvc 只有正常事件没有失败事件,下一步查什么?
参考答案:先确认 kubectl get csidrivers 是否为空——如果没有任何 CSI 驱动注册,provisioner 名字再正确也没人响应,PVC 会静默停在 Pending。其次确认 volumeBindingMode: WaitForFirstConsumer 时是否有 Pod 在引用该 PVC,因为该模式下不调度就永远不造卷。再看 external-provisioner 的日志。
12.4 架构判断题:有人说「把 hostPath 换成 PVC 就等于解决了多节点数据一致性问题」,这个判断对吗?
参考答案:不对。换成 PVC 只是把「节点目录」换成了「集群资源」,一致性取决于三件事:访问模式是否与副本数匹配,多写必须用支持 RWX 的后端或改成每副本独立 PVC;回收策略是否保守;拓扑约束是否与调度策略协调。如果用了 RWO 加 Immediate 加多副本,PVC 反而会把故障从数据分叉升级为卷挂载失败。存储抽象解决的是生命周期问题,不是一致性问题——一致性终究由业务与数据库自己负责。
理解要落到概念
实战要落到命令
排障要落到证据
判断要落到边界
13、思考
回到最初的冲突:本地盘之所以骗人,不是因为它是本地的,而是因为它没有被写成契约——没有容量、没有访问模式、没有回收策略、没有拓扑声明,于是所有责任都落在了运维的记性上。
这套 Storage 链路真正提供的,是把「这份数据值多少钱、能不能重建、能不能容忍误删、能不能跨节点共享」从口头共识,变成集群里可读、可校验、可被准入策略拦截的对象。PV、PVC、StorageClass、CSI、VolumeSnapshot 各自只管一段,任何一段缺位,责任就会滑回人的记性里。
所以判断标准很简单:一条存储声明,能不能让一个不熟悉这套系统的工程师在五分钟内答出「删了会怎样、漂了会怎样、满了会怎样」。答不出来,它就不是契约,只是配置。
存储无小事
边界即安全
证据定取舍
一链贯始终

浙公网安备 33010602011771号