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 形态(集群内没有 Prometheus CRD,靠托管侧 operator 认领 ServiceMonitor)
  • GPU:NVIDIA 消费级卡(GeForce 系,无 ECC),单机 8 卡
  • 已有监控:dcgm-exporter + 托管 Prometheus,指标 remote_write 进时序库,Nightingale(n9e)出告警,经自建"告警网关"发飞书
  • gpud v0.10.0

本文是「GPU 掉卡告警怎么做」系列第二篇。上一篇讲清了为什么 count<8XID 阈值都会失灵;本篇讲怎么落地:从一次"告警全程静悄悄"的真实掉卡,一路做到能正确恢复的告警。

链路总览

GPU掉卡监控终态(gpud状态桥)

① 内核 /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 }

逐个坑:

  1. affinity: null:chart 默认 nodeAffinity 卡 nvidia.com/gpu.product(GFD 标签,托管集群不一定有)→ 调度不上。{} 删不掉(helm 对 map 深合并),必须 null,再用 nodeSelector 指到你的 GPU 节点标签(示例 gpu=enable)。
  2. endpoint 必须置空:渲染出的启动命令会硬塞 --endpoint=...gpud-manager,telemetry.enabled=false 拦不住,只有 gpud.endpoint="" 才本地自治不外连。
  3. priorityClassName: system-node-critical 写死在模板里(values 改不了)→ 理论上能抢占低优先级 pod;渲染后 sed 删掉更稳(上面命令已含)。
  4. dry-run 验不了特权:托管集群的镜像仓库准入 webhook--dry-run=server 阶段就以 "imagePullSecrets not contain them yet" 拦下,到不了 privileged 判定——privileged 能不能上,只能靠单节点真 apply 验。
  5. 镜像 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 } }

托管集群没有 Prometheus CRD,接入无法预检,只能部署后实测:实测 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.goCheck()):

① 卡坏了 → ② 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、行为实测与三个坑

Xid119 告警数据流向(状态桥版)

验证 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 分钟 + 评估周期)。

三个实测踩出来的坑:

  1. 注入必须用该机真卡的 PCI 地址(先 nvidia-smi --query-gpu=pci.bus_id 查)。随手编个假 PCI,gpud 映射不到卡 → reason 里没有 UUID → 桥的 uuid 标签为空 → join DCGM 配不上 → 规则不触发,你会误以为链路坏了。真实故障驱动打的必是真 PCI,不受影响。
  2. set-healthy 扛不住 gpud pod 重启:它只清当前实例状态;pod 一重启(含 DaemonSet 滚动升级)会重读持久内核日志,3 天窗口内的注入残行会复活告警(节点 boot time 没变,对账判它"未处理")。规矩:注入过的节点 3 天内别动它的 gpud pod,复活了就再 set-healthy;真实故障无此问题(修复=重启节点,boot time 更新,对账兜住)。也别用"重启节点"清测试残留——既驱逐业务 Pod,还污染重启对账账本(假 Xid+真重启被记一次,凑够 2 次阈值会误升级"送修")。
  3. 时序查询的两个"骗局":查 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 被打残,数据源整块消失 触发假恢复 + 让兜底规则同时失明——故障在它最该被盯住的时刻,弄瞎了所有盯它的眼睛

两个教训:

  1. ==79 这类规则的"恢复"也保证不了为真(79 的消失 ≠ 卡修好,可能只是 exporter 不展示了/残了)——它的恢复通知也该关,逻辑同第八节:谁保证不了的话,谁就别说;
  2. 对上不了 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)。
posted @ 2026-06-11 11:14  Hello_worlds  阅读(6)  评论(0)    收藏  举报