PVC 显示 Bound,Pod 却一直 Pending:本地盘的绑定顺序

卷已经绑定,Pod 却调度不上去,重建三次都一样。原因是 PVC 抢在调度前就绑定了,盘钉在那台节点上;本地盘不会漂,节点维护也不能直接 drain。

给监控组件加一个 sidecar 存储的时候,遇到过一个很没道理的现象:PVC 明明是 Bound,Pod 却一直 Pending,镜像早就拉完了,节点资源也够。我把 Pod 删了重建三次,结果一样。

这种情况很容易被当成"调度器卡住了"。实际上 Pending 的原因写在 events 最后一行,只是平时没人往下翻。

一、先看 events 的最后一行

kubectl -n monitoring describe pod prometheus-0 | sed -n '/Events/,$p'

关键那行是:

Warning  FailedScheduling  node(s) had volume node affinity conflict

翻译过来:这个 Pod 要用的卷,只能挂在某台节点上,而调度器选的那台不是它。Bound 只说明"卷和 PVC 配对了",不代表"这个 Pod 能被放到卷所在的节点上"。这两件事是分开的,Bound 不等于能用。

二、Immediate 和 WaitForFirstConsumer 的区别

我们建 StorageClass 的时候用的是 no-provisioner 的静态供给模式,第一版没写 volumeBindingMode。它的默认值是 Immediate:PVC 一创建就立刻绑定 PV,根本不考虑 Pod 将来调度到哪。

于是顺序变成:PVC 绑了一台节点上的盘 → 调度器按资源挑了一台节点 → 两者不一致 → 永久 Pending。删 Pod 重建没用,因为 PVC 已经绑死了。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-static
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
# 关键:等第一个消费者出现再绑定,让调度器先决定节点
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false

改成 WaitForFirstConsumer 之后,绑定被推迟到调度阶段:调度器先看这个 Pod 有哪些候选节点,再看这些节点上哪些 PV 可用,两边取交集,然后才绑定。改成这样之后同一个 PVC 声明,Pod 一分钟内就起来了。

绑定模式 绑定时机 结果
Immediate(默认) PVC 创建即绑定 盘先选好节点,Pod 可能被调度到别处,永久 Pending
WaitForFirstConsumer 有 Pod 引用时才绑定 调度器先选节点,再在有盘的节点里选,冲突消失

三、本地盘的真实代价是"钉死"

这套集群没有共享存储,十三个本地 PV 里有十一个绑在同一台 worker 上,只有两个在控制面节点。这是容量和成本权衡的结果——本地 NVMe 比网络盘快,也不用维护分布式存储集群——但代价必须写清楚:盘不会漂。

具体表现有三条:

  • Pod 只能调度到盘所在的那台节点,节点挂了 Pod 起不来,不是"等一会儿就好"。
  • 滚动更新时如果副本数只有 1,Recreate 策略下会有一个明显的空窗。
  • 节点维护不能 kubectl drain,驱逐只会把 Pod 删掉,盘还在原地。

第三条最容易出事。我们做停机预案的时候专门写明:先停 worker,再停控制面;不要 drain。因为 drain 会把有状态 Pod 全部驱逐,而它们在新节点上没有任何盘可用,等于把一次可恢复的停机变成一次全量不可用。

四、把网络盘换成本地盘的搬迁做法

前期有十二个 PV 走的是网络文件系统。后来发现两个问题:一是其中两个是嵌入式数据库,锁和 fsync 语义在网络盘上不成立;二是网络盘那台机器本身成了单点。

搬迁用的是最朴素的办法——同一块磁盘内 mv:

# 目录在同一块 ext4 上,mv 是原子的,保留属主与权限
sudo mv /data/nfs/monitoring/prometheus /data/k8s-local/monitoring/prometheus
sudo chown -R 999:root /data/k8s-local/monitoring/prometheus
sudo chmod 700 /data/k8s-local/monitoring/prometheus

然后新建十一个本地 PV,全部 Retain + nodeAffinity 指向同一台节点。旧 PV 保留不删,万一要回滚,把 PVC 改回去就行。

搬迁过程中还合并了两个指向同一路径的 PVC——同一个目录被两个声明各挂了一次,容器里看到的是两份独立的挂载点。RWO 允许同节点多 Pod 共挂同一个 PV,这一点在本地盘上是成立的,但前提是这两个 Pod 必须都在那台节点上。

五、本地盘没有配额,容量告警会失真

本地 PV 的 capacity 只是一个标签,调度器拿它做匹配,文件系统层面没有任何限制。所以会出现这种情况:PV 写着 200Gi,实际只占 500M,而节点根盘只剩 20Gi,告警却一切正常。

KubePersistentVolumeFillingUp 这条告警基于 kubelet 上报的 volume_stats,本地盘目录没有配额时它算不准,长期处于不触发状态。我们后来补了一条按目录 du 的巡检,并且把阈值从"剩余 15%、持续 15 分钟"改成"剩余 25%、持续 5 分钟"——磁盘写满这类故障,五分钟和十五分钟的区别就是"能救"和"只能扩容"。

# 本地 PV 目录实际占用,比 capacity 更可信
for d in /data/k8s-local/*/; do
  printf '%-48s %s\n' "$d" "$(du -sh "$d" 2>/dev/null | cut -f1)"
done | sort -k2 -h | tail -8

df -h /data | tail -1

六、把检查做成清单

这类问题的共同点是"配置写对了、顺序错了",靠人记不住。我们现在新建 PV 一律走同一份清单:

检查项 期望值 不满足的后果
volumeBindingMode WaitForFirstConsumer PVC 先绑定,Pod 永久 Pending
reclaimPolicy Retain 误删 PVC 时数据被回收
nodeAffinity 节点名 与 kubectl get node 的 NAME 完全一致 PV 无法被任何 Pod 使用
目录属主与权限 与容器运行用户一致(如 999:root 700) 容器启动报 Permission denied
目录所在文件系统余量 ≥ 25% 写入失败、数据库崩溃
备份是否覆盖该目录 有独立备份任务 节点故障即数据丢失

其中"目录属主"这一条踩过:盘挂上了、Pod 起来了、日志里是 MySQL 的 Permission denied。本地盘不像某些网络存储有服务端映射,属主就是文件系统上的属主。

小结

  • PVC Bound 和 Pod 能调度 是两件事,Pending 的答案在 events 最后一行。
  • 静态供给的 StorageClass 必须写 volumeBindingMode: WaitForFirstConsumer,否则 PVC 抢在调度前绑定。
  • 本地盘的代价是 Pod 被钉在节点上,滚动更新有空窗,节点维护不能 drain。
  • 停机顺序按"先 worker 后控制面"走,本地盘不会漂,drain 只会把服务变不可用。
  • 本地 PV 的 capacity 不是配额,容量告警会失真,要补目录级 du 巡检。
  • 同一目录被两个 PVC 各挂一次不会报错,只会看到两份数据;目录属主与文件系统余量最容易被跳过,出事时表现都是服务起不来。
查看「容器与编排」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

PVC 显示 Bound,Pod 却一直 Pending:本地盘的绑定顺序 - 起航社区