Node Feature Discovery — 发现到的事实如何变成标签
一句话总结
GPU Operator 并不直接去看节点上插着的显卡,而是看节点标签,这些标签分别由 Node Feature Discovery(NFD)、GPU Feature Discovery(GFD)和 Operator 自身添加。所以一旦缺少标签,既没有报错也没有警告,就是什么事都不会发生。
为什么需要它
处理 GPU 节点的软件,至少要知道三件事:这个节点上有没有 NVIDIA 显卡,有几块、是什么型号,驱动是哪个版本。过去的办法是登录节点,输入 lspci,再运行 nvidia-smi。节点有 3 个还行,有 300 个就不可能了。
Kubernetes 对这个问题给出的答案是:“把硬件信息以标签的形式写到 Node 对象上”。一旦变成标签,后面的一切就都能用 Kubernetes 的日常工具来解决。调度器的 nodeSelector 和 nodeAffinity 读取这些标签,DaemonSet 的 nodeSelector 用这些标签决定放置位置,人则用 kubectl get node -L 一目了然地查看。把查询硬件的问题变成查询标签的问题,就是这个设计的全部。
NFD 是承担这项工作的 Kubernetes SIG 项目。它的结构分为两部分。在每个节点上以 DaemonSet 运行的 nfd-worker,按 CPU、内核、PCI、USB、内存、存储设备、网络等类别收集并上报信息;nfd-master 接收这些上报,把标签写到 Node 对象上。worker 不直接修改节点,而要经过 master,是为了把修改 Node 对象的权限集中在一处。
工作原理
标签的前缀会显示它的来源。只要记住这一点,读屏幕的速度就会不一样。
| 前缀 | 添加者 | 示例 |
|---|---|---|
feature.node.kubernetes.io/ |
收到 nfd-worker 上报的 nfd-master | pci-10de.present、kernel-version.full、system-os_release.ID |
nvidia.com/gpu.*、nvidia.com/cuda.* |
gpu-feature-discovery | gpu.product、gpu.count、gpu.memory、cuda.driver.major |
nvidia.com/gpu.deploy.* |
GPU Operator | gpu.deploy.driver、gpu.deploy.container-toolkit |
NVIDIA 文档写明,通过 feature.node.kubernetes.io/pci-10de.present=true 标签的存在来识别 GPU worker 节点。0x10de 是分配给 NVIDIA 的 PCI 厂商 ID。NFD 的 PCI 标签默认使用 <class>_<vendor> 格式的设备名称,而 GPU Operator 被配置为只看厂商。
GFD 添加的标签更具体。照搬官方文档中的输出示例,大致是这样的。
{
"nvidia.com/cuda.driver.major": "450",
"nvidia.com/cuda.driver.minor": "80",
"nvidia.com/cuda.driver.rev": "02",
"nvidia.com/cuda.runtime.major": "11",
"nvidia.com/cuda.runtime.minor": "0",
"nvidia.com/gpu.compute.major": "8",
"nvidia.com/gpu.count": "1",
"nvidia.com/gpu.family": "ampere",
"nvidia.com/gpu.memory": "40537",
"nvidia.com/gpu.product": "A100-SXM4-40GB"
}
值得注意的是,驱动版本被拆成了 major、minor、rev 三段。没有把 450.80.02 整个放进一个标签,原因是为了比较。nodeAffinity 的 Gt 和 Lt 运算符会把值读成整数,所以拆成几段之后,才能写出“驱动版本 550 以上的节点”这样的条件。
标签的值有语法限制。 不能超过 63 个字符,必须以字母或数字开头和结尾,中间只能出现 -、_、.。然而驱动给出的设备名称像 NVIDIA A100-SXM4-40GB 这样含有空格。直接放进去的话,API 服务器会以 Invalid value 拒绝。所以 GFD 会先整理名称再添加,我们在屏幕上看到的 NVIDIA-A100-SXM4-40GB 就是这种规范化的结果。不了解这一点,就会在“按文档里写的型号名称设置了条件,却没有任何节点匹配”这种问题上耗掉很长时间。
读取标签的一方也有规则。nodeSelector 只看键和值是否完全相同,而 nodeAffinity 可以使用 In、NotIn、Exists、DoesNotExist、Gt、Lt。这里有一个人们常常弄错的结构。
nodeSelectorTerms是列表,各项之间是 OR。只要有一项满足,这个节点就成为候选。- 同一项中的
matchExpressions是 AND。必须全部满足。
而且 NotIn 也会放行根本没有该键的节点。 如果想表达“除了 A10 以外的 GPU 节点”,只用一个 NotIn,连没有 GPU 的节点也会成为候选。这就是为什么先配合 Exists 缩小范围成了惯用写法。
名称末尾的 IgnoredDuringExecution 也不是随便加上的。requiredDuringSchedulingIgnoredDuringExecution 只在调度时要求条件,对已经启动的 Pod,即使条件不再成立也不会把它赶走。所以即使误删了标签,正在运行的工作负载依然完好,从下一次新启动的 Pod 开始,才会悄悄变成 Pending。事故要过几个小时才暴露出来,就是这种结构造成的。
标签的归属
NFD 添加的标签属于 NFD。worker 会定期重新上报,master 会让节点与这份上报保持一致,所以人手工修改的值会在下一个周期悄悄恢复原样。第一次遇到时,会去追查“标签总是复活”这个幽灵,但弄清楚之后就会发现这是理所当然的行为——如果发现出来的事实可以被人覆盖,就没有理由相信这个标签了。
所以由人来决定的事实,要写到别的键里。 团队归属、工作负载等级、维护窗口之类的信息,应写到使用自家公司前缀的标签上,对发现类标签只读不写。反过来,如果想让 NFD 执行人定的规则,就用 NodeFeatureRule 声明规则,让 NFD 来添加——这样这个标签也成为 NFD 的标签,会被一致地管理。
在现场相遇的样子
第一,什么事都不会发生。 安装了 GPU Operator,却一个 operand 都没有启动,这类报告最常见的原因是缺少 pci-10de.present 标签。可能是单独安装了 NFD 后标签前缀的设置变了,也可能是新节点加入后,nfd-worker 的 DaemonSet 没有在该节点上启动。无论哪种情况,都没有错误日志。 条件不满足时放置就不会发生,而没有发生的事是不会留下日志的。
第二,型号名称有细微差别。 nvidia.com/gpu.product 是经过 GFD 规范化的值,开启 MIG 后会带上像 NVIDIA-H100-80GB-HBM3-MIG-1g.10gb 这样的后缀。开启时间分片后又会带上另一种后缀。所以按型号名称设置条件时,要看的不是文档中的名称,而是当前集群中的标签值。
第三,随身带着标签检查器的团队更快。 节点声称自己是 GPU 节点,却缺了型号、数量、显存中的某一项,或者本该是数字的值不是数字,这些事确实会发生。与其靠人眼去发现,不如用一个小脚本来发现,这样每次有新节点加入,30 秒就能确认完毕。关键在于这个脚本要随时获取节点列表。把节点名称写死的检查器,从扩容那天起就开始说谎了。
第四,本环境诚实的局限。 实验环境中没有 GPU,也没有 NFD 和 GFD。所以“发现”要由你用标签搭建出来。不过,标签值的校验、nodeAffinity 的评估、调度器的放置决策和节点添加,都由 kwok 启动的真实 API 服务器和真实调度器完成——那不是模拟,而是真实的组件。
参考文档
- Node Feature Discovery 入门:https://kubernetes-sigs.github.io/node-feature-discovery/stable/get-started/index.html
- NFD 添加的特性标签列表:https://kubernetes-sigs.github.io/node-feature-discovery/stable/usage/features.html
- GPU Feature Discovery 添加的标签(MIG 策略文档中的输出示例):https://docs.nvidia.com/datacenter/cloud-native/kubernetes/latest/index.html
- 把 Pod 放置到节点上(nodeSelector、nodeAffinity):https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/
- 标签与选择器(值的语法限制):https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
下一项实验要做什么
在 kwok 启动的真实 API 服务器上,亲手搭建三种来源的标签。放入含空格的设备名称,让 API 服务器拒绝它,再制作把这个名称整理成标签值的工具。用 In、NotIn、Exists、Gt 和 term 列表,让调度器亲自确认 Pod 会去哪个节点,并亲眼看到删除标签之后,正在运行的 Pod 依然保留,而只有新的 Pod 会变成 Pending。最后制作把发现结果恢复原样的同步器,以及即使有新节点加入也不会说谎的标签规范检查器。