第 35 篇 k8s之PVC 与 StorageClass:动态存储供应

0 阅读9分钟

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。


在第 34 篇中,我们学了 emptyDir 和 hostPath。它们解决的是“Pod 内部容器间共享文件”和“访问宿主机特定路径”的问题。但一个最核心的生产需求还没有解决:Pod 重建后,数据如何保留?

如果 Redis 的 Pod 被重新调度到另一台节点上,emptyDir 和 hostPath 都无法将数据带过去。emptyDir 随 Pod 删除而销毁,hostPath 只存在于特定节点上。我们需要一种与 Pod 生命周期解耦、可在集群任意节点访问的持久化存储

这正是 PV(PersistentVolume,持久卷)和 PVC(PersistentVolumeClaim,持久卷声明)的设计目标。在 Docker 生态中,我们通过 docker volume create 来创建命名卷,容器重启、删除都不会丢失数据。但在 K8s 集群中,管理员不可能为每个应用手动在每台节点上创建 Volume——我们需要一种自动化的动态供给机制。今天这篇,我们就从 PV/PVC 的核心概念讲起,通过贯穿案例的 Redis 持久化存储,深入理解 StorageClass 如何实现“声明即存储”。

一、PV 和 PVC 的抽象模型

1.1 为什么需要 PV 和 PVC?

在第 7 篇 Docker 数据管理中,我们创建 Volume 的命令是 docker volume create redis-data,然后在 docker run 时用 -v redis-data:/data 挂载。这种方式在单机上工作良好,但在 K8s 集群中有两个致命缺陷:

  • 不跨节点:Pod 重新调度到另一台节点时,新节点上没有这个 Volume。

  • 人工运维:管理员需要提前创建好 Volume,开发者需要知道 Volume 的具体名称和配置。

K8s 的解决方案是将存储抽象为两个独立的对象,实现管理员与开发者的职责分离

  • PV(PersistentVolume):集群管理员准备的存储资源,可以来自 NFS、云存储(AWS EBS、GCE PD)、本地磁盘等。PV 与 Pod 生命周期无关——Pod 被删了,PV 还在。

  • PVC(PersistentVolumeClaim):开发者声明对存储的需求:“我需要 1Gi 空间,读写一次即可”。K8s 自动找匹配的 PV 绑定给这个 PVC。

1.2 用“租房子”来理解 PV 和 PVC

把 K8s 集群想象成一个租房市场:

  • PV 是房东已装修好的房源(一套具体的房子,位于某小区某单元),由管理员(中介)提前准备好。

  • PVC 是租客的求租需求——“我需要 50 平米、朝南的一居室”。租客不需要知道具体哪套房子,只需要描述需求。

  • K8s 的匹配引擎 是中介,自动将合适的房源(PV)匹配给租客需求(PVC)。匹配后,这套房子就归这个租客独占使用。

  • 如果租客搬走了(Pod 删除),PVC 还在,房子仍然保留(PV 保留)。新租客(重建的 Pod)可以通过同一个 PVC 继续使用同一套房子和里面的家具(数据)。

1.3 PV 的三种供给方式

在实际工作中,静态供给只适合极少数固定存储资源已知的场景(比如一个已经部署好的 NFS 服务器)。生产环境的标配是动态供给——你只需要声明“我要 1Gi 的云盘”,StorageClass 自动调用云 API 创建。

二、静态供给:理解 PV 和 PVC 的匹配机制

在进入动态供给之前,先通过一个静态供给的简单示例理解 PV 和 PVC 的绑定关系。

2.1 创建一个 PV

apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /mnt/data

关键字段:

  • capacity.storage: 1Gi:PV 的容量

  • accessModes: ReadWriteOnce:访问模式,单个节点读写

  • hostPath:在 Minikube 环境中使用 hostPath 模拟真实存储,生产环境会替换为云存储(如 awsElasticBlockStore

2.2 创建一个 PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pvc-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

PVC 不指定具体 PV 名称,只声明需求:“我要 1Gi、单节点读写”。K8s 自动找到匹配的 PV 并绑定。

kubectl apply -f task-pv.yaml
kubectl apply -f task-pvc.yaml
kubectl get pvc

输出:

NAME             STATUS   VOLUME          CAPACITY   ACCESS MODES   STORAGECLASS   AGE
task-pvc-claim   Bound    task-pv-volume   1Gi        RWO                           30s

STATUS=Bound 表示 PVC 已成功绑定到 task-pv-volume 这个 PV。STORAGECLASS 列为空,表示这是静态供给,没有用到 StorageClass。

2.3 在 Pod 中使用 PVC

volumes:
  - name: task-storage
    persistentVolumeClaim:
      claimName: task-pvc-claim

Pod 引用 PVC,PVC 绑定 PV,PV 对应物理存储。这是三层引用链——开发者只需关心 PVC,不必知道底层存储的具体位置和类型。如果你删除了这个 Pod,重新创建时引用同一个 PVC,依然能访问到原来的数据。

2.4 访问模式

大多数数据库(MySQL、Redis)需要 RWO 模式。共享文件存储(如 NFS)可以支持 RWX 模式。如果 Pod 被调度到不同节点上,RWO 模式的 PV 将无法在新节点上挂载——这就是为什么数据库这类有状态应用需要用 StatefulSet(第 27 篇)并配合节点亲和性来管理。

三、StorageClass 与动态供给

静态供给的问题很明显:管理员需要预先创建每个 PV,开发者扩容时要找管理员创建新的 PV。当集群中有数百个应用、每个应用都需要存储时,这根本不可行。

StorageClass 解决了这个问题。它定义了一个“存储模板”:当 PVC 被创建时,StorageClass 自动调用后端存储的 API 创建 PV,并绑定给 PVC。

3.1 StorageClass 的工作原理

PVC 创建 → StorageClass → Provisioner 调用 API → 自动创建 PV → 绑定 PVC → Pod 挂载

管理员只需要创建 StorageClass 对象(一次性配置),之后开发者创建 PVC 时,PV 会自动生成。这正是“声明式存储”的核心思想——你不再需要关心 PV 是谁创建的、在哪创建的,只需要在 PVC 中声明“我要什么”。

3.2 Minikube 中的 StorageClass

Minikube 默认自带一个 StorageClass:

输出:

NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate
  • (default):集群的默认 StorageClass,PVC 不指定 storageClassName 时自动使用

  • PROVISIONER:负责实际创建存储的组件,minikube-hostpath 在宿主机上创建目录模拟真实存储

  • RECLAIMPOLICY: Delete:PVC 被删除时,PV 也被自动删除

  • VOLUMEBINDINGMODE: Immediate:PVC 创建后立即绑定 PV,不需要等待 Pod 调度

3.3 删除策略

Retain 策略适用于需要保留数据做审计或恢复的场景。但 Retain 后 PV 变为 Released 状态,不能被新的 PVC 自动绑定——需要管理员手动清除 PV 中的旧数据并重新标记为 Available

3.4 卷绑定模式

WaitForFirstConsumer 在云环境中非常重要:如果 PVC 立即创建了可用区 A 的云盘,但 Pod 被调度到可用区 B,Pod 将无法挂载这个云盘。延迟绑定确保了存储资源始终与 Pod 在同一可用区,避免了跨可用区的存储挂载问题。

四、实战:为 Redis 配置动态持久化存储

现在把理论应用到贯穿案例中。为 Redis 创建一个使用动态供给的 PVC,确保 Redis 数据在 Pod 重建后不丢失。

4.1 创建 PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: redis-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: standard

PVC 不指定任何具体的 PV 名称或节点路径,只声明了“我要 1Gi、RWO 模式、使用 standard StorageClass”。剩下的全部自动化——StorageClass 调用 Provisioner 创建 PV,K8s 将 PV 绑定到 PVC。

kubectl apply -f redis-pvc.yaml
kubectl get pvc
# NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# redis-pvc   Bound    pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890   1Gi        RWO            standard       10s

4.2 部署 Redis 使用 PVC

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:alpine
          ports:
            - containerPort: 6379
          volumeMounts:
            - name: redis-data
              mountPath: /data
      volumes:
        - name: redis-data
          persistentVolumeClaim:
            claimName: redis-pvc

重要:如果使用 RWO 模式(单节点读写),请务必将 replicas 设为 1。Redis Deployment 的多个副本会创建多个 Pod,而 RWO 卷只能挂载到单个节点上——如果两个 Pod 被调度到不同节点,第二个 Pod 将无法挂载同一个 PVC,导致启动失败。多副本有状态应用应使用 StatefulSet,每个 Pod 绑定独立的 PVC。

4.3 验证持久性

# 写入数据
kubectl exec deploy/redis -- redis-cli set counter 100

# 删除 Pod(不删除 PVC)
kubectl delete pod -l app=redis

# 等待新 Pod 启动
kubectl get pods -l app=redis -w

# 验证数据仍然存在
kubectl exec deploy/redis -- redis-cli get counter
# "100"

这就是 PVC 持久化的直观体现:Pod 被删了,PV 还在,PVC 还在,数据完好无损。新 Pod 通过同一个 PVC 挂载到同一个 PV,Redis 启动时从 AOF 文件中恢复了之前的键值对。

# 查看自动创建的 PV
kubectl get pv
# NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS
# pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890   1Gi        RWO            Delete           Bound

五、对比 Docker Compose 的存储方式

Docker Compose 的命名卷适合单机场景,但在跨主机、动态供给、访问控制方面有明显的局限性。这正是 K8s PV/PVC 体系的设计价值所在。

六、PVC 扩容

PVC 支持动态扩容(需要 StorageClass 启用 allowVolumeExpansion: true):

kubectl patch pvc redis-pvc -p '{"spec":{"resources":{"requests":{"storage":"2Gi"}}}}'
# persistentvolumeclaim/redis-pvc patched

kubectl get pvc redis-pvc -w
# 等待 CAPACITY 从 1Gi 变为 2Gi

注意:并非所有存储后端都支持扩容,且只能扩容不能缩容。云存储通常支持在线扩容,不会影响正在运行的 Pod。

七、命令速查表

八、本篇总结

  • PV 和 PVC 的关系:PV 是实际的存储资源,PVC 是应用对存储的需求声明。PVC 绑定 PV 后,Pod 通过 PVC 挂载持久化存储。这种抽象实现了管理员与开发者的职责分离。

  • StorageClass 动态供给:当 PVC 指定 StorageClass 时,系统自动调用 Provisioner 创建 PV,无需管理员预先准备存储。这是生产环境的标配。

  • 访问模式与 Pod 副本的关系:RWO 卷只能挂载到单个节点,多副本有状态应用应使用 StatefulSet(后续第 27 篇相关主题的扩展内容),每个 Pod 绑定独立的 PVC。

  • 与 Compose 的演进关系:Compose 的命名卷是单机手动模式,PVC 是集群自动化模式。思路一致(卷的生命周期独立于容器),但 K8s 在规模和自动化程度上提升了几个数量级。

通过本篇,Redis 的数据真正实现了跨 Pod 生命周期的持久化。下一篇——第 36 篇:资源管理:Requests、Limits 与 QoS,我们将学习如何限制 Pod 的 CPU 和内存使用,防止单个 Pod 过度消耗资源影响同节点的其他 Pod,让集群的资源使用更加公平和可预测。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !