Kubernetes 调度器默认只感知 CPU 和内存这类可共享资源,对节点上的 NVMe 本地盘完全不感知。传统 PV 调度流程是先调度 Pod,再完成存储卷的绑定与挂载,这种先有鸡还是先有蛋的顺序在本地盘场景下彻底失效。如果 Pod 被调度到没有对应 NVMe 盘的节点,Kubernetes 无法把远程存储变成本地盘,唯一的出路就是让调度器在决策阶段就了解盘的存在,并让绑定动作延后到节点选定之后。

本地 NVMe 卷调度为什么会失败
很多团队第一次接入本地盘时会遇到这样的现象:创建了一个引用本地 PVC 的 Deployment,结果 Pod 一直 Pending,describe 输出中写着 0/3 nodes are available 之类的调度失败信息。这个报错的根源,是 Kubernetes 存储系统与调度系统分工不同带来的矛盾。
对于网络存储 PV,调度器只负责把 Pod 放到满足资源的节点,之后存储控制器再把云盘或分布式存储挂载到该节点,位置无关紧要。但本地 NVMe PV 的可用性完全取决于节点物理位置——盘插在哪台机器上,Pod 就必须运行在哪台机器上。默认调度流程在决策时拿不到卷的位置信息,于是出现两种后果:要么 Pod 被调度到错误节点导致挂载失败,要么 PVC 一直无法匹配到合适的 PV,整个工作负载卡死在 Pending 状态。
Local PV 的调度原理:nodeAffinity 与延迟绑定
解决上述矛盾依靠两套机制:一是 PV 上声明的 nodeAffinity 指定卷所在的物理节点,二是 StorageClass 的 volumeBindingMode 控制 PVC 与 PV 的绑定时机。两者必须配合使用,调度器才能做出正确的放置决策。
每个 Local PV 都必须声明节点亲和性,调度器在过滤阶段读取这个约束条件,把 Pod 的候选节点集合收缩到能访问该卷的节点范围内。不过仅有亲和性还不够,PVC 与 PV 的绑定时机同样关键。如果 PVC 在 Pod 调度前就提前绑定到一个随机 PV,调度器依然可能把 Pod 派到错误的节点上,因为绑定的卷在另一台机器上,Pod 无法访问。
因此本地盘场景必须使用 WaitForFirstConsumer 绑定模式。这种模式下 PVC 不会立即绑定 PV,而是等到第一个消费者 Pod 出现时,调度器先把候选节点过滤一遍,然后基于 PV 的 nodeAffinity 筛选出可用的节点,再完成绑定。整个流程变成先选节点,再绑卷,最后创建 Pod,从根本上解决了位置感知的问题。
调度器的卷过滤流程
调度器处理携带本地 PVC 的 Pod 时,会经历以下几个关键阶段:
- 节点预选:遍历集群全部节点,过滤掉不满足节点亲和性、资源不足、存在污点的节点
- 卷亲和性检查:确认候选节点上是否存在与 PVC 容量和访问模式匹配,且 nodeAffinity 命中的 PV
- 节点评分:对通过过滤的节点按资源余量、Pod 分布等策略打分,选出最优节点
- 延迟绑定执行:调度器选定节点后,通知 PV 控制器将 PVC 与目标 PV 进行双向绑定
需要注意的是,卷亲和性检查依赖调度器的 VolumeBinding 插件。如果集群管理员在 kube-scheduler 配置中禁用了该插件,延迟绑定就不会触发,本地卷调度也自然失效。
实战:配置可被调度的本地 NVMe 卷
下面通过完整的 YAML 示例演示如何搭建一套可用的本地 NVMe 存储体系。首先创建 StorageClass,provisioner 使用 kubernetes.io/no-provisioner,并开启延迟绑定:
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: local-nvme provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Delete
provisioner 必须设置为 kubernetes.io/no-provisioner,这表示本地卷由管理员预先手动创建,Kubernetes 不提供自动供给能力。volumeBindingMode 使用 WaitForFirstConsumer 是核心,它把绑定动作推迟到第一个消费者出现之后,让调度器有机会介入。
接着为某台节点上的 NVMe 盘创建 PV。假设节点名为 node-storage-01,NVMe 盘挂载在 /mnt/nvme0n1:
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-nvme-pv-01
labels:
type: local-nvme
spec:
capacity:
storage: 800Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-nvme
local:
path: /mnt/nvme0n1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-storage-01
local.path 指向节点上的实际挂载路径,nodeAffinity 则把这块盘限定在 node-storage-01 这台机器上。这里特别建议把 reclaimPolicy 设置为 Retain,因为本地盘上的数据在 PV 删除后无法自动清理,Retain 模式可以防止 Kubernetes 在 PV 删除时误删物理数据。
然后创建 PVC 和 Deployment,验证调度是否正常工作:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nvme-pvc
spec:
storageClassName: local-nvme
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 800Gi
apiVersion: apps/v1
kind: Deployment
metadata:
name: nvme-consumer
spec:
replicas: 1
selector:
matchLabels:
app: nvme-app
template:
metadata:
labels:
app: nvme-app
spec:
containers:
- name: app
image: busybox:1.36
command: ["sleep", "3600"]
volumeMounts:
- name: nvme-data
mountPath: /data
volumes:
- name: nvme-data
persistentVolumeClaim:
claimName: nvme-pvc
创建 Deployment 后观察 PVC 状态变化:Pod 调度前 PVC 会短暂处于 Pending,调度器完成节点选择后,PVC 自动变为 Bound,Pod 进入 Running。这个过程中,kube-scheduler 动态读取 StorageClass 的绑定模式并触发延迟绑定,PVC 和 PV 的绑定事件可以在 kubectl describe pvc 的输出中看到。
调度器的卷评分与本地盘优化
默认调度器具备卷亲和性过滤能力,却不会对节点上的本地盘数量进行加权评分。这意味着当多个节点都满足亲和性条件时,调度器可能把多个 Pod 集中放到同一台机器上,而不是选择空闲盘最多的节点。对本地 NVMe 存储来说,这种无差别打分容易造成热点不均,某些盘跑满而另一些盘闲置。
社区的常见优化方案包括使用 NodeVolumeLimits 插件设置每台节点可挂载卷的数量上限,或者通过扩展调度器为盘余量增加权重。另一种更轻量的做法是为本地盘节点打上专用标签,配合节点亲和性或 nodeSelector,让使用本地盘的 Pod 与普通 Pod 在节点层面隔离,避免资源竞争。
如果你的集群规模较大且对调度质量要求高,可以考虑使用 TopoLVM 这类基于 CSI 的本地存储方案。它在调度器侧实现了容量感知,Pod 调度时不仅检查盘是否存在,还会计算卷组剩余空间,动态创建逻辑卷,避免手工维护大量 PV 的繁琐工作。
本地 NVMe 调度中的常见坑
生产环境中以下几个问题几乎每个使用本地盘的团队都会遇到,提前了解可以节省大量排障时间。
第一,忘记为 PV 设置正确的 nodeAffinity。有的管理员直接从文档复制 PV 定义,忘记修改节点名,结果 Pod 被调度到没有盘的节点,挂载阶段报出 FailedMount 或 MountVolume.MountDevice failed 错误。排查时先看 describe pod 中的事件,确认卷亲和性是否命中。
第二,PVC 请求容量与 PV 容量不匹配。Local PV 不支持动态扩容,PVC 匹配完全按照容量和访问模式进行,申请容量必须与 PV 完全一致,不能像网络存储那样申请一个小于 PV 的值。常见的把 800Gi 的盘申请成 1Ti 的 PVC,就会导致调度器找不到匹配卷。
第三,ReclaimPolicy 设置不当带来数据风险。如果使用 Delete 模式,PV 删除时 Kubelet 会尝试清理本地数据,但清理失败并不会阻止 PV 对象删除,容易留下残留脏数据。对于存放重要数据的本地盘,建议始终使用 Retain,由运维人员手动处理数据生命周期。
第四,把本地盘用于多副本有状态应用。Local PV 默认只支持 ReadWriteOnce 访问模式,一个卷同时只能被一个节点挂载,Deployment 副本数大于 1 时无法共享同一块盘。要么为每个副本单独准备 PV,要么改用新版本提供的 ReadWriteOncePod 模式,确保单个 Pod 独占卷访问。
排障时的标准动作是先执行 kubectl describe pvc 查看绑定状态,再执行 kubectl describe pod 查看调度器事件。调度失败的事件里一般会明确写出 node affinity 或 volume node affinity 不匹配,顺着这些线索检查节点标签、PV 的 nodeSelectorTerms 以及 StorageClass 的绑定模式,大部分问题都能在几分钟内定位。
扩展:动态本地存储方案选型
手工维护几十个 PV 在大型集群中不现实,因此社区发展出了若干自动化工具。除了上面提到的 TopoLVM,还有 Rancher 的 Local Path Provisioner,它直接在节点指定目录下为 PVC 创建子目录并自动生成 PV,配合 nodeAffinity 把 Pod 固定到对应节点。这些方案的底层机制都是利用调度器的卷亲和性完成节点选择,只是把 PV 创建过程自动化了。
选型时可以从三个维度考量:是否需要卷快照、是否需要容量配额、是否需要故障迁移。本地 NVMe 存储天然不支持跨节点迁移,一旦节点宕机,盘上的数据只能等节点恢复后才能访问。因此设计存储架构时,建议把本地盘定位为高性能临时存储或可重建数据的缓存层,而把需要高可靠性的持久数据放在分布式存储上,两种存储结合使用。
总体来看,本地 NVMe 存储卷调度是由 nodeAffinity 硬约束、WaitForFirstConsumer 延迟绑定、调度器卷过滤插件三部分协作完成的闭环。理解了三者的运行顺序和依赖关系,就能在生产环境中准确排障,设计出既发挥本地盘性能优势又足够稳定的存储架构。
Kubernetes本地存储NVMe修改时间:2026-08-25 01:31:48