Files

21 KiB
Raw Permalink Blame History

tags, create time
tags create time
k8s
storage
pv
pvc
statefulset
devops
2026-07-07 15:48

Kubernetes 存储 — PV、PVC 与 StatefulSet

概述

容器的本质是短暂的(ephemeral):Pod 被删除后,其内部所有文件随之消散。这对无状态服务无关紧要,但对数据库、消息队列等有状态应用来说是致命的。Kubernetes 通过分层存储抽象 —— Volume → PersistentVolume(PV)→ PersistentVolumeClaim(PVC)→ StorageClass —— 将"存储供给"与"存储消费"解耦,让开发者无需关心底层基础设施,只需声明"我需要多大、什么访问模式的存储",即可自动获得持久化能力。配合 StatefulSet,K8s 还为有状态应用提供了稳定的网络标识和有序的生命周期管理。

卷(Volume)基础

Volume 是 K8s 最早引入的存储概念,与 Pod 同生命周期(除非是持久卷类型)。它定义在 Pod spec 中,可被同一 Pod 内的多个容器共享。

常见卷类型

卷类型 生命周期 典型用途 是否持久
emptyDir Pod 存续期间 容器间共享临时文件、缓存 否
hostPath 节点存续期间 访问宿主机文件(如 Docker socket) 节点级持久
nfs 独立于 Pod 多节点共享存储 是
configMap / secret Pod 存续期间 注入配置和敏感信息 否
persistentVolumeClaim 独立于 Pod 绑定 PV,正式的持久化方案 是

emptyDir 示例

apiVersion: v1
kind: Pod
metadata:
  name: shared-data
spec:
  containers:
    - name: writer
      image: busybox
      command: ["sh", "-c", "echo hello > /data/msg && sleep 3600"]
      volumeMounts:
        - name: cache
          mountPath: /data
    - name: reader
      image: busybox
      command: ["sh", "-c", "cat /data/msg && sleep 3600"]
      volumeMounts:
        - name: cache
          mountPath: /data
  volumes:
    - name: cache
      emptyDir: {}

[!tip] emptyDir 的 medium: Memory 配置可将其挂载为 tmpfs(RAM 磁盘),适用于对 I/O 延迟极敏感的场景,但会计入容器内存用量。

hostPath 注意事项

hostPath 直接暴露宿主机目录,存在安全风险。生产环境应优先使用 PVC。如果必须使用,建议设置 type 字段限制行为:

volumes:
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
      type: Socket  # 仅允许已存在的 socket 文件

hostPath 的 type 可选值:DirectoryOrCreate、Directory、FileOrCreate、File、Socket、CharDevice、BlockDevice。

持久卷(PersistentVolume)

PersistentVolume(PV)是集群级别的存储资源,由管理员预先创建或由 StorageClass 动态供给。它与 Pod 完全解耦 —— Pod 删除后 PV 仍然存在。

PV 详解

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-data
spec:
  capacity:
    storage: 10Gi           # 存储容量
  accessModes:
    - ReadWriteMany         # 访问模式
  persistentVolumeReclaimPolicy: Retain  # 回收策略
  storageClassName: slow    # 存储类名称
  nfs:
    server: 192.168.1.100
    path: /exports/data

关键字段说明:

字段 说明
capacity.storage PV 的存储容量,只能声明一种资源(目前仅支持 storage)
accessModes 定义卷的挂载方式,可声明多种,但实际只能使用一种
persistentVolumeReclaimPolicy PVC 释放后 PV 的处理策略:Retain / Delete / Recycle
storageClassName 关联的 StorageClass 名称,为空字符串表示不关联任何类
volumeMode Filesystem(默认)或 Block(原始块设备)
mountOptions 挂载选项,如 ["hard", "nfsvers=4.1"]

[!info] PV 的状态包括 Available(未绑定)、Bound(已绑定到 PVC)、Released(PVC 已删除但 PV 未回收)、Failed(自动回收失败)。

回收策略(Reclaim Policy)

当 PVC 被删除后,PV 的回收策略决定了其后续行为。

策略 行为 适用场景
Retain PV 状态变为 Released,数据保留,需管理员手动处理 有保留价值的数据
Delete PV 及底层存储资源一并删除 动态供给的临时存储
Recycle 执行 rm -rf /volume/*,PV 回到 Available(已废弃) 仅兼容简单存储后端
stateDiagram-v2
    [*] --> Available: PV 创建
    Available --> Bound: PVC 绑定
    Bound --> Released: PVC 删除

    Released --> Available: Recycle (rm -rf)
    Released --> [*]: Delete (删除底层存储)
    Released --> Released: Retain (等待手动处理)

    state "管理员手动操作" as manual
    Released --> manual: 需人工介入
    manual --> Available: 修复后重新上线
    manual --> [*]: 彻底删除

[!warning] Recycle 策略已被废弃,且仅对 NFS 和 hostPath 有效。生产环境应优先使用动态供给 + Delete 策略,或 Retain 策略配合手动运维。

访问模式(Access Modes)

模式 缩写 含义
ReadWriteOnce RWO 单节点读写挂载
ReadOnlyMany ROX 多节点只读挂载
ReadWriteMany RWX 多节点读写挂载
ReadWriteOncePod RWOP 单 Pod 读写(K8s 1.22+)

各存储后端对访问模式的支持情况:

存储后端 RWO ROX RWX RWOP
本地磁盘 (local) 支持 - - 支持
AWS EBS 支持 - - 支持
GCE PD 支持 支持 - 支持
Azure Disk 支持 - - 支持
Azure File 支持 支持 支持 -
Ceph RBD 支持 支持 - -
CephFS 支持 支持 支持 -
NFS 支持 支持 支持 -
Longhorn 支持 支持 支持 -

[!tip] 为什么不是所有存储都支持 RWX?因为多节点同时写入需要分布式文件系统级别的并发控制(如分布式锁、一致性协议)。块存储(EBS、PD)天然不支持多写,必须通过 NFS/文件系统层封装才能实现 RWX。

持久卷声明(PersistentVolumeClaim)

PVC 是用户对存储资源的"请求"。用户不直接创建或管理 PV,而是通过 PVC 声明需求(容量、访问模式、存储类),K8s 负责将合适的 PV 绑定到 PVC。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  storageClassName: fast-ssd

PVC 绑定机制

PVC 与 PV 的绑定有两种模式:

静态供给(Static Provisioning)

管理员手动创建 PV 资源,用户创建 PVC 后,K8s 控制面中的 PV controller 根据容量、访问模式、存储类等条件进行匹配。

动态供给(Dynamic Provisioning)

用户创建 PVC 后,若集群中没有匹配的 PV,且 PVC 指定了 StorageClass,K8s 会调用对应的 provisioner 自动创建 PV 并完成绑定。这是生产环境中最常用的方式。

flowchart TD
    A[用户创建 PVC] --> B{是否有匹配的 Available PV?}
    B -->|是| C[绑定 PV 到 PVC]
    B -->|否| D{PVC 指定了 StorageClass?}
    D -->|否| E[PVC 持续 Pending]
    D -->|是| F[StorageClass Provisioner 创建 PV]
    F --> C
    C --> G[Pod 挂载 PVC 使用]

    style E fill:#f96,stroke:#333
    style G fill:#6f9,stroke:#333

绑定匹配规则:

  1. PV 与 PVC 的 storageClassName 必须一致
  2. PV 的 accessModes 必须包含 PVC 请求的模式
  3. PV 的 capacity.storage 必须 >= PVC 请求的容量
  4. PV 的 volumeMode 必须匹配
  5. 若存在多个匹配 PV,优先选择容量最小的满足者(最小匹配原则)
  6. 可通过标签选择器(selector)进一步筛选

[!info] 一对一绑定:一个 PV 只能绑定一个 PVC,一个 PVC 也只能绑定一个 PV。绑定后即使 PV 的标签被修改,绑定关系也不会解除。

StorageClass

StorageClass 定义了存储的"类别",是动态供给的核心。它告诉 K8s 使用哪个 provisioner、以什么参数创建存储。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs  # 存储驱动
parameters:
  type: gp3                          # 传递给 provisioner 的参数
  fsType: ext4
  iopsPerGB: "50"
reclaimPolicy: Delete                # PV 回收策略
volumeBindingMode: WaitForFirstConsumer  # 绑定时机
allowVolumeExpansion: true           # 允许扩容
mountOptions:
  - debug

关键字段详解:

字段 说明 常见值
provisioner 存储驱动标识 kubernetes.io/aws-ebs、rancher.io/local-path、csi.hetzner.cloud 等
parameters 传递给 provisioner 的键值对,不同驱动参数不同 EBS: type/fsType;Ceph: pool/clusterID
reclaimPolicy 动态创建的 PV 的回收策略,默认 Delete Retain、Delete
volumeBindingMode PV 创建和绑定的时机 Immediate(默认)、WaitForFirstConsumer
allowVolumeExpansion 是否允许 PVC 扩容,默认 false true、false
mountOptions 动态创建的 PV 的挂载选项 NFS: ["hard","nfsvers=4.1"]

[!warning] volumeBindingMode: WaitForFirstConsumer 的重要性:默认的 Immediate 模式会在 PVC 创建时立即绑定/创建 PV,这可能导致 PV 创建在与 Pod 不同的可用区(尤其在多可用区集群中),最终 Pod 调度失败。WaitForFirstConsumer 会延迟到 Pod 调度时才创建 PV,确保存储与计算在同一拓扑域。

sequenceDiagram
    participant User as 用户
    participant API as API Server
    participant SC as StorageClass
    participant PV Controller
    participant Provisioner

    Note over User,Provisioner: Immediate 模式
    User->>API: 创建 PVC
    API->>PV Controller: 协调 PVC
    PV Controller->>Provisioner: 创建 PV
    Provisioner-->>PV Controller: PV Ready
    PV Controller->>API: 绑定 PV ↔ PVC

    Note over User,Provisioner: WaitForFirstConsumer 模式
    User->>API: 创建 PVC (Pending)
    User->>API: 创建 Pod
    API->>PV Controller: Pod 调度完成,触发绑定
    PV Controller->>Provisioner: 在 Pod 所在节点/可用区创建 PV
    Provisioner-->>PV Controller: PV Ready
    PV Controller->>API: 绑定 PV ↔ PVC

StatefulSet

StatefulSet 是 K8s 为有状态应用设计的工作负载控制器,解决了 Deployment 无法满足的三个核心需求:

  1. 稳定的网络标识:Pod 名称固定(pod-0、pod-1),通过 Headless Service 提供 DNS 记录(pod-0.service-name.namespace.svc.cluster.local)
  2. 稳定的持久存储:每个 Pod 拥有独立的 PVC,Pod 重建后 PVC 不变
  3. 有序的部署/扩缩/删除:Pod 按序号顺序操作,前一个就绪后才启动下一个

StatefulSet vs Deployment

特性 Deployment StatefulSet
Pod 名称 随机后缀 有序编号(-0、-1、-2)
部署顺序 并行 顺序(前一个 Ready 后才启动下一个)
删除顺序 并行 逆序
网络标识 不稳定 稳定,配合 Headless Service
存储 共享 PVC 或无存储 每个 Pod 独立 PVC(通过 volumeClaimTemplates)
缩容 随机删除 从最大序号开始删除

volumeClaimTemplates

volumeClaimTemplates 是 StatefulSet 的核心特性 —— 它会为每个 Pod 自动创建独立的 PVC。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql          # 必须关联 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 20Gi

上例中,K8s 会自动创建三个 PVC:data-mysql-0、data-mysql-1、data-mysql-2,每个绑定独立的 PV。

[!info] StatefulSet 缩容时,PVC 不会被删除。这意味着缩容后再扩容,新 Pod 会重新挂载原来的 PVC,数据不丢失。这是 StatefulSet 最重要的特性之一。

CSI(Container Storage Interface)

CSI 是 K8s 存储插件的标准化接口规范(自 K8s 1.13 GA),取代了早期的 in-tree volume plugin 方式。

为什么需要 CSI

早期 K8s 的存储驱动代码直接存在于 K8s 主仓库(in-tree),导致:

  • 存储供应商必须将代码合入 K8s 仓库,发布周期受限于 K8s
  • K8s 仓库臃肿,维护负担重
  • 驱动 bug 可能影响 K8s 核心组件稳定性

CSI 将存储驱动移出 K8s 核心(out-of-tree),通过 gRPC 接口标准化通信。

CSI 架构

graph TD
    subgraph "Kubernetes 组件"
        AK[API Server]
        KC[kube-controller-manager]
        KM[kubelet]
    end

    subgraph "CSI 组件(集群级别)"
        CP[CSI Controller Plugin]
        CS[CSI Driver 注册]
    end

    subgraph "CSI 组件(节点级别)"
        NP[CSI Node Plugin]
    end

    subgraph "存储后端"
        S[Cloud Provider / SAN / NFS]
    end

    AK --> KC
    KC -->|创建/删除/扩容| CP
    CP --> S
    KM -->|挂载/卸载| NP
    NP --> S
    CS -.->|注册| AK

CSI 的三个核心组件:

  • CSI Controller Plugin:集群级别,负责卷的创建、删除、快照、扩容等控制面操作
  • CSI Node Plugin:节点级别,负责卷的挂载(NodePublishVolume)、卸载(NodeUnpublishVolume)等数据面操作
  • CSIDriver 对象:在 K8s 中注册驱动,声明能力(如 attachRequired、podInfoOnMount)

常见 CSI 驱动

存储后端 CSI 驱动名称 说明
AWS EBS ebs.csi.aws.com AWS 官方,替代 in-tree aws-ebs
GCE PD pd.csi.storage.gke.io GCP 官方
Azure Disk disk.csi.azure.com Azure 官方
Ceph RBD rbd.csi.ceph.com Ceph 官方
Longhorn driver.longhorn.io Rancher 开源分布式存储
OpenEBS openebs.io/local / openebs.io/zfs-localpv 开源容器原生存储
Hetzner Cloud csi.hetzner.cloud Hetzner 云盘
本地路径 rancher.io/local-path Local Path Provisioner,开发测试常用

CSI Driver 部署示例

以 AWS EBS CSI Driver 为例,通常通过 Helm 安装:

helm repo add aws-ebs-csi-driver https://kubernetes-sigs.github.io/aws-ebs-csi-driver
helm install aws-ebs-csi-driver aws-ebs-csi-driver/aws-ebs-csi-driver \
  --namespace kube-system

安装后会在集群中创建 ebs.csi.aws.com CSIDriver 对象和对应的 DaemonSet(Node Plugin)+ Deployment(Controller Plugin)。

实战示例

以下完整示例演示如何部署一个带持久化存储的 MySQL StatefulSet。

1. StorageClass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: mysql-ssd
provisioner: rancher.io/local-path       # 本地开发环境使用 local-path
reclaimPolicy: Retain                     # 数据库场景用 Retain 更安全
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

2. Headless Service

apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
    - port: 3306
      targetPort: 3306
      name: mysql
  clusterIP: None            # Headless Service,StatefulSet 必需
  selector:
    app: mysql

3. StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 1               # 单节点 MySQL,生产建议配合 Operator 做主从
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          ports:
            - containerPort: 3306
              name: mysql
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: ROOT_PASSWORD
          volumeMounts:
            - name: mysql-data
              mountPath: /var/lib/mysql
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi
          readinessProbe:
            exec:
              command:
                - mysqladmin
                - ping
                - -h
                - localhost
            initialDelaySeconds: 10
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: mysql-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: mysql-ssd
        resources:
          requests:
            storage: 10Gi

4. Secret(MySQL root 密码)

apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
type: Opaque
stringData:
  ROOT_PASSWORD: "MyS3cur3P@ssw0rd"

5. 客户端 Pod 测试连接

apiVersion: v1
kind: Pod
metadata:
  name: mysql-client
spec:
  containers:
    - name: mysql-client
      image: mysql:8.0
      command: ["sleep", "3600"]

进入客户端 Pod 后,使用 StatefulSet 的稳定 DNS 名称连接:

kubectl exec -it mysql-client -- bash
mysql -h mysql-0.mysql.default.svc.cluster.local -u root -p

[!tip] DNS 解析规则:<pod-name>.<headless-service-name>.<namespace>.svc.cluster.local。mysql-0.mysql 即可解析,完整 FQDN 为 mysql-0.mysql.default.svc.cluster.local。

常见陷阱与最佳实践

PVC 持续 Pending

现象:PVC 长时间处于 Pending 状态,Pod 无法启动。

排查步骤:

# 查看 PVC 事件
kubectl describe pvc <pvc-name>

# 查看是否有匹配的 PV
kubectl get pv

# 查看 StorageClass 是否存在
kubectl get storageclass

常见原因:

  • 没有匹配的 Available PV(静态供给场景下)
  • StorageClass 的 provisioner 未安装或配置错误
  • volumeBindingMode: WaitForFirstConsumer 下,没有 Pod 消费该 PVC
  • 底层存储配额不足或权限不足

动态供给配置错误

现象:PVC Pending,事件中显示 waiting for first consumer to be created before binding。

排查:确认 volumeBindingMode 设置是否符合预期。如果 Pod 已经存在但仍未绑定,检查:

  • Pod 是否成功调度到节点(Pod 事件)
  • CSI Node Plugin 是否在目标节点正常运行(DaemonSet Pod 状态)
  • Provisioner 的参数是否正确(如 parameters.type 是否为合法值)

RWX 不支持的存储类型

现象:PVC 声明 ReadWriteMany 但一直 Pending。

原因:块存储(EBS、GCE PD、Azure Disk)不支持 RWX。解决方案:

  • 使用支持 RWX 的存储后端:NFS、CephFS、Azure File、Longhorn、OpenEBS
  • 使用 ReadWriteOnce(如果应用本身不支持多写,如 MySQL 单实例)
  • 引入分布式锁机制或应用层复制

PVC 扩容

扩容需要 StorageClass 设置 allowVolumeExpansion: true,且 CSI 驱动支持在线扩容:

# 编辑 PVC 请求的容量
kubectl patch pvc mysql-data-mysql-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

# 查看扩容进度
kubectl describe pvc mysql-data-mysql-0

[!warning] PVC 扩容只能增大不能缩小。部分存储后端支持在线扩容(无需重启 Pod),部分需要 unmount 后才能扩容,具体取决于 CSI 驱动实现。

数据备份策略

PV 中的数据没有内置的备份机制,需要额外配置:

  • VolumeSnapshot:K8s 1.20+ GA,通过 CSI 驱动创建卷快照
  • Velero:开源的 K8s 备份恢复工具,支持卷快照 + 资源对象的完整备份
  • 应用层备份:对于数据库,使用 mysqldump、pg_dump 等原生工具定期备份
  • 存储层快照:利用云平台的 EBS Snapshot、Azure Snapshot 等定期快照
# VolumeSnapshot 示例
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-snapshot
spec:
  volumeSnapshotClassName: csi-aws-ebs-snapclass
  source:
    persistentVolumeClaimName: mysql-data-mysql-0

最佳实践总结

  1. 始终使用 volumeBindingMode: WaitForFirstConsumer,避免多可用区集群中存储与计算分离
  2. 数据库场景使用 reclaimPolicy: Retain,防止误删 PVC 导致数据丢失
  3. 为 StatefulSet 配置 Readiness Probe,确保 K8s 在 Pod 真正就绪后才启动下一个
  4. 生产环境使用 CSI 驱动替代 in-tree plugin,获得更好的维护性和功能支持
  5. 定期备份 PV 数据,不要依赖 K8s 的存储抽象作为唯一的持久化保障
  6. 监控 PV 使用率,设置 PVC 扩容告警,避免磁盘满导致服务中断
  7. 避免使用 hostPath 作为生产存储方案,除非有明确的安全和隔离措施

延伸阅读