- Published on
KubeVirt GPU 直通 VM 为何 112 天无法被调度 — 一次真实集群的尸检
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 开篇 — 开始这场尸检
- 第 1 部分 — 症状:卡了 112 天的 VM
- 第 2 部分 — GPU 其实注册得好好的
- 第 3 部分 — 真正的原因:kubelet 的一行
- 第 4 部分 — 直通的物理法则:为什么"去别的节点"行不通
- 第 5 部分 — 恢复:先改标签,再关 swap
- 结语 — 尸检的教训
- 参考资料
开篇 — 开始这场尸检
在前一篇里,我用 Rust 写的 GPU Operator 下达了一个出乎意料的诊断:4 个 GPU 节点全部 NotReady,也就是说整个集群的 GPU 能力已经彻底死掉了。 本文就是把这场死亡的病因在真实集群里追查到底的一份尸检记录。对象是一个 8 节点的家庭实验室(GPU Operator v25.3.0、KubeVirt v1.7.0),所有证据都来自实测。如果你需要本文的背景概念,建议先读GPU Operator × KubeVirt 全面梳理篇。
第 1 部分 — 症状:卡了 112 天的 VM
先看一眼这些 KubeVirt VM 的状态。这是真实集群里的 VM 阶段分布:
$ kubectl get vm -A
阶段 数量 备注
──────────────── ──── ─────────────────────────────
Running 3
Paused 2
Stopped 1
ErrorUnschedulable 2 ← gpu-fedora, rhel9-gpu-vm (都是 GPU VM!)
gpu-fedora 与 rhel9-gpu-vm——恰好只有这两个要求 GPU 的 VM 处于 ErrorUnschedulable。那么卡了多久?kubectl get vm 的 AGE 显示 112 天。整整 112 天没能被调度、就这么被晾在一边。 不用 GPU 的那些 VM(Running、Paused)都好好的。凶手就藏在 GPU 这条路径的某处。
GPU Operator 那些 operand Pod 的状态也算不上健康:
$ kubectl get pods -n gpu-operator (状态直方图)
Running 14
Error 6 ← sandbox-device-plugin, sandbox-validator 等
Terminating 4
第 2 部分 — GPU 其实注册得好好的
有意思的反转:GPU 配置本身其实无可挑剔。 看 KubeVirt CR 里的 permittedHostDevices,5090 和 3090 连 PCI ID 都注册得准确无误:
$ kubectl get kubevirt -n kubevirt -o yaml (permittedHostDevices 摘录)
pciHostDevices:
- pciVendorSelector: "10de:2b85" # NVIDIA GB202 = RTX 5090
resourceName: nvidia.com/GB202_GEFORCE_RTX_5090
externalResourceProvider: true
- pciVendorSelector: "10de:2204" # NVIDIA GA102 = RTX 3090
resourceName: nvidia.com/GA102_GEFORCE_RTX_3090
externalResourceProvider: true
从 sandboxWorkloads.enabled: true 到 driver.enabled: false(使用宿主机驱动),把 GPU 交给 VM 的舞台全都搭好了。可 VM 就是起不来。配置再对,没有节点也是白搭——这句话改变了尸检的方向。
第 3 部分 — 真正的原因:kubelet 的一行
我登上那台唯一能调度 GPU VM 的节点(omen2,装着 RTX 5090),去看 kubelet 日志。凶手不是什么花哨的 GPU 问题,而是这一行:
$ journalctl -u kubelet -n 5 (omen2)
E0710 15:39:51 run.go:72] "command failed" err="failed to run Kubelet:
running with swap on is not supported, please disable swap
or set --fail-swap-on flag to false"
systemd[1]: kubelet.service: Main process exited, code=exited, status=1/FAILURE
systemctl is-active kubelet → activating(也就是 crash-loop)。有人在这台节点上开了 32GB 的 swapfile,而 Kubernetes 的 kubelet 默认在 swap 开启时会拒绝启动。kubelet 一死,节点就变成 NotReady,NFD 抹掉 GPU 标签,device-plugin 无法再广告 nvidia.com/gpu——架在它上面的一切随之崩塌。
把这条因果链画成一行,就是这样:
swap on → kubelet 拒绝(crash-loop) → 节点 NotReady
→ NFD 标签消失 + device-plugin 停止
→ GPU VM 无法调度(ErrorUnschedulable, 112天)
第 4 部分 — 直通的物理法则:为什么"去别的节点"行不通
换成普通的 Pod,哪怕一台节点死了,调度器也会把它挪到别的节点。GPU 直通 VM 为什么挪不了?因为它被绑死在物理设备上。 gpu-fedora 要求一个叫 nvidia.com/GB202_GEFORCE_RTX_5090 的资源,可那块物理 5090 只插在 omen2 上。那台节点一死,VM 就无处可去。
这不是 bug,而是设计——正是我在 GPU Operator × KubeVirt 篇里写下的那句话的实物:"直通给了 VM 的 GPU 没法做热迁移,这是一条与版本无关的物理法则"。和容器 GPU(用 MIG 切分、由多个 Pod 共享)不同,VM 直通把 一块 GPU = 一台节点 牢牢绑在一起。这是用放弃密度换取强隔离的那种取舍投下的阴影。
第 5 部分 — 恢复:先改标签,再关 swap
恢复过程里还埋着一个坑。omen2 的工作负载标签是 nvidia.com/gpu.workload.config=vm-passthrough。若就这么把 kubelet 直接救活,GPU Operator 会拉起 vfio-manager,把 5090 重新绑定到 vfio-pci 驱动——于是那台节点上跑着的所有 CUDA/torch 作业会瞬间死掉(这块 GPU 正是我在小模型实验篇里用的那张卡)。
所以恢复的顺序至关重要:
① (在救活 kubelet 之前) 把标签改成 container,阻止 vfio 重新绑定
kubectl label node omen2 nvidia.com/gpu.workload.config=container --overwrite
② 关闭 swap 并持久化
sudo swapoff -a
sudo sed -i '/swap/s/^/#/' /etc/fstab
③ 重启 kubelet → 节点 Ready,5090 以 nvidia.com/gpu 暴露给 Pod
sudo systemctl restart kubelet
先做①的理由是关键——因为 driver.enabled: false,GPU Operator 会直接沿用宿主机驱动,所以只要是 container 标签,就能在保留宿主机 nvidia 驱动的前提下把 GPU 暴露给 Pod。也就是说 torch 活着,k8s 也看得到 GPU。(①我已经先应用好了,②③需要 sudo,得由节点的主人亲自动手。)
结语 — 尸检的教训
最大的教训虽然老套,却每次都常读常新:华丽那层的故障,往往来自枯燥的那层。 GPU 直通、vfio、permittedHostDevices、PCI ID 一路都完美无缺,把它拖垮的却是 swap on 这一行。第二个教训是直通的物理学——把一整块 GPU 交给 VM,这个 VM 就被钉死在那台节点上。要密度就用容器+MIG,要隔离就用 VM 直通,而这个选择的代价是"节点一死,它就跟着死"。还有,这一切诊断的起点,是我用 Rust 写的那个 20 行的 Operator——基础设施,你亲手摸过多少,才能看见多少。