GPU 掉卡告警怎么做(二)之 gpud 实战
一句话结论:GPU "掉卡 / 冻卡"(Xid 119 GSP Timeout)发生时,指标层会集体说谎——
kube_node_status_capacity还是满的、DCGM_FI_DEV_XID_ERRORS恒 0、功耗 / 温度 / 利用率冻在"空闲档"。唯一没被骗的是宿主机内核日志里的NVRM: Xid。所以这类故障的监控,正确做法是在每个 GPU 节点读/dev/kmsg(用 gpud)。但真正的难点不在"抓到",而在让告警能正确恢复——gpud 的 Xid 指标是个累计且持久的 counter,怎么写表达式都拧巴。本文的终态是一个 80 行的 sidecar(挂在 gpud 旁边的伴生小容器):把 gpud 藏在 JSON API 里的"当前健康"翻译成 Prometheus 指标,告警做到坏了催到修好、修好自动恢复、全程零手工。
文中所有主机名、IP、集群 ID、Pod / Deployment 名、告警群名、镜像仓库均为占位,替换成你自己的即可。
环境
- Kubernetes:托管 Prometheus 形态(集群内没有
PrometheusCRD,靠托管侧 operator 认领ServiceMonitor) - GPU:NVIDIA 消费级卡(GeForce 系,无 ECC),单机 8 卡
- 已有监控:
dcgm-exporter+ 托管 Prometheus,指标 remote_write 进时序库,Nightingale(n9e)出告警,经自建"告警网关"发飞书 gpudv0.10.0
本文是「GPU 掉卡告警怎么做」系列第二篇。上一篇讲清了为什么
count<8和XID阈值都会失灵;本篇讲怎么落地:从一次"告警全程静悄悄"的真实掉卡,一路做到能正确恢复的告警。
链路总览

① 内核 /dev/kmsg 是掉卡时唯一还能看到 Xid 的来源 → ② gpud 判定当前健康 + sidecar 翻译成指标 → ③ 托管 Prometheus 抓取 → ④ 告警规则就一句 unhealthy > 0 → ⑤ 网关富化卡片(可选)→ ⑥ 飞书,坏了催到修好、修好自动恢复。下面从故障现场一路拆到这条链。
一、起点:一台机"有硬件隐患",告警却静悄悄
云厂商发来一条主机硬件隐患预警:当前运行正常、但有较高宕机风险,建议有容灾就切走,指向一台 8 卡 GPU 机。回头一查监控才发现:这台机在预警前一天凌晨就已经有一张卡出问题了,而我们自己的告警一条都没响。
先用最干净的信号 Capacity 回溯 16 天:
# GPU 节点容量的最小值,过去 16 天
min(kube_node_status_capacity{resource="nvidia_com_gpu"})
=> 恒为 8;<8 的时刻:无
按 Capacity 口径,16 天零掉卡。 可那张卡明明坏了。问题就在这——Capacity 根本没把它算成"坏"。一层层查下去,会发现四个指标层全在说谎。
二、四层指标为什么集体说谎
2.1 Capacity 只认"摘卡",不认"冻卡"
kube_node_status_capacity{resource="nvidia_com_gpu"} 来自 nvidia-device-plugin:它用 NVML 枚举"健康可分配"的卡数报给 kubelet,只在 device-plugin 把某张卡判 unhealthy、从可分配集合摘掉时才会 8→7(典型是 Xid 79 掉总线)。而这次的卡是挂死(hang)但还在册——NVML 仍枚举得到它,device-plugin 不摘,于是 Capacity 恒 8。
2.2 DCGM 的遥测冻在"空闲值"
那张卡冻结期间的 DCGM gauge(同一节点、纯冻结窗口、163 个采样点全程恒定):
| 指标 | 冻结值 |
|---|---|
| GPU 利用率 | 0(恒定) |
| 功耗 | 48.166 W(恒定) |
| 温度 | 40 ℃(恒定) |
| SM 时钟 | 435 MHz(恒定) |
它冻在一组像"空闲"一样的值(util 0、低功耗)。所以任何"利用率低于阈值"的规则都没法把它和"真空闲"区分开;而别的机器上同类故障也见过冻在 util 99 / 满功耗——冻结值不可预测,任何静态阈值都漏。这类只能用"数值长时间零方差(stddev_over_time == 0)"去间接抓,而那是另一条专门规则、不是现成指标自带的。
2.3 DCGM 的 Xid 指标恒 0
DCGM 有专门的 Xid 指标。回放冻结窗口:
DCGM_FI_DEV_XID_ERRORS{...,gpu="7"} => min=0 max=0 全程 0
DCGM_FI_DEV_ECC_DBE/SBE_* => 全程 0(消费卡 ECC 本就 N/A)
DCGM_FI_DEV_GPU_NVLINK_ERRORS => 0
DCGM_FI_DEV_PCIE_REPLAY_COUNTER => 0
所有错误计数器全是 0。 一个走 NVML/DCGM 的"GPU 监控 exporter"在这台坏卡上,吐出来的是一组"完全健康"的数。(顺带破个误区:机器上那个现成 nvidia-smi exporter 也没救——拉它的 /metrics,全是 gpu_utilization / gpu_power / gpu_count 这类遥测,没有 xid / ecc / error 任何一个字段。它是量表,不是报警器。)
2.4 真相只在内核日志里
指标层全 0,只能上宿主机翻内核日志(注意:节点后来重启过,journalctl -k 只剩重启后的;冻卡当时的 Xid 在 /var/log/kern.log.* 轮转文件里,journald 不跨重启持久化):
NVRM: _kgspLogXid119: *** GSP Timeout ***
NVRM: Xid (PCI:0000:e1:00): 119, Timeout after 45s of waiting for RPC response
from GPU GSP! Expected function 76 (GSP_RM_CONTROL) ... name=python3
NVRM: Xid (PCI:0000:e1:00): 154, GPU recovery action changed
from 0x0 (None) to 0x1 (GPU Reset Required)
- Xid 119 = GSP RPC Timeout:卡上的 GSP(GPU System Processor)45 秒不响应 RPC → 整张卡的管理通道挂死。
- Xid 154 = GPU Reset Required:驱动判定这卡必须 reset 才能恢复;修复方式只有重启节点。
2.5 根因:凡走 NVML 的全哑了,只有内核日志活着
关键就在 Xid 119 是 GSP 挂死,而 DCGM、nvidia-smi、device-plugin 都经 NVML → GSP 这条路读卡:
- GSP 一死,NVML 读不到设备 →
XID_ERRORS不增长(恒 0)、gauge 停更冻在最后值; - 卡没从枚举消失 → Capacity 恒 8;
- 而内核 NVRM 驱动直接把 Xid 写进 kernel log,不依赖 GSP → 只有内核日志逃过一劫。
| 信号 | Xid 119 冻卡时表现 | 能抓到? |
|---|---|---|
kube_node_status_capacity |
恒 8(卡没被摘) | ❌ |
DCGM_FI_DEV_XID_ERRORS |
恒 0(GSP 死,读不到) | ❌ |
| DCGM 功耗/温度/利用率 | 冻在空闲值 | ⚠️ 只能靠 stddev==0 间接推 |
| 现成 nvidia-smi exporter | 只有遥测,无错误字段 | ❌ |
内核 NVRM: Xid 日志 |
明确 119 + 154 |
✅ |
结论很硬:要覆盖 GSP 挂死,必须绕开 NVML、直接读 /dev/kmsg。
三、选型:DCGM / gpud / NVSentinel,为什么落在 gpud
读 /dev/kmsg 抓 Xid 这条路,现成方案有四个:自己写脚本(最轻,但码表/去重/打包全得自己养,等于重造一个小 gpud)、node-problem-detector(k8s 通用,但 GPU/Xid 规则要自己喂)、gpud(Lepton/NVIDIA,单二进制 DaemonSet,自带致命 Xid 码表)、NVSentinel(NVIDIA 官方的故障自动处置平台,自用 4 万卡)。顺带交代 gpud 的来历,它不是小作坊作品:出自 Lepton AI(贾扬清创办,团队运营过 Meta / 阿里 / Uber 的大规模 GPU 集群,主力作者是前 etcd 核心维护者 Gyuho Lee),2025 年被 NVIDIA 收购后成为 DGX Cloud Lepton(GPU 算力市场 + 车队管理平台)的健康探子——官方明说平台"leverages GPUd"做持续健康监控和故障隔离。它的商业模式是典型 open-core:节点 agent(探子)开源 Apache 2.0,云端控制平面(大脑:全车队大盘 / 告警 / 自动隔离 / 根因分析)闭源收费。这个出身解释了后文会撞到的"留白":为什么 /metrics 里没有健康 gauge、自托管的告警集成要"自己接"——人家的完整方案是把你接进它的平台,自托管这块本就不是它的生意(第五节的桥因此而生)。
把三个成体系的和现网已有的 DCGM 放一张表对齐——逐项标"能 / 不能":
| 对比项 | DCGM(dcgm-exporter) | gpud | NVSentinel |
|---|---|---|---|
| 定位 | GPU 指标 exporter | 节点级检测 / 诊断 agent | 集群级故障自动处置平台 |
| 架构 | DaemonSet exporter | 单二进制 DaemonSet,无后端 | ~15 微服务 + MongoDB + gRPC |
| 做到哪步 | 出原始指标(不判健康) | 检测 → 指标 + 健康态 + 处置建议 | 检测 → 自动 cordon/drain/重启/送修 |
| GPU 遥测(功耗/温度/利用率/时钟/显存) | ✅ 能(十几个 DCGM_FI_DEV_*) |
✅ 能(accelerator_nvidia_*) |
✅ 能 |
| Xid 报错 · 应用级(13/31/43/45) | ✅ 能(XID_ERRORS) |
✅ 能 | ✅ 能 |
| Xid 报错 · 掉总线 79 | ✅ 能(+Capacity 8→7) | ✅ 能 | ✅ 能 |
| Xid 报错 · ECC(48/63/64/92/94/95) | ✅ 能 | ✅ 能(ecc 组件) | ✅ 能 |
| Xid 报错 · NVLink(74/144–150)/ 降频 | ✅ 能(部分) | ✅ 能(nvlink/hw-slowdown 组件) | ✅ 能 |
| Xid 报错 · GSP 挂死(119/120/154,NVML 僵死) | ❌ 不能(盲,Xid 恒 0) | ✅ 能(读 kmsg) | ✅ 能(syslog) |
| InfiniBand | ❌ 不能(GPU 范畴外) | ✅ 能(infiniband 组件) | ✅ 能 |
| 系统层(CPU/磁盘/内存;新版还测 NFS hang) | ❌ 不能 | ✅ 能 | ⚠️ 部分 |
| 当前健康态 + 处置建议(Unhealthy/REBOOT) | ❌ 不能(只有原始指标) | ✅ 能(/v1/states) |
✅ 能(事件库+CEL) |
| 云厂商带外硬件隐患(站内信那类) | ❌ 不能 | ❌ 不能 | ✅ 能(CSP 云事件,仅 AWS/GCP/Azure/OCI) |
| 告警能"正确恢复"吗 | ❌ 不能(只有原始指标) | ✅ 能(set-healthy/重启回 Healthy) | ✅ 能(事件状态机) |
| 强依赖 | 无 | 无 | GPU Operator + 持久卷 |
| 资源 / 规模 | 小 / 任意 | 小(实测 ~21m CPU)/ 1~N 台 | 数量级更大 / 大车队(4 万卡) |
DCGM / gpud 两列每格都在现网或 POC 核过(
accelerator_nvidia_*的 61 个指标、/v1/states的组件名都是直接拉机器上的 gpud 看到的);NVSentinel 一列据官方文档整理、未亲自部署逐格实测,看个定位差异即可。一条诚实边界:注入 Xid 119,gpud ~20s 抓到并端到端进了时序库;但"真实 GSP 挂死时 gpud 一定比 DCGM 强"严格说仍是设计推断——真实冻卡当时的 kmsg 已轮转、没法回放坐实,只有注入能证。
表里看得很清楚:绝大多数 Xid,DCGM 现网零部署就能告;真正分水岭只有一行——GSP 挂死,DCGM 不能、读 kmsg 的能,而这恰是咬过我们一次的那类。
选型按"补这个洞 + 接进现有告警"来筛:自己写 / NPD 省了部署却背上码表维护;NVSentinel 那套重机械(MongoDB + 十几个微服务 + GPU Operator)为大车队全自动闭环设计,我们规模不大用不上,且云事件只接 AWS/GCP/Azure/OCI;gpud 正好卡在"轻量 + 自带码表 + 出指标接现有告警"这个点——处置仍走自己的告警 → 人工 runbook,规模不大时反而更可控。
打个比方:DCGM 是仪表盘,gpud 是带病历本的体检医生,NVSentinel 是能收治做手术的医院——三层互补,不是三选一。
四、部署 gpud:适配坑 + 单节点 POC 到全量
4.0 开工前清单(都满足再动手)
| 前置 | 怎么确认 |
|---|---|
| 集群允许 privileged DaemonSet | 看集群有没有 PodSecurity/gatekeeper 限制;不确定就靠 4.2 第四步单节点真 apply 验 |
| 能 SSH 到 GPU 节点 | 注入测试、查端口要用 |
| 本地有 helm(只渲染,不连集群)+ kubectl(能切到目标集群 context) | helm version / kubectl config get-contexts |
| GPU 节点有统一的节点标签 | kubectl get nodes --show-labels 里找;本文全文用示例标签 gpu=enable,请整篇替换成你自己的(没有统一标签就先打一个:kubectl label node <node> gpu=enable) |
| gpud 镜像已 mirror 到集群能拉的 registry | 见下面三连;托管集群通常只给同厂商仓库自动注 imagePullSecret,直接用公网源大概率拉不动或极慢 |
镜像 mirror 三连(源用 chart values.yaml 里默认的 image.repository,在能同时访问公网和你 registry 的机器上执行):
docker pull <chart默认repository>:v0.10.0 # 看 gpud/deployments/helm/gpud/values.yaml 里的 image 段
docker tag <chart默认repository>:v0.10.0 <你的registry>/gpud:v0.10.0
docker push <你的registry>/gpud:v0.10.0 # 镜像 ~1.5GB,跨厂商直拉实测 ~20min,mirror 后内网秒拉
4.1 渲染部署清单 + 五个不改就跑不起来的坑
gpud 官方只发 Helm chart,没有现成 kubectl apply 单文件,用 helm template 离线渲染:
git clone --depth 1 https://github.com/leptonai/gpud
helm template gpud gpud/deployments/helm/gpud -n gpud -f values-gpud.yaml \
| sed '/priorityClassName: "system-node-critical"/d' > gpud.yaml
values-gpud.yaml(只列关键覆盖,其余用 chart 默认):
image: { repository: <你的registry>/gpud, tag: "v0.10.0" }
gpud:
telemetry: { enabled: false } # 不回连厂商后端
endpoint: "" # ★必须置空:否则模板硬塞 --endpoint=厂商gpud-manager 会外连
affinity: null # 删默认 nodeAffinity(它卡 nvidia.com/gpu.product);必须 null,{} 删不掉
nodeSelector: { gpu: "enable" } # 示例标签,换成你自己的
tolerations: [{ operator: "Exists" }]
securityContext: { privileged: true, runAsUser: 0 } # 读 /dev/kmsg
service: { enabled: true, port: 15132 } # 配套 ServiceMonitor 抓取(默认 false)
resources:
requests: { cpu: 200m, memory: 256Mi } # 别用默认 10m:实际 ~21m 会算成占 request 200%+ 触发比率类误报
limits: { cpu: "1", memory: 1Gi }
逐个坑:
affinity: null:chart 默认 nodeAffinity 卡nvidia.com/gpu.product(GFD 标签,托管集群不一定有)→ 调度不上。{}删不掉(helm 对 map 深合并),必须null,再用 nodeSelector 指到你的 GPU 节点标签(示例gpu=enable)。endpoint必须置空:渲染出的启动命令会硬塞--endpoint=...gpud-manager,telemetry.enabled=false拦不住,只有gpud.endpoint=""才本地自治不外连。priorityClassName: system-node-critical写死在模板里(values 改不了)→ 理论上能抢占低优先级 pod;渲染后sed删掉更稳(上面命令已含)。- dry-run 验不了特权:托管集群的镜像仓库准入 webhook 在
--dry-run=server阶段就以 "imagePullSecrets not contain them yet" 拦下,到不了 privileged 判定——privileged 能不能上,只能靠单节点真 apply 验。 - 镜像 mirror 到同厂商 registry:厂商镜像源可达但要鉴权,而托管集群只给同厂商仓库自动注 imagePullSecret → 直接 mirror 到同厂商(同 region)registry 最省事,跨厂商拉那 ~1.3GB 实测 ~20min。
4.2 落地七步:单节点验通 → 全量 → 接入抓取
测试和生产是两套集群,每段开头先 kubectl config use-context 锁到对应集群,之后都是普通 kubectl、不带集群参数,免得串。全局回滚都是 kubectl delete ns gpud(秒级,gpud 不占 GPU 名额、不在卡上跑负载,删了不影响任何 GPU 业务)。
第一步,dry-run 预检(仅语法校验):
kubectl create ns gpud
kubectl apply -n gpud -f gpud.yaml --dry-run=server
被镜像仓库准入 webhook 拦报 "imagePullSecrets not contain them yet" 是正常的;只有报 PodSecurity/gatekeeper denied 才是真被拦。
第二步,选节点 + 确认端口空闲:
kubectl get nodes -l gpu=enable -o wide # 列出全部GPU节点,挑一台低负载/非关键的做试点,下文称 NODE
ssh <NODE> 'ss -lntp | grep 15132' # 期望空(gpud hostNetwork 要绑 15132)
第三步,从全量清单派生单节点版(一条 sed:锁 hostname + 删优先级,不重渲):
sed -e '/priorityClassName: "system-node-critical"/d' \
-e 's#\(gpu: enable\)#\1\n kubernetes.io/hostname: <NODE>#' \
gpud.yaml > gpud-poc.yaml
grep -E "hostname|priorityClassName" gpud-poc.yaml # 应见 nodeSelector 多一行 hostname、无 priorityClassName
第四步,部署 + 确认只落这一台:
kubectl apply -n gpud -f gpud-poc.yaml
kubectl -n gpud get pod -o wide # 期望:仅 1 个 pod、1/1 Running、就在 NODE 上
1/1 Running 同时坐实了 dry-run 验不了的两件事:privileged 放行了 + 镜像拉到了。ImagePullBackOff → 镜像 mirror 没配好;CrashLoopBackOff → 看下一步日志。
第五步,看日志 + 端到端抓一条 Xid:
P=$(kubectl -n gpud get pod -ojsonpath='{.items[0].metadata.name}')
kubectl -n gpud logs $P --tail=30 # 无 panic、在读 kmsg/枚举 GPU
# 注入一条假 Xid(注意:PCI 地址必须用本机真卡的,先 nvidia-smi --query-gpu=pci.bus_id 查,原因见第九节)
ssh <NODE> "echo '<5>NVRM: Xid (PCI:0000:21:00): 119, poc-test' > /dev/kmsg"
kubectl -n gpud exec $P -- curl -sk https://localhost:15132/metrics | grep -i xid
# 实测 ~20s 后出现: accelerator_nvidia_xid_errors_total{uuid="GPU-...",xid="119"} 1
第六步,全量:锁定到生产集群,kubectl apply -n gpud -f gpud.yaml(不带 hostname 锁的全量版),kubectl -n gpud get pod | grep -c Running 应等于 GPU 节点数。
第七步,接入托管 Prometheus 抓取(gpud 是 HTTPS 自签 /metrics;ServiceMonitor 放进托管 Prometheus 监听的 ns、带它认领用的标签——各家不同,照集群里现有能被抓的 SM 抄):
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata: { name: gpud, namespace: <托管Prometheus监听的ns>, labels: { <认领标签>: <值> } }
spec:
namespaceSelector: { matchNames: ["gpud"] }
selector: { matchLabels: { app.kubernetes.io/name: gpud, app.kubernetes.io/instance: gpud } }
endpoints:
- { port: https, scheme: https, path: /metrics, interval: 30s, tlsConfig: { insecureSkipVerify: true } }
托管集群没有
PrometheusCRD,接入无法预检,只能部署后实测:实测 28 个accelerator_nvidia_*遥测(每指标 8 卡)进了时序库,注入的 xid 也查得到——但注意查 counter 类"是否发生过",要用 range 别用 instant(注入停止 ~5 分钟后 instant 因 stale 不返回,看着像没进库,range 一拉就在)。
部署风险——本质是"一个只读特权 DaemonSet",低且可控:
| 风险点 | 说明 / 缓解 |
|---|---|
| 特权(privileged+hostPID+hostNetwork) | 只读监控、读 /dev/kmsg 必需;不改宿主机状态 |
| 占 GPU? | 不占:不申请 nvidia.com/gpu、被动读 NVML,不抢算力 |
| 与 dcgm-exporter 冲突? | 共存无碍(都只读 NVML);实测两者并存、8 卡都读到 |
| hostNetwork 端口(默认 15132) | 上线前确认端口空,否则 gpud 起不来 |
| 回退 | kubectl delete ns gpud 秒清,不影响 GPU 业务 |
五、核心方案「状态桥」:用 sidecar 把 gpud 的"当前健康"翻译成监控指标
先用白话把这节要解决的问题说清楚。
做告警,本质上只需要一个信息:这张卡"现在"坏没坏。 坏 → 一直报,催人去修;好了 → 自动闭嘴。就这么简单的需求,却卡在一个尴尬的地方:
- gpud 的 Prometheus 指标(
xid_errors_total)说的是"历史上出过几次错"——出过一次就永远记着、擦不掉。拿"历史"做告警,修好了它也不闭嘴,怎么调参数都拧巴(踩坑细账见第八节); - gpud 其实知道"现在坏没坏"——它内部一直在判定每张卡的当前健康(Unhealthy/Healthy),还附带原因和修法。但这个判断只通过一个 JSON 接口(
/v1/states)给出,而 Prometheus 只会抓指标格式、读不了 JSON。
也就是说:答案 gpud 手里有,监控却听不懂它说的话——两边语言不通。
解法就是架一个"桥":一个 80 行的小翻译器(sidecar),把 gpud 的 JSON 回答翻译成监控认识的指标——坏了指标出现、修好指标消失,告警从此跟着真实状态走。
本节路线:gpud 的接口里有什么(5.1)→ 它的"坏没坏"判断怎么来的、可不可信(5.2,源码深潜,只想落地可跳到 5.3)→ 翻译器设计与完整代码(5.3)→ 四条命令部署(5.4)。
5.1 gpud 的 API 里有什么
全家福(都在 15132 端口,HTTPS 自签,逐个 curl 实测过):
| 端点 | 给什么 | 谁在用 |
|---|---|---|
/healthz |
gpud 进程自身活没活 | 探活 |
/v1/components |
在监控哪些组件(error-xid/ecc/hw-slowdown/fabric-manager/infiniband + cpu/disk/memory 等) | 了解覆盖面 |
/v1/states |
每个组件当前健不健康 + 原因 + 处置建议(支持 ?components= 过滤) |
桥读它 |
/v1/events |
各组件事件流水 | 排障人工看 |
/metrics |
Prometheus 遥测 + 累计 counter(61 个,无健康 gauge) | 别拿它做告警主体 |
/v1/states 长这样——坏了才 Unhealthy、真修好才回 Healthy,还自带原因和处置建议:
{ "component": "accelerator-nvidia-error-xid",
"states": [{ "health": "Unhealthy",
"reason": "XID 119 GSP_RPC_TIMEOUT ... on GPU PCI:0000:e1:00 UUID:GPU-...",
"suggested_actions": { "repair_actions": ["REBOOT_SYSTEM"] } }] }
5.2 Unhealthy 是怎么产生的(源码深潜;只想落地可直接跳 5.3)
它不是哪里存着的现成数据,是 gpud 每轮健康检查现算的结论。完整链条(源码在 components/accelerator/nvidia/xid/component.go 的 Check()):
① 卡坏了 → ② NVIDIA 驱动往内核日志写一行 "NVRM: Xid (...): 119, ..."
③ gpud 守着 /dev/kmsg 读到,解析成一条【事件】(时间/码/卡)落本地 SQLite 事件库
④ 查内置码表定级:119/79/154→Fatal;31/43→应用级
⑤ 每轮健康检查扫"回看窗口(默认 3 天)内的事件库":
cr.health = HealthStateTypeHealthy // 默认先置健康
for _, foundErr := range cr.FoundErrors { // 遍历 kmsg 匹配出的错误
if foundErr.Detail.EventType == Critical || foundErr.Detail.EventType == Fatal {
cr.health = HealthStateTypeUnhealthy // ← Unhealthy 就这一个 if 产生
break
}
}
⑥ reason=把事件拼成人话, suggested_actions=码表给的处置 → ⑦ /v1/states 吐 JSON
判定一句话:"3 天窗口内是否存在未处理的致命事件"。注意它不是"反复读日志原文"——日志行只被消费一次变事件,之后判定全靠事件库(dmesg -C 清缓冲不影响)。"未处理"的出口只有三个:节点重启(gpud 检测 boot time 变化做对账,"需重启的 Xid 后面发生了重启"=已处理;同一 Xid 熬过 2 次重启仍复发 → 处置建议自动升级"硬件送修")、set-healthy(人工 ack,要在该节点的 gpud 容器里执行:kubectl -n gpud exec <该节点pod> -c gpud -- gpud set-healthy <组件>,它默认作用于本机)、事件熬满 3 天出窗口。另有个好设计:gpud 自己不舒服(NVML 初始化失败、kmsg 读不了)也置 Unhealthy——"监工失明"本身就算故障。
这套健康生命周期正是告警想要的,只是它在 JSON API 里、监控抓不到。gpud 官方对自托管的态度也明确:它只做数据源,完整自动方案是接入它们的商业控制平面;自托管就自己接 /v1/states。于是有了桥。
5.3 桥的设计:80 行 sidecar,只翻译、不判断
gpud Pod(每个GPU节点)
├─ gpud 容器(原样,一行不改)
│ └─ /v1/states ← "现在坏没坏" + 原因 + 处置建议
└─ sidecar 翻译器(新加)
每次被抓取时问一遍 /v1/states:
Unhealthy → 吐 gpud_component_unhealthy{node,component,uuid,xid,repair} 1
Healthy → 这行不存在
三样东西怎么进 Prometheus(一一对应):
| gpud 给的 | 进指标的方式 |
|---|---|
health(坏没坏) |
用"series 存不存在"表达:Unhealthy → ... 1(1 是写死的常量,不携带信息);Healthy → 这行不存在(不是 0——否则几百条恒 0 的废 series 白占库) |
reason 里的干货 |
正则抽出 xid 码和 GPU uuid 进标签;整句不进(自由文本做标签会爆 series,Prometheus 硬规矩;人看的那句话由告警网关渲染) |
suggested_actions |
repair 标签(如 REBOOT_SYSTEM;复发升级"送修"时卡片上的建议跟着变) |
翻译器不夹带任何判断,"哪些错该告警"由两道现成的闸把关,都不需要我们手工养:
- 闸1(码级,gpud 官方养):致命 Xid 才置 Unhealthy,应用级 31/43 不触发——判断逻辑在 gpud 二进制里随版本走,升级镜像自动带新码表(实测注入 Xid 31,组件保持 Healthy、不出指标);
- 闸2(组件级,白名单):只翻译 GPU 硬件类组件,磁盘满之类的系统组件不混进 GPU 告警。
exporter 完整代码(gpud-state-exporter.py,纯标准库零依赖,可直接抄):
#!/usr/bin/env python3
# gpud-state-exporter: gpud /v1/states(当前健康) -> Prometheus 指标
import json, os, re, ssl, urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
GPUD = os.environ.get("GPUD_ENDPOINT", "https://localhost:15132/v1/states")
NODE = os.environ.get("NODE_NAME", "") # 注入 status.hostIP,与 DCGM Hostname 对齐
PORT = int(os.environ.get("EXPORTER_PORT", "9401"))
# 闸2:只翻译 GPU 硬件类组件(gpud 稳定组件名,不随码表变)
FATAL_COMPONENTS = {
"accelerator-nvidia-error-xid",
"accelerator-nvidia-error-sxid",
"accelerator-nvidia-ecc",
"accelerator-nvidia-hw-slowdown",
"accelerator-nvidia-fabric-manager",
}
_CTX = ssl.create_default_context()
_CTX.check_hostname = False
_CTX.verify_mode = ssl.CERT_NONE # gpud 是自签 https
_XID = re.compile(r"\bX?ID[ :]?(\d+)\b", re.I) # 从 reason 抽 xid 码
_UUID = re.compile(r"(GPU-[0-9a-fA-F-]{36})") # 从 reason 抽 GPU UUID
def _esc(v):
return str(v).replace("\\", "\\\\").replace('"', '\\"')
def fetch_states():
req = urllib.request.Request(GPUD)
with urllib.request.urlopen(req, timeout=8, context=_CTX) as r:
return json.loads(r.read().decode())
def render():
lines = ["# TYPE gpud_state_scrape_ok gauge",
"# TYPE gpud_component_unhealthy gauge"]
ok = 0
try:
data = fetch_states()
ok = 1
for comp in data:
cname = comp.get("component", "")
if cname not in FATAL_COMPONENTS: # 闸2:非 GPU 硬件类,跳过
continue
for st in comp.get("states", []) or []:
# 闸1 的结果在这里消费:只翻 gpud 判了 Unhealthy 的
if str(st.get("health", "")).lower() != "unhealthy":
continue # Healthy 不输出(无 0,靠 series 消失表达恢复)
reason = st.get("reason", "") or ""
mx = _XID.search(reason)
mu = _UUID.search(reason)
sa = st.get("suggested_actions") or {}
repair = ",".join(sa.get("repair_actions", []) or [])
labels = ('node="%s",component="%s",name="%s",uuid="%s",xid="%s",repair="%s"'
% (_esc(NODE), _esc(cname), _esc(st.get("name", "")),
_esc(mu.group(1) if mu else ""), _esc(mx.group(1) if mx else ""),
_esc(repair)))
lines.append('gpud_component_unhealthy{%s} 1' % labels) # 1=写死常量,信息在"存在"
except Exception:
ok = 0
lines.append('gpud_state_scrape_ok %d' % ok) # 桥自身存活基线(恒在)
return ("\n".join(lines) + "\n").encode()
class H(BaseHTTPRequestHandler):
def do_GET(self):
if self.path.rstrip("/") in ("/metrics", ""):
body = render()
self.send_response(200)
self.send_header("Content-Type", "text/plain; version=0.0.4")
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404); self.end_headers()
def log_message(self, *a):
pass
if __name__ == "__main__":
HTTPServer(("0.0.0.0", PORT), H).serve_forever()
5.4 部署四件套
sidecar 的 DaemonSet patch(ds-patch.json,strategic merge 按容器名追加,不动 gpud 容器):
{"spec":{"template":{"spec":{
"containers":[{
"name":"state-exporter",
"image":"python:3.12-alpine",
"command":["python3","/bridge/gpud-state-exporter.py"],
"env":[
{"name":"GPUD_ENDPOINT","value":"https://localhost:15132/v1/states"},
{"name":"EXPORTER_PORT","value":"9401"},
{"name":"NODE_NAME","valueFrom":{"fieldRef":{"fieldPath":"status.hostIP"}}}
],
"volumeMounts":[{"name":"bridge","mountPath":"/bridge"}],
"resources":{"requests":{"cpu":"50m","memory":"64Mi"},"limits":{"cpu":"200m","memory":"128Mi"}}
}],
"volumes":[{"name":"bridge","configMap":{"name":"gpud-state-exporter"}}]
}}}}
四条命令(gpud 本体零改动、不影响 GPU 业务,回滚就是反向三条 patch)。动手前先确认 9401 端口在节点上空闲(gpud 是 hostNetwork,sidecar 跟着直接绑节点端口):ssh <NODE> 'ss -lntp | grep 9401' 期望为空,被占就换 EXPORTER_PORT。
# 1 exporter 代码进 ConfigMap
kubectl -n gpud create configmap gpud-state-exporter --from-file=gpud-state-exporter.py
# 2 DaemonSet 加 sidecar
kubectl -n gpud patch ds gpud --type=strategic --patch-file=ds-patch.json
# 3 Service 加 9401 端口
kubectl -n gpud patch svc gpud -p '{"spec":{"ports":[{"name":"http-state","port":9401,"targetPort":9401}]}}'
# 4 ServiceMonitor 加 endpoint(http 明文 9401)
kubectl -n <托管prom的ns> patch servicemonitor gpud --type=json -p '[{"op":"add","path":"/spec/endpoints/-","value":{"port":"http-state","scheme":"http","path":"/metrics","interval":"30s"}}]'
滚动完成后,每个节点的 gpud pod 变 2/2;gpud_state_scrape_ok == 1 的节点数 = GPU 节点数,就是桥的存活基线。
六、告警规则:一句 > 0,join DCGM 借标签
在你的告警平台上新建一条规则(我们用 n9e,Alertmanager / Grafana alerting 等任何支持 PromQL 的平台同理;数据源选桥指标所在的那个时序库)。桥的指标本身就是"现在坏没坏",表达式不再需要 increase / 窗口 / 任何补偿手段:
max by (cluster_env, Hostname, gpu, uuid, xid, repair) (
gpud_component_unhealthy{cluster_env="<env>", component="accelerator-nvidia-error-xid"}
* on(uuid) group_left(Hostname, gpu)
(label_replace(DCGM_FI_DEV_GPU_UTIL{cluster_env="<env>"}, "uuid", "$1", "UUID", "(.*)") * 0 + 1)
) > 0
三块逐段拆,都是踩出来的:
- 核心条件就一句
gpud_component_unhealthy > 0:这卡现在坏着。 - join DCGM 借
Hostname/gpu:桥的指标只有node+uuid,没有 gpu 序号;而富化卡片(下一节)认 DCGM 那套Hostname+gpu(0–7)。DCGM 同时有UUID+Hostname+gpu,所以按 UUID join 借过来——label_replace先把 DCGM 的大写UUID复制成小写uuid才能对上;* 0 + 1把 DCGM 的值抹成 1(只借标签不改值);group_left保留左侧全部标签。GSP 挂死时 DCGM 遥测虽冻结但 series 还在,join 仍能匹配。 max by(...)折叠掉pod标签:指标带pod(sidecar 自己的 pod 名),gpud pod 一重启,时序库里会同时存在多条不同pod的 series,n9e 按标签全集 hash 区分事件 → 同一张卡发好几条重复告警。max by折叠后一张物理卡只出一条。
规则参数:for=60s(防单次抓取抖动)、重复通知 30 分钟(没修就一直催)、恢复通知=开——这是和 counter 方案最大的姿态差异:桥的恢复是 gpud 确认"卡真的好了",是真话,放心开。
顺带把"哪些 Xid 该告"的底数列清(含义据 NVIDIA Xid 文档,分级据 gpud 码表;这套分级就是闸1,已内置,列出来是让你知道它在拦什么):
| Xid | 含义 | 该不该告 / 处置 |
|---|---|---|
| 13 / 31 / 43 / 45 | 图形引擎异常 / 显存 page fault / GPU 停止处理 / 抢占清理 | 应用级(多为业务代码非法访问),不告 |
| 48 | Double Bit ECC | 致命 → reset(数据中心卡;消费卡无 ECC) |
| 62 / 64 / 95 | 微控制器 halt / ECC 行重映射失败 / Uncontained ECC | 致命 → 重启 node,64/95 疑似坏卡 |
| 74 / 144–150 | NVLink / NVSwitch 链路错误 | 致命 → 查 NVLink + 重启 node |
| 79 | 掉总线(GPU fell off the bus) | 致命 → 重启整机(这种 Capacity 会 8→7、DCGM 也能感知) |
| 109 / 110 | 上下文切换超时 / 安全 fault | 致命 → reset |
| 119 / 120 | GSP 挂死(本文主角) | 致命 → 重启 node(gpu-reset 常失败);唯 kmsg 可见 |
| 154 | GPU Reset Required(常跟在 119 后) | 致命 → 重启 node |
七、告警卡片富化:卡号 / 关联Pod / 受影响Deploy / Xid码+处理建议(可选增强)
先说清依赖,免得照抄卡住:本节是可选的体验增强,前提是你的告警链路里有一个自己可控的 webhook 网关(告警平台 → 你的网关 → 飞书/钉钉/企微)。没有的话,跳过本节完全不影响告警本身——第六节的规则已带
Hostname/gpu/xid/repair标签,告警平台原生通知里都能看到,只是没有"逐卡 Pod 列表/受影响 Deploy"这层增值。下面给的是网关里的实现逻辑(Python 伪码),抄思路即可。
掉一张卡通常要重启整机、驱逐节点上全部 Pod,所以卡片要展示整机影响面。网关收到告警(带 Hostname)后,反查该节点每张卡上的 Pod:
# 反查: count(DCGM_FI_DEV_GPU_UTIL{Hostname=...}) by (gpu, pod, namespace)
# 逐卡列出,掉卡那张标 ◀掉卡 + Xid码;再按 deployment 聚合"受影响 N / 全集群 M"
lines = ["本节点关联Pod"]
for gpu, ns, pod in rows:
mark = " ◀掉卡" + fmt_xid(dead_xid) if str(gpu) == str(dead_gpu) else ""
lines.append(" gpu%s %s/%s%s" % (gpu, ns, pod, mark))
Xid 码 → 含义 / 处理建议在渲染侧配一张表(原生 Prometheus 没有"标签值转数值"的函数,硬塞进触发值要逐码 or,不如直接读 xid 标签):
XID_INFO = {
"79": {"desc": "掉总线", "action": "需重启整机"},
"119": {"desc": "GSP挂死", "action": "需重启node(gpu-reset常失败)"},
"120": {"desc": "GSP错误", "action": "需重启node"},
"154": {"desc": "需reset", "action": "需重启node"},
"64": {"desc": "显存remap失败", "action": "需重启node,疑似坏卡"},
"95": {"desc": "不可控显存错", "action": "需重启node,疑似坏卡"},
# ... 其余致命码同理
}
def fmt_xid(xid):
info = XID_INFO.get(str(xid), {})
s = " Xid%s" % xid
if info.get("desc"): s += "(%s)" % info["desc"]
if info.get("action"): s += " · %s" % info["action"]
return s # 未知码优雅降级成 " Xid<code>"
成品卡片(Pod/Deploy 名已脱敏):
告警实例: 192.168.1.10 / gpu7
本节点关联Pod
gpu0 ns-a/biz-workload-a-xxxxx
gpu1 ns-a/biz-workload-a-yyyyy
...
gpu6 ns-a/biz-client-b-mmmmm
gpu7 ns-a/biz-workload-a-zzzzz ◀掉卡 Xid119(GSP挂死) · 需重启node(gpu-reset常失败)
受影响的deploy
ns-a/biz-workload-a (本节点 6 个受影响 / 全集群共 N 个)
ns-a/biz-client-b (本节点 2 个受影响 / 全集群共 M 个)
值班一眼能看到:这台的 gpu7 因 Xid119(GSP 挂死)掉了、需重启 node;重启会驱逐整机 8 个 Pod,落在哪两个 Deployment、占全集群多大比例都列了出来。
八、为什么不能直接用 counter:恢复陷阱
第五节开头说 counter "怎么写都拧巴",这里把账算清——这是整个工程里翻车最多的地方,也是桥存在的全部理由。
先把"恢复"的机制说穿:告警系统(n9e/Alertmanager 都一样)判恢复就一句话——这轮查询不再命中,就算恢复。它不知道卡好没好,只知道条件还成不成立;开了恢复通知,它就把"条件不成立"当"问题没了"广播出去。恢复通知是不是真话,完全取决于"查询不命中"是否等于"问题解决了"。拿 counter 怎么写,这个等号都不成立:
写法一:> 0 → 永不恢复。 这个 counter 累计且持久——为清掉一条测试注入的假 Xid,把能想到的全试了:
| 操作 | 结果 |
|---|---|
| 删 gpud pod | 新 pod 起来仍 xid=N |
dmesg -C 清内核 ring buffer + 重启 pod |
仍在 |
删 gpud 状态库 /var/lib/gpud/gpud.state* + 重启 |
仍在 |
--force --grace-period=0 强杀 + 删库 |
还是在 |
根因:gpud 把事件持久化在 SQLite,优雅退出还会把内存计数刷回库;强杀+删库+清缓冲后还能数出来,只能来自持久内核日志(kern.log,gpud 重建历史事件时会读)。NVIDIA 官方也明说 Xid "error counters persist for the life of the GPU"——它就是设计成抹不掉的。于是 >0 一旦命中永久触发:真掉卡修好重启后,gpud 从持久日志又数出来,告警永不恢复(实测刷过群)。
写法二:increase[窗口] → 假恢复。 GSP 挂死通常只打一次 Xid,窗口一滑过、increase 归 0,告警恢复了可卡还坏着,值班被"恢复"误导撤掉关注。把窗口拉长?窗口长度既是"催多久"也是"修好后多挂多久"——3 小时窗口能催 6 次,但 1 小时修好的卡剩下 2 小时还在催,要么忍、要么修完去手动点静默。修好了还得伺候告警闭嘴——在这个方案上调参数,永远调不出两头都对。
一张表收口,三种方案对同一场景:
| 告警 | 没修时 | 修好后 | |
|---|---|---|---|
>0(counter) |
✅ | ✅ 一直报 | ❌ 永远报不停 |
increase[3h] |
✅ | ⚠️ 只催 3 小时 | ❌ 尾巴催 + 手动静默 |
| 状态桥 | ✅ 同款富化卡 | ✅ 催到修好 | ✅ 自动恢复 + 真恢复通知 |
病根一句话:Xid counter 是"历史上发生过什么"(事件),告警要的是"现在还坏不坏"(状态)——拿事件冒充状态,修没修好它根本不知道。 桥做的事就是把数据源换成真正的状态。
九、端到端验证:SOP、行为实测与三个坑

验证 SOP 三步(全程在测试群):
1. 注入: echo '<5>NVRM: Xid (PCI:0000:21:00): 119, test' > /dev/kmsg
2. 验证: 1-2 分钟富化告警卡到群;不处理则每 30 分钟催
3. 收尾: kubectl -n gpud exec <该节点pod> -c gpud -- gpud set-healthy accelerator-nvidia-error-xid
→ 几分钟后告警自动恢复 + 恢复通知到群
行为全景(均实测):
| 场景 | 行为 |
|---|---|
| 掉卡 | 1-2 分钟首报(gpud 抓 kmsg ~20s + 抓取 30s + 评估 60s) |
| 5 小时没人修 | 一直 Unhealthy,每 30 分钟催,催到修为止(上限=3天事件窗口) |
| 30 分钟修好(重启节点) | gpud 重启对账自动回 Healthy → 指标消失 → 告警自动恢复 + 真恢复通知,零手工 |
| 修好后又复发 | 新事件再告警;同一 Xid 熬过 2 次重启仍复发 → 处置建议自动改"送修" |
| 注入应用级 Xid 31 | 不报(闸1 官方分级,保持 Healthy) |
实测一轮闭环的时间线:17:43 告警(卡片与 counter 版一个像素不差,触发时值是干净的 1)→ set-healthy → 17:50 自动恢复+恢复通知,7 分钟闭环(失效判定 ~5 分钟 + 评估周期)。
三个实测踩出来的坑:
- 注入必须用该机真卡的 PCI 地址(先
nvidia-smi --query-gpu=pci.bus_id查)。随手编个假 PCI,gpud 映射不到卡 → reason 里没有 UUID → 桥的uuid标签为空 → join DCGM 配不上 → 规则不触发,你会误以为链路坏了。真实故障驱动打的必是真 PCI,不受影响。 set-healthy扛不住 gpud pod 重启:它只清当前实例状态;pod 一重启(含 DaemonSet 滚动升级)会重读持久内核日志,3 天窗口内的注入残行会复活告警(节点 boot time 没变,对账判它"未处理")。规矩:注入过的节点 3 天内别动它的 gpud pod,复活了就再 set-healthy;真实故障无此问题(修复=重启节点,boot time 更新,对账兜住)。也别用"重启节点"清测试残留——既驱逐业务 Pod,还污染重启对账账本(假 Xid+真重启被记一次,凑够 2 次阈值会误升级"送修")。- 时序查询的两个"骗局":查 counter 类"是否发生过",用 range 别用 instant(停止 ~5 分钟后 instant 因 stale 不返回);反过来 range 会把已消失 series 的末样本"续命"最多 ~5 分钟(回看填充),你看到的"最后样本时间"可能比真实停止晚——对账故障时间线时两头都得留神。
十、真实掉总线案例:故障连监控本身都能打残
桥验证收尾的同一天,另一个 GPU 集群(vcluster 租户形态,没有宿主机权限、上不了 gpud,只有 DCGM)来了一次真实的 Xid 79 掉总线。它把本文的结论当场验证了一遍,还暴露了一个前面没见过的新问题——故障可以把监控组件(dcgm-exporter)本身打残。时间线(T0=掉卡时刻):
T0 掉总线,dcgm-exporter 输出 err_code="79" 的 series,值 79
T0+1min 基于 DCGM 的 `==79` 告警规则触发 ✓(报对了,富化卡也对)
T0+10min ★ 故障把 dcgm-exporter 打残:
- 79 series 消失 → `==79` 不命中 → 规则发了"恢复通知" ✗(卡根本没修!)
- POWER 指标整个消失(8→0) → 2.2 说的那条"stddev==0 抓冻结"兜底规则三指标 and 断链,永久失明
- 全部 pod 标签丢失(8→0) → 该兜底规则的 pod!="" 过滤再补一刀
此后 卡仍坏着(温度/时钟全冻结),但没有任何活跃告警盯着它
第二节见过指标骗人的两种方式:冻结(遥测停在假值上,阈值规则全失效)和恒 0(错误计数器沉默,Xid 规则漏报)。这个案例添了第三种,而且最狠:
| 骗法 | 案例 | 杀伤 |
|---|---|---|
| 冻结(说谎) | GSP 挂死时遥测停在"空闲值" | 阈值类规则全失效 |
| 恒 0(沉默) | GSP 挂死时 XID_ERRORS 全程 0 |
Xid 类规则漏报 |
| 残缺(本案例新见) | 掉总线 10 分钟后 exporter 被打残,数据源整块消失 | 触发假恢复 + 让兜底规则同时失明——故障在它最该被盯住的时刻,弄瞎了所有盯它的眼睛 |
两个教训:
==79这类规则的"恢复"也保证不了为真(79 的消失 ≠ 卡修好,可能只是 exporter 不展示了/残了)——它的恢复通知也该关,逻辑同第八节:谁保证不了的话,谁就别说;- 对上不了 gpud 的集群,可以把"数据残缺"本身做成告警反杀这个模式:
# 有温度指标却没功耗指标的节点 = exporter 残了 = 大概率出了硬件事
count by (Hostname) (DCGM_FI_DEV_GPU_TEMP{cluster_env="<env>"})
unless count by (Hostname) (DCGM_FI_DEV_POWER_USAGE{cluster_env="<env>"})
它是状态型的:exporter 残着就一直报,节点重启修复(POWER 回来)自动消——恢复语义是真的,相当于给没有 gpud 的集群配了个简版状态桥。
十一、这套覆盖不了什么(别造成假性安全感)
- fake-node / vcluster 这类租户集群:没有宿主机访问,gpud 这种要 privileged + 读
/dev/kmsg的上不去,只能靠平台自带健康巡检 + 上一节的"残缺告警"兜。 - 云厂商"硬件隐患"那类带外站内信:平台从 BMC/带外看到的(容器内看不到),gpud 不接;要覆盖只能单独对接云厂商事件 API。
- 本质:DCGM/gpud 都是"故障后被动检测",覆盖不了"当前正常、但有隐患"的预测性信号(本文开头那条站内信就属于这类)。
快速参考
判断"冻卡"而不是"空闲"(无 gpud 时的指标侧兜底)
(stddev_over_time(DCGM_FI_DEV_POWER_USAGE{pod!=""}[15m]) == 0)
and (stddev_over_time(DCGM_FI_DEV_GPU_TEMP{pod!=""}[15m]) == 0)
and (stddev_over_time(DCGM_FI_DEV_SM_CLOCK{pod!=""}[15m]) == 0)
致命 Xid 速记
| Xid | 含义 | 处理 |
|---|---|---|
| 79 | 掉总线(Capacity 会 8→7) | 重启整机 |
| 119 | GSP RPC Timeout(GSP 挂死,指标全瞎只内核日志有) | 重启 node(gpu-reset 常失败) |
| 120 | GSP Error | 重启 node |
| 154 | GPU Reset Required(常跟在 119 后) | 重启 node |
| 64 / 95 | 显存 remap 失败 / 不可控显存错 | 重启 node,疑似坏卡 |
终态告警规则(状态桥版)
max by (cluster_env, Hostname, gpu, uuid, xid, repair) (
gpud_component_unhealthy{cluster_env="<env>", component="accelerator-nvidia-error-xid"}
* on(uuid) group_left(Hostname, gpu)
(label_replace(DCGM_FI_DEV_GPU_UTIL{cluster_env="<env>"}, "uuid", "$1", "UUID", "(.*)") * 0 + 1)
) > 0
# for=60s,重复30min(催到修好),恢复通知=开(桥的恢复是真的)
桥部署四件套速记:exporter 进 ConfigMap → DS patch 加 sidecar(python:3.12-alpine,读 localhost:15132/v1/states,吐 :9401/metrics)→ svc 加 9401 → SM 加 endpoint。回滚=反向三条 patch。
gpud 关键认知(本文最该记住的)
accelerator_nvidia_xid_errors_total是累计持久 counter(sqlite + 重读持久内核日志),> 0永不恢复、increase假恢复——别拿它做告警主体。- gpud 的
/metrics(61 个)没有健康 gauge;"当前健康"只在/v1/states——用 sidecar 桥接出来才是正解。 - gpud 判健康 = "3 天事件窗口内是否存在未处理的致命事件";出口三个:节点重启(boot time 对账)、
set-healthy(在该节点 gpud 容器里执行)、事件出窗口;同一 Xid 熬过 2 次重启仍复发 → 处置建议自动升级"送修"。 - 测试注入用真卡 PCI;收尾用 set-healthy(注意它扛不住 pod 重启重读 kern.log,注入过的节点 3 天内别动它的 gpud pod)。
几条铁律
- GSP 挂死(119/120)DCGM/nvidia-smi/Capacity 全盲,只有
/dev/kmsg有 → 必须读 kmsg。 - 取证:冻卡当时的 Xid 在
/var/log/kern.log.*,zgrep -i xid /var/log/kern.log*(节点重启后journalctl -k没有旧日志);/dev/kmsg读取受dmesg_restrict限制(=1 时需 root +CAP_SYSLOG)。 - 规则务必
max by去掉pod标签,否则重启过 gpud pod 会发重复告警。 - 告警"恢复"语义要诚实:恢复条件对应不上"问题解决了"的,恢复通知一律关;只有状态型信号(桥的 Unhealthy、stddev 冻结、数据残缺)才配发恢复。
- 故障可能打残数据源本身(掉总线把 exporter 弄残)→ 假恢复+兜底失明;"数据残缺"本身要做成告警(
TEMP unless POWER)。

浙公网安备 33010602011771号