存储: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。存储后端可以提供更大的卷,但不能以更小的卷满足该请求。
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 调用 CSIDeleteVolume删除后端卷。适合可重新生成的数据和已经由其他系统完成保护的数据。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 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 运行。

图: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 结束后,节点和控制面的清理顺序与挂载过程相反:
- Kubelet 调用
NodeUnpublishVolume,移除 Pod 专属挂载。 - 本节点不再有 Pod 使用该卷时,支持 staging 的驱动执行
NodeUnstageVolume。 - 需要 Attach 的驱动由 external-attacher 调用
ControllerUnpublishVolume,随后删除 VolumeAttachment。 - 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。尚未绑定的ImmediatePVC 会让 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_VOLUMEcapability。驱动支持时准备全局 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:
GenerateRunContainerOptions从 Volume Manager 取得已挂载卷。makeMounts把 NodePublish 的 host path 与 PodSpec 中的mountPath组合成kubecontainer.Mount。generateContainerConfig转换为 CRIContainerConfig.Mounts。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 中声明
csivolume,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 内置
localvolume,把节点上的磁盘、分区或目录声明为静态 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 与 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 架构。来源: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 Architecture,Apache-2.0。
Fluid 使用 Dataset 描述逻辑数据,使用 AlluxioRuntime、JuiceFSRuntime 等 Runtime 描述缓存和访问引擎。Operator 与 Runtime Controller 管理 Runtime 的部署、扩缩容和预热,CSI Plugin 或 Sidecar 把数据交给应用 Pod。Fluid 本身不保存持久数据。

图: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。
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 集群;同名集群已存在时会直接报错,避免修改现有环境。
脚本检查以下状态:
- 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 删除后,
Deletereclaim policy 使 PV 与后端卷一起删除。
完整命令、实测输出、对象解释和清理方法见 examples/chapter-01/09-storage/README.md。实验结束后删除集群:
相关资料¶
- Kubernetes Volumes
- Kubernetes Persistent Volumes
- Kubernetes Storage Classes
- Kubernetes Storage API
- Container Storage Interface Specification
- Kubernetes CSI Developer Documentation
- Kubernetes CSI Drivers
- Rook Storage Architecture
- Ceph Architecture
- JuiceFS Community Architecture
- Alluxio Architecture
- Fluid Architecture and Concepts