夜雨聆风学习资料网

ARTICLE · 1044482

k8s VolumeBinding 插件如何把存储拓扑纳入 Pod 调度

k8s VolumeBinding 插件如何把存储拓扑纳入 Pod 调度

一个 Pod 同时申请 CPU、内存和 PVC 时,真正的问题不只是“哪个节点还有资源”,还包括:这个节点能不能挂上这块盘。

例如云盘已经创建在 zone-a,Pod 被调度到 zone-b;或者 Local PV 的数据就在 worker-1 本地,Pod 却被放到了 worker-2。CPU 和内存都够,Pod 仍然无法启动。

VolumeBinding 是 kube-scheduler 的内置插件。它把 PVC/PV 的绑定关系、PV 的 nodeAffinity、StorageClass 的 volumeBindingMode 一起放进调度过程,让调度器选出的节点既满足计算资源,也满足存储位置。

前提:它主要处理通过 PVC 使用的持久卷。emptyDir、ConfigMap、Secret 这类卷不走这条存储绑定链路。

/ / /

1. 为什么“先创建卷,再调度 Pod”会出问题

先看最典型的跨可用区场景:

PVC 创建  ↓存储驱动立即创建云盘:zone-a  ↓调度器根据 CPU / 内存把 Pod 放到 zone-b  ↓云盘无法跨 zone 挂载  ↓Pod 卡在 Pending / ContainerCreating

问题的根源是顺序错了:卷的位置在不知道 Pod 最终节点前就被决定了。

本地盘场景更直接:

PV local.path=/data/appPV.nodeAffinity=worker-1Pod 被调度到 worker-2  →  worker-2 没有 /data/app 这份数据

PV 不是“集群里任意节点都能用的目录”。对 Local PV、单可用区云盘、带故障域限制的 SAN/CSI 卷来说,数据位置本身就是硬约束。

/ / /

2. 两种绑定模式:Immediate 和 WaitForFirstConsumer

控制这个时机的是 StorageClass 的 volumeBindingMode

模式
PVC 什么时候绑定/创建卷
适合什么
主要风险
`Immediate`
PVC 创建后立刻
网络存储、卷不受节点/zone 限制
卷可能先落到 Pod 不能使用的位置
`WaitForFirstConsumer`
有 Pod 消费 PVC、调度器选定合适节点后
Local PV、分区云盘、拓扑感知 CSI 存储
PVC 在没有消费者时会一直 Pending

最小 StorageClass 示例:

apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:  name: csi-topology-awareprovisioner: csi.example.iovolumeBindingMode: WaitForFirstConsumerreclaimPolicy: DeleteallowVolumeExpansion: true

WaitForFirstConsumer 常被误解成“等 Pod 已运行后再创建盘”。不是。

它的实际含义是:等调度器先找到一个同时满足 Pod 和存储条件的候选节点,再把该节点作为卷绑定或动态创建卷的拓扑依据。

所以 PVC 单独创建时可能是 Pending,这不一定是故障。没有 Pod、没有节点选择,就没有足够信息决定卷应落在哪里。

/ / /

3. VolumeBinding 在调度周期里做了什么

可以把它理解成调度器里的“存储可行性检查员”。核心链路如下:

Pod 提交(引用 PVC)        │        ▼PreFilter:读取 PVC / PV,区分已绑定、待绑定、需要动态创建的 Claim        │        ▼Filter:逐个候选节点检查        ├── 已绑定 PV 的 nodeAffinity 是否匹配?        ├── 未绑定 PVC 能否找到与该节点匹配的 PV?        └── 动态创建的卷能否按该节点的拓扑创建?        │        ▼调度器综合 CPU、内存、污点、亲和性、存储条件,选出一个节点        │        ▼Reserve:暂存该节点对应的 PV 选择,避免并发调度重复使用同一 PV        │        ▼PreBind:提交 PV/PVC 绑定;动态卷则把选中节点传递给 provisioner        │        ▼Pod 绑定到 Node,kubelet 再执行 attach / mount

这里最重要的是 Filter:它不是只看“PVC 是否 Bound”,而是把存储拓扑当成一个节点过滤条件。

例如某个 PV 有:

spec:  nodeAffinity:    required:      nodeSelectorTerms:      - matchExpressions:        - key: kubernetes.io/hostname          operator: In          values:          - worker-1

那么对这个 PVC 而言,worker-2worker-3 在 VolumeBinding 看来都不是候选节点。

/ / /

4. 静态 PV:先选节点,再找能用的 PV

静态 Local PV 的常见写法如下:

apiVersion: v1kind: PersistentVolumemetadata:  name: app-local-pv-01spec:  capacity:    storage: 20Gi  accessModes:  - ReadWriteOnce  storageClassName: local-fast  persistentVolumeReclaimPolicy: Retain  local:    path: /data/local-pv/app-01  nodeAffinity:    required:      nodeSelectorTerms:      - matchExpressions:        - key: kubernetes.io/hostname          operator: In          values:          - worker-1---apiVersion: storage.k8s.io/v1kind: StorageClassmetadata:  name: local-fastprovisioner: kubernetes.io/no-provisionervolumeBindingMode: WaitForFirstConsumer

当 Pod 引用匹配的 PVC 时,VolumeBinding 会在节点过滤阶段判断:

worker-1:有匹配容量、访问模式、StorageClass,且 PV nodeAffinity 命中 → 可行worker-2:PV 的 nodeAffinity 不命中 → 不可行

这就是 Local PV 能把 Pod 拉回数据所在节点的原因。

但也要认清边界:VolumeBinding 解决的是“正确放置”,不复制数据,也不让 Local PV 具备高可用。worker-1 故障后,数据仍在 worker-1 的本地磁盘;Pod 即使可以重建,也未必有其他节点可用。

/ / /

5. 动态卷:调度器如何把节点拓扑传给 CSI

动态卷不一定已有 PV。对于 WaitForFirstConsumer,VolumeBinding 先让调度器选出节点,再由外部 provisioner 按该节点的可用区、机架或其他 CSI 拓扑能力创建卷。

典型链路:

Pod + 未绑定 PVC        │        ▼VolumeBinding 找到可行节点:node-a(zone-a)        │        ▼PVC 被标记 selected-node=node-a        │        ▼external-provisioner 调用 CSI CreateVolume        │        ▼CSI 按 zone-a 创建卷,生成 PV 并写入相应 nodeAffinity        │        ▼PVC Bound,Pod 继续绑定到 node-a

实际实现中,调度器会通过 PVC 的 volume.kubernetes.io/selected-node 注解把选中节点交给动态供给组件。管理员通常不应手工修改它;这是控制器和调度器之间的协作状态,不是给业务 YAML 固定节点的配置项。

调度器不直接创建云盘。 它只负责选择一个存储可行的节点,并把结果交给 CSI external-provisioner;真正调用云盘、Ceph、SAN 或其他后端 API 的是存储驱动链路。

/ / /

6. 计算亲和性与存储亲和性会一起生效

最终节点必须同时通过多层约束:

Pod nodeSelector / nodeAffinity+ Pod anti-affinity+ Taints / Tolerations+ CPU / memory / 扩展资源+ PVC 对应 PV 的 nodeAffinity+ CSI 可提供的拓扑= 最终可调度节点集合

这也解释了为什么“PVC 已经 Bound,Pod 还是 Pending”。

比如:PV 只能在 zone-a 使用,业务 Pod 却写了:

affinity:  nodeAffinity:    requiredDuringSchedulingIgnoredDuringExecution:      nodeSelectorTerms:      - matchExpressions:        - key: topology.kubernetes.io/zone          operator: In          values: [zone-b]

计算亲和性要求 zone-b,存储亲和性要求 zone-a,交集为空。调度器无法替你迁盘,只能让 Pod Pending。

/ / /

7. 最小验证:不要只看 PVC 是否 Bound

排查前先把 Pod、PVC、PV、StorageClass 放在一起看:

kubectl get pod -n app web-0 -o widekubectl get pvc -n app data-web-0 -o widekubectl get pv -o widekubectl get sc -o custom-columns=NAME:.metadata.name,MODE:.volumeBindingMode,PROVISIONER:.provisioner

再看决定拓扑的字段和调度事件:

kubectl get pv <pv-name> -o yaml | sed -n '/nodeAffinity:/,/persistentVolumeReclaimPolicy:/p'kubectl describe pod -n app web-0kubectl describe pvc -n app data-web-0

重点找四类信息:

现象
优先检查
PVC 长期 Pending
是否 `WaitForFirstConsumer` 且没有引用它的 Pod;CSI provisioner 是否正常
Pod Pending,提示 volume node affinity conflict
PV 所在 node/zone 与 Pod 节点条件是否冲突
`0/N nodes are available`
事件中是否包含 `didn't find available persistent volumes to bind`
Pod 已调度但挂载失败
CSI attach/mount、节点插件、云盘权限;这已超出 VolumeBinding 的节点筛选阶段

kubectl describe pod 的 Events 是第一证据。不要看到 PVC Pending 就先删 PVC 重建;对延迟绑定场景,Pending 可能只是正常等待消费者。

/ / /

8. TOP 3 高频避坑

① Local PV 却使用 Immediate

这是 Local PV 最常见的配置错误。PV 资源还没与具体 Pod 的节点选择联合判断,就可能被提前绑定,导致后续 Pod 的节点条件和数据位置冲突。

处理: Local PV 的 StorageClass 使用 kubernetes.io/no-provisioner + WaitForFirstConsumer;PV 必须有正确的 nodeAffinity

② 以为 WaitForFirstConsumer 能自动实现高可用

延迟绑定只解决“首次调度时卷落在正确拓扑”,不能跨节点复制本地数据,也不会在节点故障后把 Local PV 自动搬走。

处理: 需要跨节点恢复能力时,选择有副本/故障域能力的 CSI 存储,或把备份、快照、恢复流程单独设计。

③ 手工创建 Pod 的 nodeName

nodeName 会绕过 kube-scheduler。对于 WaitForFirstConsumer,调度器没有机会运行 VolumeBinding 的节点选择与绑定流程,可能让 PVC 一直 Pending 或造成不符合拓扑的使用方式。

处理: 用 nodeSelector、node affinity 或 Pod affinity 表达约束,让 scheduler 正常参与决策;除非是在明确理解影响的运维应急场景,不要把 nodeName 当日常部署手段。

/ / /

9. 概念别混:PVC Bound、Pod Scheduled、Volume Mounted

这三个状态发生在不同阶段:

PVC Bound  = PV/PVC 的控制面绑定已完成Pod Scheduled  = scheduler 已选定 NodeVolume Mounted  = 目标节点 kubelet / CSI Node Plugin 已把卷挂入 Pod

VolumeBinding 主要介于前两者之间,确保绑定和节点选择不相互矛盾;它不替代 CSI 的 attach/mount,也不保证应用读写一定成功。

因此完整验收至少应包含:

kubectl get pod -n app web-0 -o widekubectl exec -n app web-0 -- df -h /datakubectl exec -n app web-0 -- sh -c 'date > /data/volume-binding-check && cat /data/volume-binding-check'

生产场景不要把测试文件写进真实数据库目录。应使用独立 PVC 或隔离测试 Pod。

/ / /

10. 怎么选

存储场景
建议
NFS / CephFS 等任意节点可挂载的共享存储
`Immediate` 通常可用;仍要按 CSI 文档确认
云盘、块存储,卷受 zone 限制
`WaitForFirstConsumer`
Local PV
必须配合 PV `nodeAffinity` 与 `WaitForFirstConsumer`
需要跨节点自动恢复的数据服务
不要把 Local PV 当 HA;选复制型 CSI + 备份恢复验证

/ / /

总结

VolumeBinding 的核心不是“给 PVC 找 PV”,而是让存储位置参与 Pod 节点选择。Immediate:先绑卷,适合拓扑不敏感的存储。WaitForFirstConsumer:先选满足存储条件的节点,再绑卷/建卷。静态 PV:检查 PV.nodeAffinity 是否与候选节点匹配。动态 CSI:把选中节点传给 provisioner,由后端按拓扑创建卷。排查顺序:Pod Events → PVC → PV.nodeAffinity → StorageClass.volumeBindingMode → CSI 控制器/节点插件。它解决首次放置正确性,不解决数据复制、节点故障迁移和应用一致性。

/ / /

附录:只读排查命令

# 1. 查看集群中各 StorageClass 的绑定模式kubectl get sc -o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner,MODE:.volumeBindingMode# 2. 查看所有未绑定 PVC 与它们正在等待什么kubectl get pvc -Akubectl get events -A --sort-by=.lastTimestamp | grep -Ei 'volume|persistentvolume|provision'# 3. 查看某个 Pod 关联哪些 PVCkubectl get pod -n <namespace> <pod-name> \  -o jsonpath='{range .spec.volumes[*]}{.name}{"  pvc="}{.persistentVolumeClaim.claimName}{"\n"}{end}'# 4. 从 PVC 找到 PV,再看其节点亲和性kubectl get pvc -n <namespace> <pvc-name> \  -o jsonpath='{.spec.volumeName}{"\n"}'kubectl get pv <pv-name> -o yaml# 5. 查看调度失败的原始事件kubectl describe pod -n <namespace> <pod-name>

参考:Kubernetes 官方文档 StorageClass volumeBindingMode、Persistent Volumes,以及 kube-scheduler VolumeBinding 插件实现。

相关学习资料