跳转至

存储:PersistentVolume、CSI 与数据路径

Kubernetes 使用 Volume 把存储挂入 Pod,并使用 PersistentVolume 子系统把应用的存储需求与具体后端分开。应用通过 PersistentVolumeClaim(PVC)声明容量、访问模式和 StorageClass;集群为它绑定 PersistentVolume(PV);CSI Driver 再把块设备或文件系统交给目标节点上的 Kubelet。

CSI 只规定容器编排系统调用存储驱动的接口。数据最终保存在云盘、本地盘、Ceph、JuiceFS 或其他系统中,取决于 StorageClass 指定的驱动和后端。设计 AI 工作负载的数据路径时,还要分别处理容器镜像、只读模型与训练数据、Checkpoint 以及节点本地缓存。

Kubernetes 存储模型

Volume 与持久化数据

容器进程看到的文件系统由镜像层、容器可写层和挂载到容器内的 Volume 组成。容器重建时,可写层随旧容器一起删除;Volume 中的数据是否保留,由 Volume 类型及其后端决定。

Pod 在 spec.volumes 中声明卷,在 containers[*].volumeMounts 中指定容器内的目录。一个 Volume 可以挂给同一 Pod 中的多个容器。emptyDir 等临时卷跟随 Pod 生命周期,PV 则独立于使用它的 Pod;两者都可以在容器重启后保留数据。Kubernetes 官方的 Volumes 文档列出了各类卷的生命周期和挂载方式。

PersistentVolume 子系统通过三个对象描述动态供给:

  • StorageClass:指定 provisioner、后端参数、卷绑定模式、回收策略和是否允许扩容。
  • PersistentVolumeClaim:记录应用申请的容量、访问模式、Volume Mode 和 StorageClass。
  • PersistentVolume:记录已经供给的卷、CSI Driver、后端 volumeHandle、节点拓扑和绑定关系。

下图展示了这三个对象与 Pod、CSI Driver 和存储后端的关系。

flowchart LR
    Pod["Pod<br/>uses claimName"] --> PVC["PersistentVolumeClaim<br/>capacity · access mode"]
    PVC --> SC["StorageClass<br/>provisioner · policy"]
    PVC <--> PV["PersistentVolume<br/>driver · volumeHandle"]
    SC --> Sidecar["external-provisioner"]
    Sidecar --> Driver["CSI Controller Service"]
    Driver --> Backend["storage backend"]
    PV --> Kubelet["Kubelet Volume Manager"]
    Kubelet --> Node["CSI Node Service"]
    Node --> Backend

StorageClass、PVC 与 PV

vLLM 示例使用 chapter01-hostpath StorageClass 和 vllm-model-cache PVC。以下字段决定卷的供给与回收方式:

  • provisioner:hostpath.csi.k8s.io 与 CSI Driver 的名称一致,external-provisioner 据此选择驱动。
  • volumeBindingMode:WaitForFirstConsumer 把动态供给推迟到 Scheduler 选出节点之后,使卷拓扑与 Pod 的节点约束一起计算。
  • reclaimPolicy:Delete 表示 PVC 释放后,动态创建的 PV 和后端卷进入删除流程。
  • accessModes:ReadWriteOnce 要求卷以读写方式挂载到一台 Node;它不限制同一 Node 上只能有一个 Pod 使用该卷。
  • resources.requests.storage:PVC 申请 10 GiB。存储后端可以提供更大的卷,但不能以更小的卷满足该请求。
examples/chapter-01/12-vllm/01-model-cache.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: chapter01-hostpath
provisioner: hostpath.csi.k8s.io
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
  kind: fast
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vllm-model-cache
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: chapter01-hostpath
  resources:
    requests:
      storage: 10Gi

动态供给完成后,PV 的 spec.csi 保存驱动返回的信息:

  • driver:负责该卷的 CSI Driver 名称。
  • volumeHandle:驱动用于标识后端卷的不透明 ID,后续 CSI RPC 使用它定位同一块卷。
  • volumeAttributes:驱动返回并需要在挂载时继续使用的属性。
  • nodeAffinity:该卷可以被哪些 Node 使用。带有可用区或节点拓扑的卷依靠它约束调度。
  • claimRef:当前绑定的 PVC。

访问模式

访问模式描述一块 PV 可以怎样挂载,不代表存储系统会在写入冲突时自动提供应用级锁。PVC 与 PV 必须具有兼容的访问模式。

模式 缩写 含义
ReadWriteOnce RWO 以读写方式挂载到一台 Node;同一 Node 上的多个 Pod 仍可能共同访问
ReadOnlyMany ROX 以只读方式挂载到多台 Node
ReadWriteMany RWX 以读写方式挂载到多台 Node
ReadWriteOncePod RWOP 整个集群中只允许一个 Pod 以读写方式使用;仅支持 CSI 卷

严格单写实例应选择 RWOP。RWO 解决的是 Node 级挂载范围,不能替代 RWOP 的 Pod 级互斥。具体语义见 Persistent Volumes 的 Access Modes。

Volume Mode

PV 和 PVC 的 volumeMode 必须一致:

  • Filesystem:默认模式。Kubernetes 把卷挂载为目录;空白块设备通常在首次挂载前创建文件系统。
  • Block:把原始块设备映射给容器,不创建文件系统。Pod 使用 volumeDevices 指定容器内设备路径,应用负责块设备的数据布局。

模型权重、数据集和 Checkpoint 通常通过文件接口访问,因此使用 Filesystem。数据库或自建存储引擎需要直接管理块设备时,才使用 Block。

绑定模式

StorageClass 的 volumeBindingMode 决定动态供给发生在节点选择之前还是之后:

  • Immediate:PVC 出现后立即供给和绑定。后端具有可用区或节点拓扑时,提前创建的卷可能与 Pod 的 GPU、Node affinity 或其他约束不在同一位置。
  • WaitForFirstConsumer:等到消费 PVC 的 Pod 进入调度。Scheduler 先把 Pod 的计算资源、节点约束和存储拓扑放在同一次节点筛选中,再触发供给。

示例使用 WaitForFirstConsumer。PVC 创建后先保持 Pending;消费它的 Pod 进入调度后,Scheduler 选择 Node,并在 PVC 上写入 volume.kubernetes.io/selected-node。

回收策略

PV 的 persistentVolumeReclaimPolicy 决定 PVC 释放后的处理方式:

  • Delete:删除动态创建的 PV,并由 external-provisioner 调用 CSI DeleteVolume 删除后端卷。适合可重新生成的数据和已经由其他系统完成保护的数据。
  • Retain:PV 进入 Released,后端数据保留。管理员确认数据用途并清理 claimRef 或存储内容后,才能重新使用或删除。

回收策略只处理 PVC 释放后的卷,不提供备份、异地副本或误删除恢复。生产数据仍需要存储系统自己的 Snapshot、备份和灾难恢复方案。

CSI

Container Storage Interface(CSI)是一组 gRPC 接口。Kubernetes、Nomad 等编排系统通过同一套接口调用独立发布的存储驱动,驱动无需编译进 Kubernetes 二进制。接口定义由 Container Storage Interface Specification 维护。

CSI 服务

一个 CSI Driver 实现 Identity、Node,以及需要时实现 Controller Service:

Service 运行位置 主要 RPC
Identity Controller 和 Node Driver 都可提供 GetPluginInfo 返回驱动名称和版本,GetPluginCapabilities 声明能力,Probe 检查服务是否可用
Controller 控制面组件 CreateVolume、DeleteVolume、ControllerPublishVolume、ControllerUnpublishVolume、ControllerExpandVolume、Snapshot 和容量查询等操作
Node 每台存储节点 NodeGetInfo、NodeStageVolume、NodePublishVolume、卸载、节点扩容和卷状态查询

Controller Service 整体是可选能力。只处理节点本地临时卷的驱动可以只实现 Identity 和 Node;需要动态供给、Attach、Snapshot 或控制面扩容时,再实现对应的 Controller RPC,并通过 capability 明确声明。

部署结构

Kubernetes 中的 CSI Driver 通常分为 Controller Component 和 Node Component。Controller Component 以 Deployment 或 StatefulSet 运行;Node Component 以 DaemonSet 覆盖需要使用存储的 Node。Deploying a CSI Driver on Kubernetes 给出了两类组件的通信和挂载要求。

Controller sidecar 与 CSI Driver 容器通过同一 Pod 中共享的 Unix socket 通信。它们观察 Kubernetes 对象,把对象变化转换为 Controller RPC,再把返回结果写回 API Server。

CSI Controller sidecar 观察 Kubernetes API,并通过 Unix socket 调用 CSI Controller Service

图:CSI Controller Component 的 API 与 socket 通信。来源:Kubernetes CSI Docs sidecar-container.png,Apache-2.0。

Node Component 使用两条不同的 socket:

  • Driver socket:Kubelet 调用 CSI Node Service。
  • Registration socket:node-driver-registrar 把驱动名称、socket 路径和支持的版本注册到 Kubelet Plugin Manager。

Node Driver 需要访问宿主机设备和 /var/lib/kubelet 下的挂载目录。它在容器内创建的 mount 必须传播回宿主机,因此对应 HostPath 通常使用 Bidirectional mount propagation,并以 privileged container 运行。

node-driver-registrar 通过 registration socket 注册驱动,Kubelet 通过 driver socket 调用 CSI Node Service

图:Kubelet 与 CSI Node Component 的两条 socket。来源:Kubernetes CSI Docs kubelet.png,Apache-2.0。

CSI API 对象

CSI Driver 和 Kubernetes 通过一组 storage.k8s.io/v1 对象交换能力、拓扑和 Attach 状态:

对象 写入方 用途
CSIDriver 驱动安装清单 声明 attachRequired、podInfoOnMount、fsGroupPolicy、生命周期模式和容量跟踪等 Kubernetes 行为
CSINode Kubelet 的 CSI registration 过程 记录一台 Node 已注册的驱动、Node ID、拓扑键和可挂载卷数量
CSIStorageCapacity external-provisioner 记录某个 StorageClass 在一个拓扑 segment 中可供给的容量,供 Scheduler 过滤 Node
VolumeAttachment Attach/Detach Controller 表示一块 PV 应 Attach 到某台 Node;external-attacher 根据它调用 Controller Publish/Unpublish

StorageClass、PVC 和 PV 属于通用 PersistentVolume API;上表对象负责补充 CSI 驱动能力和运行状态。完整字段见 Kubernetes Storage API。

Sidecar 容器

sidecar 是 Kubernetes CSI 集成代码,不属于 CSI Driver 的业务数据面。每个 sidecar 观察特定 API 对象,再调用驱动 socket 中的 Controller 或 Identity RPC。驱动只部署它实际支持的功能。

Sidecar 观察的对象或入口 调用与结果
external-provisioner PVC、PV、StorageClass、CSIStorageCapacity 调用 CreateVolume / DeleteVolume,创建 PV,并可发布容量信息
external-attacher VolumeAttachment 调用 ControllerPublishVolume / ControllerUnpublishVolume,更新 Attach 状态
external-resizer PVC 容量或 VolumeAttributesClass 变化 调用 Controller 扩容或 ControllerModifyVolume,更新 PVC/PV 状态
external-snapshotter VolumeSnapshotContent 调用 CreateSnapshot / DeleteSnapshot;集群还需独立安装 Snapshot CRD 和 snapshot-controller
node-driver-registrar Kubelet Plugin Manager registration socket 调用 GetPluginInfo 取得驱动名,并向 Kubelet 注册 Driver socket
livenessprobe HTTP health endpoint 周期调用 Probe,把 CSI Driver 状态提供给 Kubernetes liveness probe
external-health-monitor-controller PVC、Pod 与 CSI volume health 调用 ControllerGetVolume,把异常状态写成 PVC Event;是否部署由驱动能力决定

各 sidecar 的版本兼容矩阵、RBAC 和参数以 Kubernetes CSI Sidecar Containers 为准。

CSI 卷生命周期

动态供给的 CSI 卷从 PVC 创建到进入容器,需要经过供给、节点选择、Attach、节点挂载和容器挂载。实际执行的 RPC 取决于驱动声明的能力:attachRequired: false 会跳过 Attach,未声明 STAGE_UNSTAGE_VOLUME 的驱动会跳过 NodeStage/NodeUnstage。

下图展示了 WaitForFirstConsumer StorageClass 从 PVC 到容器挂载的主路径,其中 Attach 和 staging 按驱动能力执行。

sequenceDiagram
    autonumber
    participant U as User
    participant A as API Server
    participant S as Scheduler VolumeBinding
    participant P as external-provisioner
    participant C as CSI Controller
    participant D as Attach/Detach Controller
    participant T as external-attacher
    participant K as Kubelet Volume Manager
    participant N as CSI Node
    participant R as Container Runtime

    U->>A: Create PVC and Pod
    S->>A: Read Pod, PVC, PV, StorageClass, Node
    S->>A: Set PVC selected-node
    P->>C: CreateVolume(capacity, topology)
    C-->>P: volume_id and topology
    P->>A: Create PV pre-bound to PVC
    S->>A: Bind Pod to Node
    opt CSIDriver attachRequired=true
        D->>A: Create VolumeAttachment
        T->>C: ControllerPublishVolume
        T->>A: Mark VolumeAttachment attached
    end
    opt Driver supports staging
        K->>N: NodeStageVolume
    end
    K->>N: NodePublishVolume
    K->>R: CreateContainer with volume mounts
    R-->>U: Process sees mounted data

动态供给

PVC 指定的 StorageClass 使用 CSI provisioner 时,external-provisioner 负责把 PVC 转成 CreateVolume 请求。请求包含容量范围、Volume Capability、StorageClass 参数、数据源和可接受的拓扑。驱动返回 volume_id、实际容量、volume context 和可访问拓扑。

external-provisioner 使用返回结果创建 PV,并通过 claimRef 预绑定到原 PVC。PersistentVolume Controller 检查 PV 与 PVC 的容量、访问模式、Volume Mode 和 StorageClass,完成双向绑定并更新两边的状态。PVC Bound 表示控制面已经选定 PV;它不表示卷已经 Attach、挂载或进入容器。

节点选择

WaitForFirstConsumer 让 Scheduler 的 VolumeBinding 插件参与供给位置选择:

  • 已绑定 PVC:检查 PV 的 nodeAffinity 是否与候选 Node 匹配。
  • 未绑定 PVC:寻找与 PVC 匹配的现有 PV,或确认 StorageClass 可以在候选 Node 的拓扑中动态供给。
  • 容量跟踪:驱动的 CSIDriver.spec.storageCapacity 为 true 时,Scheduler 使用 CSIStorageCapacity 排除容量不足的拓扑。
  • 选定 Node:PreBind 阶段为待供给 PVC 写入 volume.kubernetes.io/selected-node,并等待供给和绑定完成。

GPU Pod 的 Node affinity、taint、GPU request 和存储拓扑都在 Scheduler 的同一轮 Filter 中生效。VolumeBinding 的扩展点和 Scheduler 主循环见 Scheduler:Pod 的节点选择。

Attach

块存储、云盘等后端通常需要先把卷连接到目标 Node。Attach/Detach Controller 创建 VolumeAttachment,external-attacher 调用 ControllerPublishVolume,并把驱动返回的 publish_context 写回 VolumeAttachment。Kubelet 在节点挂载前等待 status.attached: true,再把 publish_context 传给 Node Service。

NFS、CephFS、JuiceFS 等网络文件系统通常不需要控制面 Attach。驱动通过 CSIDriver.spec.attachRequired: false 明确这一点后,Kubernetes 跳过 VolumeAttachment 和 ControllerPublish/Unpublish。是否跳过 Attach 由 CSIDriver 声明,不由 PVC 的 RWO 或 RWX 直接决定。Skip Attach 给出了配置和兼容要求。

节点挂载

Kubelet Volume Manager 观察已经分配到本节点的 Pod,并让实际挂载状态逐步接近期望状态。CSI 文件系统卷通常经过两层路径:

  • NodeStageVolume:把卷准备并挂到 Node 级 staging 目录。驱动可以在这里格式化空白块设备、挂载文件系统或建立全节点共享的底层连接。
  • NodePublishVolume:把已经 stage 的卷发布到该 Pod 专属的目录;未使用 staging 的驱动直接从后端发布到 Pod 目录。

Kubelet 在 NodePublishVolume 成功后生成容器配置,把 Pod 目录作为 mount source 交给 CRI。containerd 创建容器时将这个 mount 写入 OCI Runtime Spec,容器进程最终在 mountPath 看到数据。容器创建过程见 容器运行时:从 CRI 到容器进程。

卸载与回收

Pod 结束后,节点和控制面的清理顺序与挂载过程相反:

  1. Kubelet 调用 NodeUnpublishVolume,移除 Pod 专属挂载。
  2. 本节点不再有 Pod 使用该卷时,支持 staging 的驱动执行 NodeUnstageVolume。
  3. 需要 Attach 的驱动由 external-attacher 调用 ControllerUnpublishVolume,随后删除 VolumeAttachment。
  4. PVC 删除后,PersistentVolume Controller 根据 PV reclaim policy 处理卷。Delete 策略由 external-provisioner 调用 DeleteVolume;Retain 保留后端数据和人工处理入口。

Pod 删除只触发节点卸载,不等于删除 PVC。Deployment 替换 Pod 时,同一 PVC 可以重新挂到新 Pod;数据能否跨 Node 使用取决于后端、访问模式和拓扑。

核心源码

Kubernetes v1.36.4 的存储主路径分布在 PersistentVolume Controller、Scheduler VolumeBinding、Attach/Detach Controller、Kubelet Volume Manager 和 in-tree CSI volume plugin 中。CSI sidecar 与 Driver 位于独立仓库,Kubernetes 核心通过 API 对象和 Unix socket 与它们协作。

PersistentVolume Controller

PersistentVolume Controller 维护 PV/PVC 的匹配、绑定和回收。syncClaim 根据 PVC 的绑定状态进入不同处理路径;syncUnboundClaim 查找容量、StorageClass、Volume Mode 和 Access Mode 都匹配的 PV。没有现成 PV 时,它根据 StorageClass 和绑定模式选择等待 Scheduler 或进入供给流程。

CSI 动态供给由外置 provisioner 完成。provisionClaimOperationExternal 在 PVC 上记录 provisioner,external-provisioner 随后观察 PVC、连接 CSI Controller Service 并调用 CreateVolume。

PV 出现后,bind 依次写入 PV claimRef、PV 状态、PVC spec.volumeName 和 PVC 状态。多个可重试的 API 请求分别更新 PV 与 PVC,因此两个对象可能短暂处在不同进度。PVC 删除后,reclaimVolume 根据 reclaim policy 进入保留或删除流程。

VolumeBinding

Scheduler 的 VolumeBinding 插件把卷约束接入 Scheduling Framework:

  • PreFilter:收集 Pod 引用的 PVC,把延迟绑定 PVC 写入当前 CycleState。尚未绑定的 Immediate PVC 会让 Pod 暂时无法调度。
  • Filter:逐 Node 调用 FindPodVolumes,检查已绑定 PV 的 Node Affinity、可用静态 PV、动态供给拓扑和 CSIStorageCapacity。
  • Reserve:在 Scheduler 本地 PV/PVC cache 中假定选择结果;动态供给 PVC 的本地副本在这里得到 selected-node。
  • PreBind:调用 BindPodVolumes 把假定结果写入 API,并等待 PV Controller 或 external-provisioner 完成绑定。
  • Unreserve:后续绑定失败时撤销 Scheduler 本地假定状态。

VolumeBinding 选择可以同时满足 Pod 和卷约束的 Node。CreateVolume 仍由 external-provisioner 发起,Attach 和节点挂载也在后续组件中完成。

Attach/Detach Controller

Attach/Detach Controller 为全部节点维护一套 Desired State of World(DSW)和 Actual State of World(ASW)。findAndAddActivePods 把已调度 Pod 需要的可 Attach 卷加入 DSW;attachDesiredVolumes 比较 DSW 与 ASW,并发起缺少的 Attach 操作。

对于 CSI 卷,csiAttacher.Attach 创建 (driver, volumeHandle, node) 对应的 VolumeAttachment,并等待 status.attached。external-attacher 观察这个对象,调用 CSI ControllerPublishVolume,再更新状态。Core 中的 csiAttacher.Attach 只负责 VolumeAttachment,不直接发送 Controller RPC。

Pod 不再使用该卷后,reconciler.reconcile 等待 Node 报告卷已经卸载,再进入 Detach;csiAttacher.Detach 删除 VolumeAttachment,external-attacher 根据删除事件调用 ControllerUnpublishVolume。

Kubelet Volume Manager

NewVolumeManager 在每个 Kubelet 中组装四个主要部分:

  • Desired State of World:记录本节点的 Pod 应使用哪些卷。processPodVolumes 从 Pod、PVC 和 PV 生成期望状态。
  • Actual State of World:记录 Kubelet 已知的 Attach、全局 staging 和 Pod mount 状态。
  • Reconciler:reconcile 周期比较两份状态,依次发起卸载、挂载和设备清理。
  • OperationExecutor:MountVolume 与 UnmountVolume 使用带去重和退避的异步执行器运行卷操作。

GenerateMountVolumeFunc 展开一次文件系统卷挂载:等待 Attach,按驱动能力执行全局 device mount,再执行 Pod 专属 Mounter.SetUp,最后把挂载结果写入 ASW。Attach/Detach Controller 和 Kubelet 各自维护 DSW/ASW;前者面向集群中的 Node attach,后者面向本节点的 stage 和 Pod mount。

CSI Volume Plugin

pkg/volume/csi 是编译进 Kubernetes 的 CSI 适配层,具体存储驱动作为独立进程运行。ProbeVolumePlugins 把 CSI plugin 和 RegistrationHandler 注册到通用 Volume Plugin Manager。Kubelet Plugin Manager 收到 registration socket 后,RegisterPlugin 保存 Driver socket,调用 NodeGetInfo,并更新 CSINode 与 Node topology 信息。

Node 侧 RPC 由通用 Volume Plugin 操作转换而来:

  • NodeStageVolume:csiAttacher.MountDevice 检查 STAGE_UNSTAGE_VOLUME capability。驱动支持时准备全局 staging path,并由 NodeStageVolume 调用 Node Service;不支持时跳过。
  • NodePublishVolume:csiMountMgr.SetUpAt 读取 CSIDriver、Secret、publish context 和 staging path,再把卷发布到 Pod 专属目录。NodePublishVolume 负责组装并发送 CSI 请求。
  • 卸载:TearDownAt 调用 NodeUnpublishVolume;UnmountDevice 在需要时调用 NodeUnstageVolume。

Linux 默认的 Pod target path 形如 /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<volume-name>/mount。实际目录名经过 qualified-name 转义,应从 Kubelet 日志或节点文件系统读取,不根据 PVC 名手工拼接。

容器挂载

Kubelet.SyncPod 在创建业务容器前调用 WaitForAttachAndMount。卷没有进入 ASW mounted 状态时,Kubelet 保持 Pod 等待,并通过 Event 报告未 Attach 或未 Mount 的卷。

卷准备完成后,路径按以下调用链进入 CRI:

  1. GenerateRunContainerOptions 从 Volume Manager 取得已挂载卷。
  2. makeMounts 把 NodePublish 的 host path 与 PodSpec 中的 mountPath 组合成 kubecontainer.Mount。
  3. generateContainerConfig 转换为 CRI ContainerConfig.Mounts。
  4. startContainer 调用 runtimeService.CreateContainer。

CSI Driver 只负责把卷发布到 Node 上的 Pod target path。容器内 mountPath 来自 PodSpec,Kubelet 和容器运行时负责最后一段映射。

CSI 能力

CSI Driver 通过 capability 表明自己支持哪些 RPC,Kubernetes 再通过 sidecar、CSIDriver 和 StorageClass 启用相应功能。StorageClass 中出现某个字段,不代表后端一定支持该功能;驱动 capability、sidecar 版本和后端能力必须同时满足。

卷扩容

StorageClass 设置 allowVolumeExpansion: true 后,用户可以增大 PVC 的 resources.requests.storage。external-resizer 调用 ControllerExpandVolume 扩大后端卷;需要节点侧处理时,Kubelet 再调用 NodeExpandVolume 扩展文件系统。Kubernetes 只支持扩容,不支持把 PVC 缩小。驱动还需要声明 EXPAND_VOLUME capability。

VolumeAttributesClass

VolumeAttributesClass 为 CSI 卷定义可修改的属性,例如 IOPS、吞吐或后端服务等级。PVC 可以在创建时引用一个 class,也可以在运行期间把 volumeAttributesClassName 改成另一个 class;external-resizer 调用 CSI ControllerModifyVolume 完成修改。

Kubernetes v1.36 使用稳定的 storage.k8s.io/v1 API。该功能只适用于实现 ModifyVolume 的 CSI Driver,class 中的参数含义由具体驱动定义。Volume Attributes Classes 给出了状态字段和修改流程。

Snapshot

VolumeSnapshot API 由 VolumeSnapshot、VolumeSnapshotContent 和 VolumeSnapshotClass 三个 CRD 组成。snapshot-controller 处理跨驱动的对象绑定,部署在 CSI Controller Component 中的 external-snapshotter 调用驱动的 CreateSnapshot 和 DeleteSnapshot。

Snapshot 记录某一时点的卷内容,是否应用一致、是否跨可用区复制、是否能作为灾备副本由存储系统决定。数据库在拍摄 Snapshot 前通常还需要执行 flush、freeze 或应用级协调。详细对象关系见 Volume Snapshots。

Clone 与 Volume Populator

PVC 的 dataSource 可以引用同一 Namespace 中的另一个 PVC,要求 CSI Driver 支持 CLONE_VOLUME。动态供给时,external-provisioner 把源卷 ID 放入 CreateVolumeRequest.volume_content_source,新 PVC 获得一块独立卷。

Volume Populator 把自定义资源作为数据源,例如把对象存储中的数据集导入新卷。Kubernetes 使用 dataSourceRef 保留自定义 Group/Kind;populator controller 负责准备临时 PVC 和填充数据。它解决初始化流程,不改变卷后续的读写语义。

临时 CSI 卷

CSI 支持两类跟随 Pod 生命周期的用法:

  • Generic Ephemeral Volume:Pod 中的 ephemeral.volumeClaimTemplate 动态创建普通 PVC,因而可以使用供给、容量跟踪、Snapshot 和扩容等 PersistentVolume 能力。
  • CSI Ephemeral Volume:直接在 Pod 中声明 csi volume,Kubelet 调用 NodePublishVolume;驱动自行管理随 Pod 创建和删除的后端内容,不创建 PV/PVC。

两者都适合 scratch 或短期工作目录。需要在 Pod 删除后保留的数据仍应使用独立 PVC。

容量跟踪与卷健康

开启 storage capacity tracking 后,external-provisioner 根据 CSI GetCapacity 创建 CSIStorageCapacity。Scheduler 在 WaitForFirstConsumer 流程中比较 PVC 容量和候选拓扑,避免先选中无法供给卷的 Node。

Volume health monitoring 分成 Controller 和 Node 两条路径。Controller health monitor 可以把异常状态写成 PVC Event;Kubelet 调用 NodeGetVolumeStats 时,也可以把异常状态记录到 PVC。两者都要求 Driver 支持相应 capability,且不能替代后端自身的告警系统。

FSGroup

Pod 的 securityContext.fsGroup 用于让非 root 进程访问卷。CSIDriver.spec.fsGroupPolicy 决定 Kubelet 是否递归修改文件属组;驱动支持 VOLUME_MOUNT_GROUP 时,Kubelet 把 fsGroup 交给 NodeStageVolume 或 NodePublishVolume,由驱动在挂载时处理。大目录递归 chown 会显著延长 Pod 启动时间,生产环境需要根据驱动和文件系统选择策略。

Kubernetes 存储方案

存储方案需要按所在层级比较。CSI Driver 负责 Kubernetes 接入;Ceph、JuiceFS、NFS 等后端保存数据;Rook 和 Piraeus Operator 负责部署与运维;Alluxio 和 Fluid 负责缓存或数据编排。把这些项目放在一张“存储产品”表中直接比较,会混淆应用接口和故障边界。

云存储

  • AWS EBS CSI Driver 把 Availability Zone 内的 EBS 块设备映射为 PV,提供动态供给、Attach、Snapshot 和扩容。拓扑卷通常配合 WaitForFirstConsumer;安装与能力见 AWS EBS CSI Driver。
  • AWS EFS CSI Driver 把 EFS 文件系统映射为共享文件卷。动态供给通过 EFS Access Point 为 PVC 创建独立入口,适合 RWX;Driver 不改变 EFS 本身的吞吐和一致性模型。AWS EFS CSI Driver 记录了 Access Point 和配额限制。
  • Azure Disk CSI Driver 提供 Azure Managed Disk 块卷,GCE PD CSI Driver 提供 Google Compute Engine Persistent Disk。托管 Kubernetes 通常由平台安装和升级 Driver,自建集群则要单独管理版本、IAM 与节点组件。

节点本地存储

  • Local PV 使用 Kubernetes 内置 local volume,把节点上的磁盘、分区或目录声明为静态 PV。PV 必须带 nodeAffinity,数据和节点故障边界相同;Local Volume 不提供动态供给。
  • TopoLVM 通过 CSI 在选定 Node 的 LVM Volume Group 中动态创建 Logical Volume,并把容量信息提供给 Scheduler。它改善本地盘的供给和容量管理,仍不提供跨 Node 副本。TopoLVM 的兼容矩阵当前只列到 Kubernetes 1.35,在 1.36 集群使用前需要单独验证。
  • OpenEBS LocalPV 包含不同实现。Hostpath LocalPV 使用外部 provisioner、不是 CSI;LVM LocalPV 和 ZFS LocalPV 使用 CSI 创建 LV、ZFS Dataset 或 Zvol。三种实现都把数据保存在选定 Node。

复制块存储

  • Longhorn 把块卷复制到多个 Kubernetes Node,并通过 CSI 提供 PV。它的 RWX 模式在块卷之上增加 Share Manager 和 NFS 访问层;底层数据引擎仍是复制块存储。Longhorn Documentation 给出了副本重建、备份和故障处理流程。
  • OpenEBS Replicated PV Mayastor 在多个 Node 保存同步块副本,并通过 CSI 和 NVMe-oF 等组件连接使用卷的 Node。它与 OpenEBS LocalPV 的故障边界不同,具体数据路径见 Replicated Storage。
  • Piraeus Datastore 组合 Piraeus Operator、LINSTOR、LINSTOR CSI 与 DRBD。Operator 部署组件,LINSTOR 管理卷和副本放置,DRBD 同步块数据,CSI 负责 PV 接入;应用 I/O 不经过 Operator。Piraeus Operator 和 LINSTOR CSI 分别记录管理面与接入层。

分布式与共享文件系统

  • CephFS 通过 Ceph CSI 提供共享 POSIX 文件卷,Rook 可以负责部署和维护 Ceph。Ceph RBD、CephFS 与 RGW 的差别在下一节展开。
  • JuiceFS 使用独立元数据引擎和对象存储保存文件系统状态与数据,并通过 FUSE/CSI 向 Pod 提供共享 POSIX 文件接口。它与 Mount Pod、缓存的关系在后文单独说明。
  • NFS CSI Driver 把已有 NFSv3/NFSv4 Server 挂成 PV,也可以在 Export 下为 PVC 创建子目录。Driver 不安装或维护 NFS Server;NFS CSI Driver 只负责 Kubernetes 接入。
  • Lustre 的 Kubernetes 能力取决于具体发行版或云服务,例如 AWS FSx for Lustre CSI Driver 和 Google Managed Lustre CSI Driver。某个 Driver 的动态供给能力不能外推到所有 Lustre 集群。
  • BeeGFS CSI Driver 把已有 BeeGFS 集群映射为 PV,并可动态创建远端目录;它不部署 BeeGFS Server。BeeGFS CSI Driver 当前兼容矩阵只列到 Kubernetes 1.34,1.36 环境需要实测。
  • 3FS 面向 AI 训练和推理提供基于 SSD、RDMA 的分布式文件系统,应用通过 FUSE 或 USRBIO 访问。官方 DeepSeek 3FS 主仓库没有 CSI Driver 或 StorageClass 接入,不能把它写成可以直接申请 PVC 的项目。

对象存储

Ceph RGW、RustFS 和历史上的 MinIO 向应用提供 S3 API。对象存储 Server 可以使用 PVC 保存自己的数据,业务应用则通过 Endpoint、Bucket 和凭据访问对象。需要 POSIX 文件接口时,还要使用文件系统或专门的访问层。

  • Ceph RGW 与 RBD、CephFS 共用 Ceph 集群能力,但对外接口是 S3/Swift HTTP API。
  • RustFS 是 Rust 实现的 S3 兼容对象存储。RustFS Kubernetes 安装文档 使用 Helm 和 PVC 部署 Server;其官方仓库当前仍把 distributed mode 标为 under testing,生产使用应以实际兼容性和故障测试为准。
  • MinIO 的开源 Server 曾是常见 S3 方案,但官方仓库已于 2026-04-25 归档。新集群选型不应再把它列为持续维护的默认方案。

缓存与数据编排

Alluxio 位于计算与持久存储之间,使用内存、SSD 或磁盘缓存远端数据;Fluid 通过 Dataset 和 Runtime CRD 在 Kubernetes 中部署并管理 Alluxio、JuiceFS 等 Runtime。两者可以缩短数据到计算节点的路径,但不能替代底层持久存储的副本、备份和恢复。

Rook 与 Ceph

Rook 是 Kubernetes Operator,Ceph 是存储系统,Ceph CSI 是 PV/PVC 接入层。Rook 根据 CephCluster、CephBlockPool、CephFilesystem 和 CephObjectStore 等 CRD 部署并维护 MON、MGR、OSD、MDS 与 RGW;应用 I/O 直接进入 Ceph 数据服务,不经过 Rook Operator。

下图展示了管理面、CSI 接入和三类 Ceph 数据服务。

Rook Operator 管理 Ceph 集群,应用分别通过 Ceph CSI 使用 RBD 和 CephFS,或通过 S3 API 使用 RGW

图:Rook 与 Ceph 的高层架构。来源:Rook High-Level Architecture,CC BY 4.0。

Ceph 的三类接口处理不同工作负载:

  • RBD:把 Ceph image 暴露为块设备,通过 RBD CSI 创建 PV。常用于 RWO 数据库、队列和需要块设备语义的 Checkpoint 卷。
  • CephFS:MDS 管理文件系统元数据,客户端直接与 Ceph 集群读写数据,通过 CephFS CSI 提供 RWX 共享文件卷。
  • RGW:通过 S3/Swift API 访问 Bucket,适合对象、归档和数据交换。它不提供普通 volumeMount 文件语义。

Rook 当前的环境要求覆盖 Kubernetes 1.31–1.36;Ceph CSI 当前测试矩阵也包含 Kubernetes 1.36。生产部署还需根据 Ceph release、内核、磁盘类型和故障域选择兼容组合。

JuiceFS

JuiceFS 向应用提供 POSIX 文件系统,并把元数据与文件数据分开保存。目录、inode、文件属性和 chunk 索引写入 Redis、TiKV、MySQL 等 Metadata Engine;文件内容切分后写入 S3、Ceph RGW 等对象存储。Pod 看到的是文件接口,不是对象存储 API。

JuiceFS CSI Driver 的默认 Mount Pod 模式在每个 Node 为卷创建或复用 JuiceFS Client。CSI Node Service 建立宿主机 FUSE mount,再把该路径发布给应用 Pod;同一 Node 上使用同一卷的 Pod 可以共享 Mount Pod 和缓存。

JuiceFS CSI Driver 通过 Mount Pod、FUSE、元数据引擎和对象存储向应用 Pod 提供文件系统

图:JuiceFS CSI Driver 架构。来源:JuiceFS CSI Driver csi-driver-architecture.svg,Apache-2.0。

JuiceFS 适合多个 Pod 或多个 Node 共享模型、训练数据和 Checkpoint。其性能边界不只由对象存储带宽决定,还包括 Metadata Engine、对象请求延迟、FUSE 开销、客户端缓存容量和小文件数量。JuiceFS Community Architecture 解释了元数据与对象数据布局,JuiceFS CSI Driver 说明了 Mount Pod、静态供给和动态供给模式。

客户端本地 block cache 可以减少重复对象读取,但它属于 Node 上的可回收缓存。模型版本、缓存预热、容量上限和淘汰策略仍需单独设计,不能把“使用 JuiceFS PVC”直接等同于“模型已经位于本地 NVMe”。

Alluxio 与 Fluid

Alluxio 是计算框架与 Under File System(UFS)之间的数据访问层。Master 管理命名空间和元数据,Worker 使用内存、SSD 或磁盘缓存数据,持久副本仍由 S3、HDFS、Ceph 等 UFS 保存。

应用通过 Alluxio 数据访问与缓存层读取底层持久存储

图:Alluxio 位于计算与持久存储之间。来源:Alluxio Architecture,Apache-2.0。

Fluid 使用 Dataset 描述逻辑数据,使用 AlluxioRuntime、JuiceFSRuntime 等 Runtime 描述缓存和访问引擎。Operator 与 Runtime Controller 管理 Runtime 的部署、扩缩容和预热,CSI Plugin 或 Sidecar 把数据交给应用 Pod。Fluid 本身不保存持久数据。

Fluid 控制面管理 Dataset 和 Runtime,数据面通过 Runtime Plugin 与 CSI Plugin 向应用提供数据

图:Fluid 的控制面和数据面。来源:Fluid Architecture,Apache-2.0。

Alluxio 与 Fluid 适合远端数据反复读取、计算节点有可用缓存介质、且缓存命中能明显缩短作业准备时间的场景。缓存层失效后,数据应能从 UFS 重新读取;Checkpoint 等唯一持久副本不能只写入 Worker cache。具体组件关系见 Alluxio Architecture 和 Fluid Architecture and Concepts。

AI 工作负载的数据路径

AI Pod 启动时至少涉及四条不同的数据路径。它们使用不同的协议、缓存键和生命周期,不能合并成一个“模型与镜像缓存”。

flowchart LR
    Registry["OCI Registry"] --> Containerd["containerd image pull"]
    Containerd --> Snapshotter["image snapshot / container rootfs"]

    Source["model or dataset source"] --> Shared["shared file system or data cache"]
    Shared --> Mount["CSI volume mount"]
    Mount --> Process["training or inference process"]

    Process --> Checkpoint["checkpoint / output PVC"]
    Checkpoint --> Durable["durable block, file, or object storage"]

    Shared --> Local["node-local NVMe cache"]
    Local --> Process
  • 容器镜像:Kubelet 请求 containerd 从 OCI Registry 拉取 manifest 和 layer,Snapshotter 生成容器 rootfs。PVC、CSI 和模型文件系统不在这条路径上。
  • 模型与训练数据:应用从 PVC 挂载的共享文件系统读取,或者由应用从 Hugging Face、对象存储等来源下载到 PVC。Alluxio、JuiceFS block cache 等缓存位于这条数据路径。
  • Checkpoint 与输出:训练进程持续写入,需要明确持久性、原子提交、Snapshot 和恢复边界。它与可重新生成的只读模型缓存应使用不同回收和保护策略。
  • 节点本地缓存与 scratch:Local PV、Generic Ephemeral Volume 或 emptyDir 可以利用本地 NVMe,吞吐和延迟较低;Node 丢失后应允许从共享存储或对象源重建。

推理场景还要把“Pod 已运行”和“模型已就绪”分开测量。PVC Bound、容器 Running 都不能证明模型文件已经进入页缓存、固定内存或 GPU。端到端指标应分别记录卷挂载、模型读取、反序列化、CPU 到 GPU 传输和引擎初始化时间。

存储选型

选型应先确定应用接口和故障边界,再比较项目。容量和峰值吞吐只是其中两项。

工作负载 需要的语义 常见起点 主要验证项
单 Pod 临时预处理 Node 本地文件、允许重建 emptyDir、Local PV、TopoLVM Node 丢失后的重跑、磁盘压力和容量隔离
单写数据库或状态服务 RWO/RWOP、低延迟、Snapshot 云块盘、Longhorn、RBD、Piraeus 写入延迟、故障切换、fencing、Snapshot 一致性
多 Node 共享训练数据 RWX POSIX、大吞吐和高元数据并发 CephFS、JuiceFS、NFS、Lustre、BeeGFS 聚合吞吐、小文件、目录扫描、客户端数量和故障恢复
大规模只读模型分发 版本化数据、并发读取、本地缓存 JuiceFS、Alluxio/Fluid、CephFS、对象存储加客户端缓存 冷启动、缓存命中、Range 读取、校验和、淘汰与预热
Checkpoint 与训练输出 持久写入、恢复、生命周期管理 分布式文件、块卷、对象存储导出流程 原子提交、增量写、Snapshot、异地备份和恢复时间
归档与数据交换 Bucket、对象版本和生命周期 Ceph RGW、RustFS、云对象存储 S3 API 兼容、multipart、IAM、跨区域复制和删除保护

比较候选方案时,应记录以下约束:

  • 接口:应用需要 Block、POSIX File 还是 S3 Object;是否必须 RWX。
  • 拓扑:卷能否跨 Node 或可用区访问,Scheduler 需要哪些 topology key。
  • 故障域:数据只在一台 Node、一个可用区,还是跨节点复制;故障后由谁恢复。
  • 一致性与并发:文件锁、rename、fsync、对象覆盖和多写者语义是否满足应用。
  • 性能形态:顺序吞吐、随机 IOPS、小文件元数据、并发客户端和冷缓存分别测试。
  • 数据保护:Snapshot 是否应用一致,备份是否独立于主集群,恢复时间是否经过演练。
  • 运维接口:容量扩展、升级、监控、配额、密钥轮换和故障诊断是否有明确责任人。

实验:观察 CSI 卷生命周期

实验使用普通两节点 Kind 集群和 CSI Hostpath Driver v1.18.0。该 Driver 是 Kubernetes CSI 项目的测试实现,后端数据位于 Kind Node 容器的本地目录;它能验证对象、RPC 和挂载路径,不能代表生产存储的副本、故障恢复和性能。

实验清单使用 WaitForFirstConsumer:PVC 创建后等待 Pod,Scheduler 选出 worker,external-provisioner 调用 CreateVolume,Kubelet 执行 NodeStage/NodePublish。删除 Pod 和 PVC 后,脚本继续验证 NodeUnpublish、NodeUnstage 和 DeleteVolume。

examples/chapter-01/09-storage/storage-demo.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: storage-demo-hostpath
provisioner: hostpath.csi.k8s.io
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
parameters:
  kind: fast
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: storage-demo
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: storage-demo-hostpath
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: storage-demo-writer
  namespace: default
spec:
  containers:
    - name: writer
      image: busybox:1.36.1
      command:
        - sh
        - -c
        - |
          printf 'written through CSI\n' > /data/marker
          sleep 3600
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: storage-demo

从仓库根目录执行验证脚本。脚本会创建名为 storage-demo 的 Kind 集群;同名集群已存在时会直接报错,避免修改现有环境。

./examples/chapter-01/09-storage/verify-kind.sh

脚本检查以下状态:

  • Kubernetes Server 为 v1.36.4。
  • hostpath.csi.k8s.io 设置 attachRequired: false,因此实验中没有 VolumeAttachment。
  • PVC 与 PV 进入 Bound,PV 记录 CSI driver 和 volume handle。
  • Pod 进入 Running,容器可以读出通过 CSI 卷写入的 marker。
  • Driver 日志出现 Create、Stage、Publish、Unpublish、Unstage 和 Delete RPC。
  • PVC 删除后,Delete reclaim policy 使 PV 与后端卷一起删除。

完整命令、实测输出、对象解释和清理方法见 examples/chapter-01/09-storage/README.md。实验结束后删除集群:

./examples/chapter-01/09-storage/cleanup-kind.sh

相关资料