NVSentinel metadata-collector模块调研
基于 NVSentinel 文档与 metadata-collector/ 代码的实现分析。文档引用指向 NVIDIA/NVSentinel main 分支。
官方文档:
- 模块说明:docs/metadata-collector.md
- Helm 配置:docs/configuration/metadata-collector.md
- 设计 ADR:docs/designs/010-metadata-retrieval.md
1. 模块定位
一句话:metadata-collector 以 DaemonSet 形式跑在每个 GPU 节点上,采集本机 GPU(及 NVSwitch / NIC 等相关拓扑)信息,写入节点本地共享存储,并补充 Pod↔GPU 映射,供其他模块只读消费。
metadata-collector 是 NVSentinel 的 节点级硬件清单 + Pod↔GPU 映射发布器。它不消费 HealthEvent,也不做隔离/修复决策;它为下游监视器与 node-drainer 提供统一的硬件语义与设备归属信息。
text
NVML / nvidia-smi / kubelet
│
▼
metadata-collector (DaemonSet, 每 GPU 节点一份)
│
├─► /var/lib/nvsentinel/gpu_metadata.json
│ ├─ gpu-health-monitor(UUID / PCI / 热限 / NVLink 误报抑制)
│ ├─ syslog-health-monitor(XID PCI→GPU、SXID NVSwitch 映射)
│ └─ nic-health-monitor(GPU↔NIC topo、NUMA)
│
└─► Pod 注解 dgxc.nvidia.com/devices
└─ node-drainer(COMPONENT_RESET 部分驱逐)
| 角色 | 职责 |
|---|---|
| metadata-collector | 采集 GPU/NVSwitch/NIC 拓扑并写本地 JSON;周期性标注 Pod 占用的 GPU UUID |
| health monitors | 读 JSON,把底层错误码关联到具体 GPU / 链路 |
| node-drainer | 读 Pod 注解,只驱逐占用故障 GPU 的 Pod(部分 drain) |
| fault-remediation | 不直接读 metadata;间接依赖「事件里是否有 GPU_UUID」以及 node-drainer 是否能部分 drain |
为什么需要它(ADR-010):各 monitor 语言/运行时不同,若各自查 NVML,标识不一致、重复特权、难测。统一落盘 JSON + K8s 注解后,跨组件用同一契约。
2. 代码结构
text
metadata-collector/
├── main.go # 两阶段生命周期:Collect 一次 → Mapper 循环
├── Dockerfile # CUDA base + CGO 链接 NVML
├── Makefile # CGO_ENABLED=1、lint-test
├── go.mod
└── pkg/
├── collector/collector.go # 编排 NVML + NIC topo → model.GPUMetadata
├── writer/writer.go # 原子写 JSON(.tmp → rename,chmod 0644)
├── nvml/
│ ├── wrapper.go # NVML 封装:Init/GetGPUInfo/ChassisSerial
│ ├── topology.go # CollectNVLinkTopology(NVML + 解析回退)
│ └── nvlink_parser.go # 解析 `nvidia-smi nvlink -R`
├── nic/topo_parser.go # 解析 `nvidia-smi topo -m`
└── mapper/
├── mapper.go # Pod→GPU 注解增删改
├── httpsclient.go # Kubelet HTTPS GET /pods
└── grpcclient.go # Kubelet PodResourcesLister gRPC
共享契约(下游都依赖):
| 路径 | 内容 |
|---|---|
data-models/pkg/model/gpu_metadata.go |
GPUMetadata / GPUInfo / NVLink JSON schema |
data-models/pkg/model/pod_device_annotation.go |
dgxc.nvidia.com/devices 注解名与结构 |
Helm Chart:distros/kubernetes/nvsentinel/charts/metadata-collector/。
依赖:github.com/NVIDIA/go-nvml、Kubernetes client-go、commons logger;构建需 CGO。
3. 运行时架构:两阶段生命周期
入口在 main.go。进程模型很清晰:先一次性采硬件,再永久跑 mapper 。任一步失败则 os.Exit(1),由 DaemonSet 重启。
text
main
├─ runCollector(ctx) # 阻塞到成功写完 JSON(或失败退出)
│ ├─ NVMLWrapper.Init()
│ ├─ collector.Collect()
│ ├─ writer.Write(gpu_metadata.json)
│ └─ NVMLWrapper.Shutdown()
└─ runMapper(ctx) # 每 30s 一轮,直到 SIGTERM
└─ PodDeviceMapper.UpdatePodDevicesAnnotations()
3.1 常量与 flag
| 符号 | 值 | 含义 |
|---|---|---|
defaultOutputPath |
/var/lib/nvsentinel/gpu_metadata.json |
默认落盘路径 |
defaultPodDeviceMonitorPeriod |
30s |
mapper 周期 |
--output-path |
覆盖默认路径 | Chart 注入为 global.metadataPath |
3.2 关键设计点:元数据不是周期刷新
runCollector 只在 Pod 启动时执行一次 。GPU 热插拔、驱动升级、fabric-manager 晚于采集完成链路训练等情况,不会自动更新 JSON,除非 DaemonSet Pod 重启。
下游读者需自行处理「文件缺失 / 字段为 nil / active link 为 0」------见 §8、§11。
3.3 产物一览
| 产物 | 目标 | 更新频率 |
|---|---|---|
| GPU 元数据 JSON | hostPath 文件 | Pod 启动一次 |
| Pod 设备注解 | dgxc.nvidia.com/devices |
约每 30s(有变化才 patch) |
不写:Node label / Node annotation / ConfigMap(ConfigMap 仅出现在 e2e 注入辅助脚本里)。
4. 采集流水线(collector)
4.1 编排入口
pkg/collector/collector.go 的 Collect:
GetDeviceCount/Hostname/GetDriverVersion(失败则 整次 Collect 失败)prepareTopologyData:建 PCI→Device map,解析nvidia-smi nvlink -R(失败则 Warn,remote_link_id退化为-1)collectGPUData:逐 GPUGetGPUInfo+CollectNVLinkTopology;GPU0 在驱动 ≥ R560 时取 chassis serial- 所有 GPU 的
NUMANode先置-1,再populateNICTopology(nvidia-smi topo -m)
go
// Collect 主序(简化)
count := nvml.GetDeviceCount()
metadata := &GPUMetadata{Version: "1.0", Timestamp: RFC3339, NodeName, DriverVersion, GPUs: ...}
prepareTopologyData(ctx) // deviceMap + parsed NVLink topology
collectGPUData(...) // 逐卡;NVLink 失败只 Warn,卡仍入库
for i := range GPUs { NUMANode = -1 }
populateNICTopology(ctx, metadata) // NIC topo 失败不拖垮整次 Collect
4.2 致命 vs 降级
| 失败点 | 行为 | 结果 |
|---|---|---|
NVML Init / device count / GetGPUInfo |
返回 error | 无 JSON,Pod CrashLoop |
nvidia-smi nvlink -R 解析失败 |
Warn,空 parsedTopology |
JSON 仍写出;remote_link_id=-1 |
单卡 CollectNVLinkTopology 失败 |
Warn | 该卡仍进 gpus[],链路字段可能不全 |
nvidia-smi topo -m 失败 |
Warn,直接 return | 无 nic_topology;numa_node 保持 -1 |
| topo 列 GPU 数 ≠ NVML count | Warn,丢弃 nic/numa | 避免错位矩阵误导 nic-health-monitor |
| 无 IB NIC 列 | Info | nic_topology 为空 |
| 驱动 major < 560 | 跳过 chassis serial | chassis_serial 为 null |
supportsChassisSerial 解析 driverVersion 的 major(. 前第一段),阈值常量 minSupportedMajorVersion = 560。
4.3 NVML 封装(pkg/nvml/wrapper.go)
NVMLWrapper 对 go-nvml 的薄封装,关键能力:
| 方法 | 作用 |
|---|---|
Init / Shutdown |
nvml.Init / Shutdown |
GetGPUInfo(index) |
UUID、规范化 PCI、序列号、设备名、slowdown_tlimit_c(FI_DEV_TEMPERATURE_SLOWDOWN_TLIMIT) |
GetChassisSerial(index) |
GetPlatformInfo(通常只对 GPU0) |
BuildDeviceMap |
PCI → Device,供 NVLink 反向查 remote link |
PCI 地址会做 normalize(小写等),保证与 syslog 里 PCI 字符串可匹配。
4.4 NVLink / NVSwitch(topology.go + nvlink_parser.go)
CollectNVLinkTopology 核心逻辑:
- 遍历
nvml.NVLINK_MAX_LINKS GetNvLinkState成功 → 计入nvlink_link_count(不论是否 ENABLED)- 仅
FEATURE_ENABLED→ 计入nvlink_active_link_count,并查 remote PCI / device type - 只把 remote 为
NVLINK_DEVICE_TYPE_SWITCH的链路写入gpuInfo.NVLinks,并收集 NVSwitch PCI 集合到顶层nvswitches[] remote_link_id:优先用nvidia-smi nvlink -R解析结果;否则 NVML 反向查找;再否则 -1
代码注释明确区分两类硬件语义(对理解 gpu-health-monitor 误报抑制很重要):
- 无 NVLink 硅片 (如部分 L40/A40):
GetNvLinkState常返回 NOT_SUPPORTED → count 为 0 - 可插 NVLink bridge 的 PCIe 卡 (A100/H100 PCIe):无桥时也可能
SUCCESS且 active=0 → 不能 单凭nvlink_link_count>0断定「正在用 NVLink」 - SXM/HGX:启动时 active=0 常表示 fabric-manager 尚未训完链路 → 消费者应视为 UNKNOWN,而非「无 NVLink」
4.5 GPU↔NIC 拓扑(pkg/nic/topo_parser.go)
RunTopoCommand 执行 nvidia-smi topo -m(超时约 30s),ParseTopoMatrix:
- 识别 GPU 列(
GPU\d+)与 NIC 列(mlx5_*/NIC\d+/ib*/roce*等) - 处理 NIC Legend (
NIC0 → mlx5_0) - 解析每卡 NUMA Affinity(Grace/GB200 上可能是逗号/区间;取可用整数值)
- 单元格合法值:
X、PIX、PXB、PHB、NODE、SYS、NV<n>
metadata-collector 只原样发布矩阵 ,不做 management/compute NIC 分类;分类在 nic-health-monitor/pkg/topology。
5. JSON 契约(gpu_metadata.json)
定义见 data-models/pkg/model/gpu_metadata.go。version 写死 "1.0",timestamp 为 UTC RFC3339。
5.1 顶层 GPUMetadata
| JSON 字段 | 类型 | 说明 |
|---|---|---|
version |
string | "1.0" |
timestamp |
string | 采集时刻 |
node_name |
string | os.Hostname()(注意:Chart 虽注入 NODE_NAME,当前 Go 代码用 hostname) |
driver_version |
string | NVML |
chassis_serial |
string | null | 驱动 ≥ R560 |
gpus |
array | 见下 |
nvswitches |
string\[\] | NVSwitch PCI 去重列表 |
nic_topology |
mapstring\[\]string | 可选;nic_topology[nic][i] 对齐 gpus[i] |
5.2 gpus[] / GPUInfo
| 字段 | 说明 |
|---|---|
gpu_id |
NVML index |
uuid / pci_address / serial_number / device_name |
标识 |
nvlinks[] |
仅到 NVSwitch 的链路:link_id、remote_pci_address、remote_link_id |
nvlink_link_count |
可查询状态的链路数;缺省(nil)=未知,0=确认无 NVLink 硅片路径 |
nvlink_active_link_count |
采集时 ACTIVE 数;语义见 §4.4 |
numa_node |
来自 topo -m;不可用时 -1 |
slowdown_tlimit_c |
慢降频温度偏移 °C;不支持则省略 |
5.3 原子写入(writer.go)
text
MarshalIndent → 写 output.tmp (先 0600) → chmod 0644 → rename 到正式路径
chmod 0644 是刻意设计:collector 常以 root 运行,而 syslog / nic monitor 可能非 root 读同一 hostPath;若保持 0600,下游会启动失败。rename 保证读者不会读到半截 JSON。
6. Pod 设备映射(mapper)
6.1 目标注解
常量:model.PodDeviceAnnotationName = "dgxc.nvidia.com/devices"。
示例:
json
{
"devices": {
"nvidia.com/gpu": ["GPU-455d8f70-2051-db6c-0430-ffc457bff834"],
"nvidia.com/pgpu": ["GPU-..."]
}
}
资源名白名单来自 EntityTypeToResourceNames["GPU_UUID"]:nvidia.com/gpu、nvidia.com/pgpu。
6.2 每轮步骤(UpdatePodDevicesAnnotations)
代码大段注释写得很清楚,摘要如下:
- Kubelet HTTPS
/pods:列出本节点 Pod(等价于 APIfieldSelector=spec.nodeName=...,但走本地 kubelet,减轻 apiserver 压力) - Kubelet PodResourcesLister gRPC (Unix socket,挂载
/var/lib/kubelet/pod-resources):拿到每个 Pod 实际分到的 GPU UUID(device plugin 或 DRA) - 对比现有注解 → MergePatch 增改 / JSONPatch 删除;无变化不写
jsonPatchPath 使用 JSON Pointer 转义:/metadata/annotations/dgxc.nvidia.com~1devices(/ → ~1)。
6.3 已知语义限制(上游注释原文意图)
| 问题 | 影响 |
|---|---|
| 只在「注解内容变化」时 patch | node-drainer 无法靠心跳发现陈旧注解;只能靠「请求了 GPU 却没有注解」间接发现 |
| PodResourcesLister 可能仍报告终态 Pod 占用设备(K8s <1.34 行为) | node-drainer 忽略终态 Pod,一般无害;设备迁到新 Pod 后会从旧注解移除 |
| 上游规划 | K8s ≥1.35 ResourceHealthStatus 成熟后,可考虑用 container status 替代本 mapper |
mapper 错误(kubelet 不可达、patch 失败且非 NotFound)会 终止进程,与 collector 同样走 CrashLoop。
7. Kubernetes 部署形态
7.1 工作负载
- Kind :DaemonSet(
charts/metadata-collector/templates/daemonset.yaml) - 开关 :父 Chart
global.metadataCollector.enabled(默认 true) - 滚动 :
maxUnavailable: 5%
7.2 调度条件(硬绑定)
yaml
nodeSelector:
nvidia.com/gpu.present: "true"
nvsentinel.dgxc.nvidia.com/driver.installed: "true"
即:依赖 GPU Operator / labeler 打上「有 GPU + 驱动已装」标签后才会调度。无这些标签的节点不会跑 collector,下游也就没有本机 JSON。
7.3 Pod 安全与网络
| 设置 | 值 | 原因 |
|---|---|---|
hostNetwork |
true | 访问本机 kubelet HTTPS |
hostPID |
true | 与节点进程视图一致 |
privileged + runAsUser: 0 |
true | NVML / 设备节点;覆盖镜像默认非 root 用户 |
runtimeClassName |
默认 "nvidia" |
GPU 运行时;CRI-O 环境可在 values 里省略 |
7.4 Volume
| Volume | hostPath | 用途 |
|---|---|---|
output |
/var/lib/nvsentinel |
写 gpu_metadata.json,与 monitors 共享 |
sys |
/sys(ro) |
拓扑/设备相关 |
pod-gpu-resources |
/var/lib/kubelet/pod-resources |
PodResourcesLister socket |
7.5 RBAC
| 资源 | 动词 | 用途 |
|---|---|---|
pods |
patch |
写/删设备注解 |
nodes/proxy |
get |
经 SA 访问 kubelet /pods |
7.6 Helm 旋钮
父 Chart:
| Key | 默认 | 作用 |
|---|---|---|
global.metadataCollector.enabled |
true |
是否部署 |
global.metadataPath |
/var/lib/nvsentinel/gpu_metadata.json |
--output-path,且与 monitors 路径一致 |
子 Chart values.yaml:
| Key | 默认 | 作用 |
|---|---|---|
image.repository |
ghcr.io/nvidia/nvsentinel/metadata-collector |
|
runtimeClassName |
"nvidia" |
|
kubeletHost |
Downward API status.hostIP |
mapper 访问 kubelet 的主机;可改为 {} 回退 localhost |
resources |
100m/128Mi → 500m/256Mi |
应用本身几乎只有一个业务 flag:--output-path。
8. 下游消费者(谁读、怎么读)
8.1 gpu-health-monitor
- 路径:
health-monitors/gpu-health-monitor/gpu_health_monitor/metadata/reader.py - 读同一 hostPath(
--metadata-path/global.metadataPath) - 典型用途:
get_gpu_uuid(gpu_id):给 DCGM 事件补 UUID(影响 COMPONENT_RESET / 部分 drain)- PCI、chassis serial
slowdown_tlimit_c:热余量阈值(设计 042)classify_nvlink_down:结合nvlink_*_count抑制 PCIe 卡上的 NVLink Down 误报
- 行为:懒加载;文件暂不存在时可重试,不永久缓存「空」
8.2 syslog-health-monitor
- 路径:
health-monitors/syslog-health-monitor/pkg/metadata/reader.go - 用途:
GetGPUByPCI:XID 日志里的 PCI → GPU UUID/序列号GetGPUByNVSwitchLink:SXID ↔ NVSwitch PCI + linkGetDriverVersion:选择 XID 解码表
- 无元数据时,错误关联粒度会退化到「节点级」而非「某张卡」
8.3 nic-health-monitor
- 路径:
health-monitors/nic-health-monitor/pkg/topology/topology.go - 硬依赖
nic_topology+ 有效 GPU NUMA(除非配置 override 绕过) - 启动时若文件缺失 /
gpus空 /nic_topology空,进程可直接退出 - 用矩阵区分 management / compute / storage NIC
8.4 node-drainer
- 路径:
node-drainer/pkg/informers/informers.go - 读的是 Pod 注解,不是 JSON 文件
filterPodsUsingEntity:COMPONENT_RESET+ 实体类型GPU_UUID时只驱逐占用该 UUID 的 Pod- 若启用相关开关,请求了 GPU 却没有
dgxc.nvidia.com/devices可能导致 drain 失败 - Chart 侧:
partialDrainEnabled、drainGPUPods
8.5 fault-remediation
- 无直接依赖 metadata-collector 代码或 JSON
- 间接关系:HealthEvent 若缺 GPU UUID,部分部署会把
COMPONENT_RESET降级 为整节点动作(如RESTART_VM)------见 fault-remediation values 注释 - 部分 drain 开启时,依赖 node-drainer ↔ 注解链路畅通
8.6 测试辅助
tests/helpers/metadata.go:e2e 用 ConfigMap + 拷贝 Pod 注入假元数据tilt/inject-nic-metadata-stub.sh:本地 stub
9. 可观测性
| 能力 | 现状 |
|---|---|
Prometheus /metrics |
无 |
| readiness / liveness | DaemonSet 未配置 |
| 日志 | commons 结构化 slog;agent 名 metadata-collector |
| 健康模型 | Collect 成功后 Pod 保持 Running;mapper 每 30s 打 podCount;任一步 fatal → exit 1 |
排障时优先看:NVML init 失败、nvidia-smi 不在 PATH、kubelet socket/HTTPS、nodeSelector 标签是否齐全、hostPath 权限是否 0644。
10. 测试
包内 *_test.go 覆盖:
| 包 | 重点 |
|---|---|
collector |
chassis serial 门控、NIC topo 填充、GPU 数量不一致丢弃 |
nic |
topo 解析(换行表头、Legend、ANSI、NUMA) |
nvml |
nvlink -R 输出解析 |
writer |
原子写与权限 |
mapper |
patch 决策、HTTPS/gRPC client(mock) |
make lint-test 需 CGO_ENABLED=1。
11. 局限与 NVIDIA 耦合
11.1 行为局限
- 元数据一次性采集:驱动/链路晚于启动变化需重启 Pod
- SXM 启动窗口 :
nvlink_active_link_count=0可能是 fabric 未就绪 - 注解无 TTL:只能靠变更 patch;无法主动标记 stale
- Dockerfile vs Chart:镜像默认非 root,Chart 强制 root+privileged
- ADR-010 与实现略有漂移:设计文曾提 init-container 模式;现实现为常驻 DaemonSet(内联一次 Collect + 循环 Mapper)
11.2 硬依赖(NVIDIA 栈)
- NVIDIA 驱动 + NVML(
go-nvml) - 节点上可执行的
nvidia-smi(nvlink / topo) runtimeClassName: nvidia(或等价 GPU runtime)- 节点标签
nvidia.com/gpu.present、nvsentinel.dgxc.nvidia.com/driver.installed - Device plugin / DRA 资源名:
nvidia.com/gpu、nvidia.com/pgpu - 构建基镜像:CUDA(Dockerfile 中
nvcr.io/nvidia/cuda:...)
12. 与主链路的关系(速查)
text
正常健康闭环:
metadata-collector 写 JSON + 注解
↓
gpu/syslog/nic monitors 读 JSON → HealthEvent(可带 GPU_UUID)
↓
platform-connectors → MongoDB
↓
fault-quarantine → node-drainer(注解决定部分 drain)
↓
fault-remediation → 维护 CR(如 GPUReset,按 UUID)
关掉 global.metadataCollector.enabled 时:
- 监视器仍可能上报事件,但 缺 UUID / 拓扑 / SXID 精确映射
- 部分 drain / 精确 COMPONENT_RESET 能力显著下降
- nic-health-monitor 往往无法启动
13. 建议阅读顺序
metadata-collector/main.go--- 两阶段生命周期data-models/pkg/model/gpu_metadata.go--- 对外契约pkg/collector/collector.go--- 编排与降级策略pkg/nvml/wrapper.go+topology.go--- NVML / NVLinkpkg/nvml/nvlink_parser.go+pkg/nic/topo_parser.go--- CLI 解析pkg/mapper/mapper.go(含大段注释)--- 注解语义与限制charts/metadata-collector/templates/daemonset.yaml--- 调度与挂载docs/designs/010-metadata-retrieval.md--- 设计动机- 任选一个消费者深挖:
- GPU:
gpu_health_monitor/metadata/reader.py - Syslog:
syslog-health-monitor/pkg/metadata/reader.go - NIC:
nic-health-monitor/pkg/topology/topology.go - Drain:
node-drainer/pkg/informers/informers.go
- GPU:
14. 小结
metadata-collector 是 NVSentinel 节点侧事实层 :用 NVML + nvidia-smi 固化 GPU/NVSwitch/NIC 拓扑到共享 JSON,并用 kubelet 本地 API 把「哪张 GPU 属于哪个 Pod」暴露成集群可观测注解。它本身不做故障决策,但 gpu-health-monitor 的 UUID enrichment、syslog 的 PCI/SXID 关联、nic 拓扑分类、node-drainer 的部分驱逐 都建立在它之上。理解其「一次采集 + 周期注解」与降级语义,是排查「为何事件没有 GPU_UUID / 为何无法 partial drain / 为何 NVLink 误报抑制失效」的关键入口。