Skip to content
Published on

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

分享
Authors

开篇 — 开始这场尸检

前一篇里,我用 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-fedorarhel9-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: truedriver.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 kubeletactivating(也就是 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——基础设施,你亲手摸过多少,才能看见多少。

参考资料