jimyag's Blog

Kubernetes PVC 生命周期:卷供应、延迟绑定与挂载排障

PVC 已经是 Bound,Pod 却一直停在 ContainerCreating。重新创建 PVC 之前,应该先确认:Bound 到底证明了哪一步完成,容器启动还在等什么?

本次在 zj 的 kind 集群中创建了一个真实的 CSI 卷。PVC 成功绑定后,应用仍因缺少另一个 Secret 卷而无法启动。补齐 Secret,原来的 PVC 没有任何变化,Pod 就恢复了。这个对照说明:PVC 绑定成功和 Pod 所有启动条件满足,是两件事。

从申请到能读写,中间有哪些动作

PVC(PersistentVolumeClaim)表达应用对存储的申请;PV(PersistentVolume)记录可供消费的卷;StorageClass 指定动态供应所用的 provisioner 和策略。CSI(Container Storage Interface)则定义 Kubernetes 与存储驱动之间的调用接口。

对于支持相应能力的 CSI 驱动,一次使用过程可能包括下面这些阶段:

阶段要完成的事排查时对应的对象或日志
供应找到已有卷,或调用驱动创建卷PVC 事件、external-provisioner、CreateVolume
绑定把 PVC 与 PV 建立关联PVC 的 spec.volumeName、PV 的 claimRef
附加让目标节点能够使用卷VolumeAttachment、external-attacher、ControllerPublishVolume
节点准备在节点上准备卷kubelet、驱动的 NodeStageVolume
发布到容器路径把卷呈现到 Pod 使用的路径kubelet、NodePublishVolume、容器挂载

不是每一种存储驱动都会执行所有可选阶段,例如有的驱动不要求 Controller Attach。也不能把“attach”一律理解成某一种云盘挂载命令。CSI 规定接口,具体操作由驱动实现。接口定义见 CSI 规范。

动态供应时,external-provisioner 观察 PVC,调用 CSI 驱动的 CreateVolume,再把返回的卷信息写成 PV;PV 控制器协调 PVC 与 PV 的绑定。对需要附加的驱动,控制面创建 VolumeAttachment,external-attacher 据此调用 ControllerPublishVolume。节点上的 kubelet 则通过 CSI node 服务准备和发布卷。

这些组件分别观察 API 状态并推进自己的工作,PVC 的 Bound 只确认声明与卷建立了绑定关系。它没有记录每个节点上的挂载是否完成,也不检查 Pod 依赖的其他 Secret 或 ConfigMap。组件分工见 CSI 架构与 PV 生命周期。

实验环境和存储实现

实验使用 kind v0.33.0、Kubernetes v1.35.1,通过 SSH 在 Linux 主机 zj 执行。CSI 部署来自 csi-driver-host-path 仓库 v1.17.0 的 kubernetes-1.30 清单;这个清单实际运行的主驱动镜像是 hostpathplugin:v1.16.1。仓库版本、清单目录名、镜像版本和集群版本是不同对象,不能混写。

hostpath 驱动是 CSI 的测试实现。实验把消费 Pod 调度到驱动所在节点,使用它观察供应、附加和挂载行为,不把它当作跨节点共享存储或云盘性能模型。

实验创建两种 StorageClass,provisioner 都是 hostpath.csi.k8s.io,分别使用 Immediate 和 WaitForFirstConsumer。

Bound 后仍然失败:看具体卡住的卷

第一组先创建一个 64 MiB、RWO 的 PVC disk,等待它变成 Bound。消费 Pod 同时挂载两个卷:disk 和名为 missing 的 Secret。此时 Secret 故意不存在。

观察结果是:PVC 保持 Bound,Pod 已经选定节点,但事件出现 FailedMount。创建缺失的 Secret 后,Pod 变成 Ready,应用可以向 /data/proof 写入 persistent。

这个实验中的挂载故障根本不在 PVC 上。如果只看到 ContainerCreating 就删除重建 PVC,不仅没有解决缺少 Secret 的问题,还可能引入数据生命周期风险。

1
2
3
kubectl --context kind-deep-dive -n dd-storage get pvc disk -o yaml
kubectl --context kind-deep-dive -n dd-storage describe pod first
kubectl --context kind-deep-dive get volumeattachment -o yaml

读 FailedMount 时应核对报错的 volume 名称,再映射到 Pod 的 spec.volumes。只有确认故障落在 CSI 卷上,才继续沿 PV、VolumeAttachment、驱动日志向下查。

RWO 允许同一节点上的两个 Pod 使用吗

实验随后创建第二个消费 Pod,把它放在同一个节点并挂载同一 PVC。两个 Pod 都 Ready,第二个 Pod 能读取第一个 Pod 写下的 persistent。删除第一个 Pod 后,第二个 Pod 仍能读到相同内容。

因此,在本次 CSI 实现中,RWO 没有把卷限制为“只能被一个 Pod 使用”。它描述的是单节点读写访问模式。若需要把读写使用限制为一个 Pod,应区分 ReadWriteOncePod。访问模式定义见 Persistent Volumes。

接着创建 RWOP PVC,让 exclusive-one 先使用它。再创建同样指向该 PVC 的 exclusive-two,第二个 Pod 无法完成调度,证据中保存了 PodScheduled=False 和对应的 FailedScheduling 事件。

配置本次观察
RWO,同一节点,两个 Pod两个 Pod 都可运行并读取同一份文件
RWOP,已有一个 Pod 使用第二个 Pod 被阻止调度

允许两个进程访问同一个文件系统,也不表示应用允许两个实例同时写同一份数据。文件锁、事务和应用一致性协议仍由应用与存储系统承担。

WaitForFirstConsumer 为什么让 PVC 保持 Pending

对于 dd-delayed StorageClass,脚本先只创建 PVC late。它保持 Pending,事件提示等待第一个消费者。创建使用它的 Pod 后,PVC 完成绑定,Pod Ready。

这个 Pending 来自 WaitForFirstConsumer 的协调方式。scheduler 的 VolumeBinding 插件把消费者的节点约束与卷拓扑一起检查;需要动态供应时,选中节点的信息通过 PVC 的 volume.kubernetes.io/selected-node 注解交给 provisioner。卷供应和绑定完成后,Pod 才能继续绑定到节点。若没有消费者,就还没有足够的调度信息决定应在哪里供应卷。实现可对照 VolumeBinding 插件。

1
2
kubectl --context kind-deep-dive -n dd-storage describe pvc late
kubectl --context kind-deep-dive get storageclass dd-delayed -o yaml

这也解释了为什么排查调度时不能只看 CPU 和内存。节点本身有空闲资源,但卷的可访问拓扑与 Pod 的调度条件不相容,Pod 仍然无法运行。

延迟绑定怎样避免先选错存储位置

WaitForFirstConsumer 把节点选择引入卷决策:scheduler 同时考虑 Pod 的节点约束和卷的拓扑条件,选出节点后才推进绑定。否则先在某个可用区供应磁盘,随后才发现 Pod 只能运行在另一个可用区,就可能得到一个 Bound 却无法消费的卷。

在 v1.35.1 的 VolumeBinding 实现中,AssumePodVolumes 先更新调度器本地的 PV/PVC 假设缓存;BindPodVolumes 才提交 API 更新并等待绑定结果。对于需要动态供应的延迟绑定 PVC,选中节点信息通过 PVC 注解传给供应组件。把“暂定方案”和“提交并等待”分开,既让后续调度知道这份卷已经被计划使用,也避免慢速供应阻塞其他 Pod 选择节点。

  sequenceDiagram
    participant S as scheduler
    participant A as API Server
    participant V as 存储控制器
    S->>S: 结合 Pod 和卷约束选择节点
    S->>S: 暂存 PV/PVC 方案
    S->>A: 提交绑定或选中节点信息
    A-->>V: 观察到 PVC 更新
    V->>A: 推进供应和绑定状态
    A-->>S: 观察到卷绑定完成
    S->>A: 提交 Pod 到节点的绑定

这里的“存储控制器”合并表示 provisioner 与 PV 控制器的协作;节点侧挂载还在后面。调度失败时,RevertAssumedPodVolumes 撤销的是本地假设缓存,不能据此推断已经创建的外部卷也被事务性回滚。

节点侧也要避免重复操作。卷操作使用 nestedPendingOperations,按卷、Pod 和节点组合检查冲突:已有相关操作执行时不再启动冲突操作;同名操作失败后按退避条件重试。因此连续看到 FailedMount,不代表 kubelet 每个同步周期都重新并发挂载一次。检查原始错误和后续重试,比反复重建 PVC 更有用。

怎样把故障定位到具体阶段

PVC Pending 时,先看 StorageClass、访问模式、容量、供应事件和是否等待消费者。PVC Bound 后,再看 Pod 是否完成调度;已调度但未启动,则读事件中具体的镜像、配置或挂载错误。

对于需要附加的 CSI 卷,VolumeAttachment 可以帮助判断控制面的附加是否完成;节点侧挂载则要结合 kubelet 和 CSI node 服务日志。出现 Multi-Attach 类错误时,需要核对旧节点的使用状态和驱动的附加记录,不能只靠删掉 API 对象推断旧节点已经停止访问。

本次测试驱动验证了 CSI 调用与访问模式,没有模拟云盘控制面的网络故障,也没有做跨主机断电测试。它提供的是排查阶段的划分,实际存储错误需要回到所用驱动和后端确认。

实验代码与使用说明见 k8sdev/deep-dive-storage。

#Kubernetes #Kind #Troubleshooting