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 一删,后端卷跟着删。没有快照就没有后悔药。

可验证的完成标准,我列了五条:

  1. 删掉 Pod 后重建,数据仍在,且不依赖它落在哪个节点;
  2. kubectl get pvc 全部 Bound,kubectl describe pvc 无反复重试事件;
  3. 三类数据使用不同 StorageClass,扩容能力、回收策略、绑定模式显式声明;
  4. 关键库有 VolumeSnapshot,并能从快照恢复出一个新 PVC;
  5. 任何一次 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 回收,丢了就重新拉。

新行为:

  1. PVC 必须显式写类名,默认类不再承接生产数据;
  2. 误删 PVC 后 PV 进入 Released,数据仍在,可手工回收或重新绑定;
  3. 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 各自只管一段,任何一段缺位,责任就会滑回人的记性里。

所以判断标准很简单:一条存储声明,能不能让一个不熟悉这套系统的工程师在五分钟内答出「删了会怎样、漂了会怎样、满了会怎样」。答不出来,它就不是契约,只是配置。

存储无小事
边界即安全
证据定取舍
一链贯始终

posted @ 2026-10-12 10:18  李福春  阅读(2)  评论(0)    收藏  举报