- 可观测性
- 指标监控
【免费下载链接】nvidia_gpu_exporter
Nvidia GPU exporter for prometheus using nvidia-smi binary OR using NVML
本文围绕项目的 agent 指南入口 CLAUDE.md 及其真正承载内容的 AGENTS.md 展开,梳理 nvidia_gpu_exporter 如何为人类与 AI agent 组织仓库知识:exec / nvml / demo 三种采集后端的设计取舍、以真实nvidia-smi输出语料驱动的测试体系、指标命名兼容契约、构建与发布管线的关键不变量。读完后你可以掌握这个 Prometheus GPU exporter 的工程全貌,并知道在改动任何模块前应遵守哪些"从源码结构看"被反复验证过的约束。
知识库入口:一个 CLAUDE.md 与三条指针
CLAUDE.md 本身只有三行:标题加一条@AGINI.md形式的指针@AGENTS.md。这不是偷懒,而是有意的机制:GEMINI.md 和 .github/copilot-instructions.md 同样指向 AGENTS.md,因此AGENTS.md 是唯一需要编辑的单一事实来源(Copilot 那份例外,因为它的使用方会把文件原文注入而不跟随指针,其中的少量规则副本需要手动保持同步)。
文档开头就给出了两条防腐规则,是整个知识库的组织原则:
- prefer pointers over copies(指针优于副本):永远不要在这里复述 flag 列表、指标列表、chart 值、文件数量、版本号或某项进行中工作的当前状态——这些住在代码和文档里,变化频繁,一份过期的副本比没有副本更糟。
- prefer invariants over inventories(不变量优于清单):属于这里的只有"地图",以及没有写在别处的推理。
另外有一条工具链约束:task lint:assets会用 markdownlint 检查每一个被跟踪的 markdown 文件(AGENTS.md 本身也在其中),所以文档要求保持"一行一句、首行为唯一顶层标题"。
项目定位与分发面:一个刻意选择的 niche
从指南看,这个仓库是一个面向 NVIDIA GPU 的 Prometheus exporter,以单个静态 Go 二进制分发。默认路径是运行nvidia-smi并解析其 CSV 输出——正因如此,凡是有该二进制的环境(Linux、Windows、macOS、容器、WSL2)都能用,不需要 C 绑定,也不需要项目自带 NVIDIA 用户态组件。
分发面相对项目体量来说相当宽:archives、原生软件包、多个 registry 上的容器镜像、Helm chart、Windows 包清单、systemd unit、Windows 原生服务,以及 Grafana dashboards。指南明确:.goreleaser.yml 和 .github/workflows/release.yml 是"发布什么、发布到哪里"的权威来源;其中几乎一切都由发布管线从本仓库产出,唯一的人工环节是 dashboards 手动发布到 grafana.com。
指南还特意说明了项目刻意为之的定位:DCGM 和 NVIDIA GPU Operator 占据数据中心集群管理场景;这个 exporter 瞄准的是nvidia-smi能够到而更重的工具覆盖不到或不愿意覆盖的一切——消费级与专业级显卡、小集群、边缘盒子、虚拟化与受限环境、新旧混合机队,以及通过 wrapper 命令进行的远程抓取。原文给出一条实用的判断标准:"如果一个改动只对大型受管数据中心机队有意义,那它大概率不属于这个项目。"
三个采集后端:exporter 形态的根源
"exporter 形态的一切,都源于获取读数的三种方式。"指南把--collect.backend的三个取值逐一解释,源码可以完全印证。
exec:默认后端,唯一全平台可用
- 作为子进程运行
nvidia-smi并解析 CSV; - 是唯一能 honor 自定义命令的后端——远程抓取(ssh wrapper)与
sudo之类的提权 wrapper 只靠它; - 也是对着行为失常的驱动最强的隔离手段:卡死的子进程可以被杀掉,而进程内的驱动调用不行。
nvml:实验性后端,编译期字段目录
nvml通过 go-nvml 直接读驱动库,仍标注为 experimental;它依赖 cgo 和 Linux,因此只作为一个独立 release flavor 存在,其默认后端在链接期被翻转。这一点在源码里清晰可见:internal/nvmlnative/backend_nvml.go 的构建标签是//go:build linux && cgo,而 internal/nvmlnative/backend_unavailable.go 以//go:build !(linux && cgo)为其余目标提供 unavailable 桩;internal/app/app.go 中的DefaultBackend注释也说明普通构建默认为exec,nvml flavor 在构建期改写为nvml。
关键细节:nvml 的 query-field 词汇表是编译期目录而非运行时发现。internal/nvmlnative/catalog.go 同时持有目录与"延迟清单"(deferredFields与deferredFieldPrefixes)——对没有经过验证映射的字段显式拒服,因此 nvml不自动是 exec 的超集;新驱动新增的字段会先出现在 exec 的发现结果中,drift 测试再强制你把它显式加入目录或延迟清单。
demo:不是第四条代码路径
demo无 GPU、无驱动、无 Linux 要求就能提供合成数据。指南强调它不是第四条代码路径:它对进程内 fakenvidia-smi跑真实的 exec 管线,再在其上合成 NVML 专属的指标族(实现见 internal/demo/demo.go,fake 共享实现在 internal/fakesmi/fakesmi.go)。所以它是纯 Go、跨平台,且几乎免费地保持了与真实后端怪癖的一致性。
两条把三者绑在一起的不变量
- 同一字段、两个后端必须逐字节一致:凡是两个后端都提供的 query field,其输出的指标——包括 label 集合和值——必须完全相同,因为用户会在后端之间切换,dashboard 与告警必须继续工作。nvml 目录的存在正是为了保证这一点:它存储的是
nvidia-smi打印的精确表头字符串,而不是它们的转述。"拒绝提供某个字段是允许的;以不同方式提供它,不允许。" - demo 瞄准的是 nvml 面而非 exec 面,且要接近到可以在其上开发 dashboard 与告警。它是"逼近"而非"复刻",差异记录在 docs/CONFIGURE.md 的 demo 一节;指南点出最容易误导的一条:demo 总是提供 PCIe throughput 族,而真实 nvml 后端只在被要求时提供(这与 internal/app/app.go 中
PCIeThroughput: cfg.pcieThroughput || cfg.backend == backendDemo的写法一致)。
最后一个工程决定:后端无法 honor 的 flag 组合是启动错误,而不是静默忽略。用户在 nvml 后端上传自定义命令,表达的是该后端做不到的意图,大声失败是唯一诚实的回答。对应实现是 internal/app/app.go 的validateBackendFlags:--nvidia-smi-command与 nvml/demo 互斥、--collect.pcie-throughput要求 nvml、--collect.compute-apps-mig要求 nvml 或 demo 且要求--collect.compute-apps、--demo-config只属于 demo——每条规则都有内联注释说明"为什么不能静默"。
代码地图:每个包负责什么
指南给出的代码地图(对照源码目录核实):
| 包 | 职责 |
|---|---|
| cmd/nvidia_gpu_exporter | 二进制本体,刻意保持薄:进程生命周期、信号处理、构建标签后的 Windows service 与 install 命令;真正逻辑都在internal/app之后 |
| cmd/fake-nvidia-smi | 独立 fakenvidia-smi二进制,内嵌整个 capture 语料 |
| internal/app | 二进制与进程内测试共用的单一入口:flag 解析与校验、后端装配、指标注册、HTTP 服务。"测试驱动的是真实入口,而不是它的重新实现" |
| internal/collect | 与后端无关的采集调度与健康核算。Live同步采集并在并发 scrape 间共享同一 in-flight 运行;Cached按定时器采集并提供最新结果 |
| internal/exporter | 把 snapshot 变成 Prometheus 指标。条件性描述的族遵循一个先例:被禁用的特性不贡献任何 descriptor,使Describe与Collect保持一致 |
| internal/nvidiasmi | exec 后端:命令拆分、字段解析、CSV 解析、值变换、per-process 查询 |
| internal/nvmlnative | nvml 后端(linux && cgo构建标签 + 其余目标的 unavailable 桩),持有字段目录、MIG 与 XID 路径、drift 测试 |
| internal/demo | demo 后端的配置与 NVML 专属族的合成器 |
| internal/fakesmi | fakenvidia-smi实现,独立二进制与 demo 后端共享 |
| internal/captures 与 internal/capture | 录制语料与其解析器 |
| internal/demodata | demo 后端内嵌的小型精选语料子集 |
| internal/integration | 端到端测试套件 |
其中值得展开的是 internal/collect 的Snapshot类型:它把数据与健康严格分开——Table是失败时为 nil 的数据,健康状态从显式布尔读取,绝不从 error 是否非 nil 推断。指南给出的不变量是:两种模式下,失败的采集都不发布 GPU 数据,而不是回退到"最后一次好读数",这样陈旧值永远不可能被误认为实时值。而辅助族(per-process 与 nvml extras)采取软失败:缺席但不算采集失败——这与 internal/collect/collect.go 中Reading的契约注释一致(per-process 查询的结果"必须永远不能使采集失败")。在 internal/collect/live.go 的Live实现里,这个契约体现为并发 scrape 共享同一次运行、最后一个等待者离场时取消运行等细致设计。
Capture 语料库:让测试"可执行"
指南自称这是"仓库中最不寻常、也最常被误读的部分"。
问题背景:exporter 解析的文本形状随 GPU 型号、驱动版本、操作系统和负载状态而变。"拥有那批硬件是不可能的,所以项目把真实nvidia-smi输出录制为自包含的 capture 文件,并把它当作测试基质。"格式、脱敏规则与贡献流程记录在 internal/captures/README.md,指南要求"碰这个区域之前先读它"。
核心机制是capture 是可执行的:
- 独立 fake
nvidia-smi逐字回放 capture,并对任意字段子集通过"从录制的 CSV 中投影列"来应答,不内置任何关于 GPU 或字段的先验知识; - 集成套件对语料中的每一个 capture运行真实 exporter 打 fake,并把抓取结果与签入的期望文件比对;
- 因此贡献一个 capture 就自动扩展了测试矩阵,且新 capture 在期望输出被生成并评审之前会让套件失败。
在动这一块之前,指南列出了四条必须记住的后果,每条都有仓库内的证据:
- 期望
.metrics文件逐字节比较,只能用套件-update标志重新生成,绝不手改;生成的 diff 就是评审步骤,也是导出输出变化唯一可见的地方。 - LF 行尾是有承重的:.gitattributes 把语料 capture 与期望输出(以及 fuzz 种子语料)钉在
eol=lf,因为 Windows 检出时的 CRLF 转换会打断整个套件。 - 完整语料绝不能进入发布二进制:只有精选 demo 子集可以。这条由 internal/integration/deps_test.go 的
TestExporterDoesNotEmbedCaptures守护——它用go list -deps检查二进制依赖图,断言不包含internal/captures、必须包含internal/demodata。指南特别指出用依赖图而非导入图的原因:失败模式是静默的体积膨胀,而不是构建失败。 - GPU 硬件 parity 测试藏在构建标签后、永不在 CI 中运行,但 CI 会编译它,使它不会在两次稀有硬件会话之间腐烂(编译入口见 internal/integration/gpuparity_test.go;.golangci.yml 的注释也承认"linter 是唯一读它的东西")。
第四条是nvml drift 测试:internal/nvmlnative/driftcheck_internal_test.go 的TestCatalogDriftAgainstCaptures把目录与必要驱动符号对照录制的 capture 检查——按捕获名中的驱动版本区分新旧,最新驱动承担硬断言(改名、删除、表头变化、新字段都会让构建失败并告诉你该修哪里),旧驱动只校验共享表头。结果是:驱动改名或撤回某个东西,可以在离线状态下被捕获。
Fuzzing:覆盖"没见过的输出"
语料覆盖了已经见过的驱动输出,fuzz 目标覆盖没见过的。指南认为这个缺口在这里是真实的:exec 后端会解析"驱动、平台或用户 wrapper 命令打印的任何东西",而解析器里一次 panic 就能带倒整个 exporter。
仓库的约定是:
- 每个 fuzz 目标断言的是一条性质,而不只是"没有 panic",且这些性质就是调用方已经依赖的契约:解析出的表可按列安全索引、变换后的值有限、发现的字段名拼进查询后仍合法、派生指标名可注册。
task test:fuzz驱动搜索(见 Taskfile.yml 中逐个包、逐个目标的循环);task test回放种子语料,使修好的 bug 无人运行 fuzzer 也保持修好。- 目标放在被测包旁的
fuzz_test.go,需要未导出访问时放fuzz_internal_test.go——这是仓库内部包测试的惯例,也是testpackagelinter 所允许的形态(种子语料例如 internal/exporter/testdata/fuzz、internal/nvidiasmi/testdata/fuzz)。
指南还给出两条"先错后学"的规则:
- 在拥有该不变量的那一层断言,而不是更低层。指标名只在所有名字一起被构造的地方要求可注册——那里才看得到冲突,也才是一个坏 descriptor 会弄垮所有 scrape 的地方。
- 把目标限制到代码真实接收的输入域。给 fake 的投影传入它永远不会路由过去的调用形状,产生的失败描述的是测试而非代码。
指标与兼容契约:最昂贵的错误类型
"导出的指标名是公共契约,破坏它是这个仓库里可用的最昂贵错误。Grafana dashboard 有很大的装机量,野外有告警规则,因此一个被改名或改 label 的 series 会静默弄坏陌生人的监控。除非有显式决定,否则把每一个既有名字当作冻结。"
指南接着列出"经常让人吃惊的具体事项",几乎每一条都能在源码里找到对应实现:
- 指标名来自
nvidia-smi打印的返回表头字符串,而非 query field 名。这正是 nvml 目录逐字节存储表头字符串的原因,也是"驱动改名表头即等于改指标名"的原因。 nvidia_smi_前缀命名的是数据 schema 而非采集机制,在包括从不运行nvidia-smi的后端在内的所有后端保持不变。- 失败采集计数器的名字含 "scrapes" 但数的是 collections。自后台采集把两者解耦起它就是错名(misnomer),但保留,因为迁移成本落在用户身上而收益只是表面的。
- 健康指标与后端无关,唯采集状态例外——后者按后端故意不同:把 NVML 状态报在一个以进程退出码命名的指标下,会静默改变既定指标的含义(对应
ExecExitCodeMetric/NVMLReturnCodeMetric两个变体,见 internal/app/app.go)。 - 给既有族加 label 会改变 series 同一性,因此放 opt-in flag 后。per-process 指标上的 MIG 归属 label(
gpu_instance_id、compute_instance_id)就是先例。 - 报状态而非数值的字段映射成数值:unavailable 值干脆不导出;不认识的值同样不导出,但会记录日志——"对新驱动状态的猜测,比可见的缺口更糟"。
- 一个不可用的派生指标名代价是整次 scrape 而非一条 series:exporter 每次 scrape 以单个 collector 注册,注册表会整体拒绝坏 descriptor。因此返回名导出空名、或撞上其他字段/自身族的字段会被带日志丢弃。多个字段竞争同一个名字时全部丢弃:谁拥有这个派生名在此处不可知,把一个字段的读数发布在 contested 名字上会给既定 series 放上错误值。
最后两条的源码证据很直接:internal/exporter/exporter.go 中fixedMetricNames是"exporter 自有族"的单一名字清单(health、identity、per-process、nvml extras、XID),reservedMetricNames说明"每个族无论其特性是否开启都被保留,因此后来开启它不会把可用配置变成失败配置";而 internal/exporter/reserved_internal_test.go 的TestReservedMetricNamesCoverAll把这份清单钉在 exporter 实际描述的内容上——"新增一个族而不在清单里登记"这类腐化无法溜过测试。
- 每个字段列表——无论用户手写还是自动发现——在查询前都会去重并追加 identity 字段。单元格按位置分配给字段,重复字段会被查询两次、为一个 series 发两条样本,从而弄垮整次 scrape。
参考与逐族语义在 docs/METRICS.md,指南要求"表面变化时保持同步"。
Grafana dashboards:源码在 UI,真值在 docs/grafana
有两块 dashboard:per-GPU 细节视图,以及可向下钻取到它的一块多 GPU 总览,两者都发布在 grafana.com。指南给出的工作流要点:
- 在 Grafana UI 中创作,导出到 docs/grafana/——那里是真值来源;Helm chart 携带副本,CI 逐字节比对,所以两处必须一起更新。
- docs/grafana/README.md 记录着 dashboard 依赖的查询与面板约定,"改任何面板查询之前先读它:其中若干约定被违反时是静默失败的"。
- README 中展示的图片由
task screenshots生成而非人工截图:无头部分由 Grafana 官方 image renderer 完成,仓库里没有任何浏览器自动化,一次抓取就是一个 HTTP GET。renderer 做不到的唯一一件事是展开折叠的 row,所以抓取指向从已发布 dashboard 派生的 row-expanded 副本。每次运行都拍摄活的 demo 数据,像素必然不同:dashboard 变化时刷新图片并评审图片 diff(对应 hack/compose/screenshots.sh 与 Taskfile.yml 的screenshots目标)。 task lint:dashboard以 strict 模式运行 grafana dashboard-linter;规则豁免写在 dashboard 旁、每条附理由,门槛是"该规则与 dashboard 长期发布的设计冲突"。指南点了一个典型:实例选择器模板变量不叫instance且不能改名——每个被收藏的 URL 都携带当前名字,所以是豁免 linter 规则而不是服从它。
Helm chart:GPU 访问的唯一正确姿势
charts/nvidia-gpu-exporter 部署 DaemonSet,是 Kubernetes 的主安装路径。指南把其中心设计决定表述得斩钉截铁:
GPU 访问来自 NVIDIA 容器运行时,经 runtime class 与 NVIDIA 环境变量注入;exporter 永远不请求
nvidia.com/gpu资源。device plugin 是独占式分配整卡的,请求了它的监控 pod 会把卡从真实工作负载手里抢走。任何重新引入 GPU 资源请求的改动都是 bug。
其余易踩的坑:
- chart 版本与应用版本从 release tag 盖章,仓库内版本是占位符;chart major 刻意等于应用 major 加一,是 chart 历史的遗留,必须由 release job 保持。
- chart README 由 helm-docs 组装,过期时 CI 失败,绝不直接编辑 charts/nvidia-gpu-exporter/README.md:prose 在 charts/nvidia-gpu-exporter/README.md.gotmpl,只有 values 参考是生成的、来自 charts/nvidia-gpu-exporter/values.yaml 的注释。编辑其中合适的一份,然后重新生成。
- charts/nvidia-gpu-exporter/values.schema.json 严格校验 values 并拒绝未知键;新增子树而没有 schema 条目会通过评审、然后在 chart lint 与安装时失败——两者必须一起改。
- 安全上下文 values刻意跳过下面的 defaulted-lookup 约定,因此
--reuse-values升级保留旧的空值、保持未加固。指南解释这是安全方向:回填会把手上钉在旧 root 镜像 tag 的用户直接送上一条runAsNonRoot保证,在升级时弄坏他们的 pod。 - 告警规则由 values 驱动,hack/alerts/verify.sh 在无集群环境下渲染并单测它们,还模拟"复用上一 release values 的升级"——因为 Helm 在
--reuse-values下不会回填新的 chart 默认值,这正是模板使用 defaulted lookup、新的 value 子树必须保持 nil-safe 的原因。 - 告警规则带纠正动作的 annotations,验证脚本会断言关键条目在措辞改写后存活。
构建、测试与 lint:一个 Taskfile,一个工具链镜像
Taskfile.yml 是入口,镜像 CI 所跑的内容。文件头部的注释解释了整体架构:每个检查在 hack/dev.Dockerfile 构建的工具链镜像中运行、一个检查一个 stage,宿主只需要 docker 和 task,CI 因此运行完全相同的工具版本;这也是检查能看到整棵树的唯一方式——internal/nvmlnative的很大一部分在linux && cgo构建标签后,macOS 宿主工具链会把它们整体排除在构建外,宿主 linter 会"没读过这些文件也报成功"。Go 版本来自 go.mod,工具链不可能偏离模块声明。
task lint覆盖一切:Go、模块整洁度、markdown、shell 脚本、dashboards、release 配置;task fmt应用格式器;迭代时应手伸向各个task lint:*目标,因为全量集合需要安装多个外部工具。- lint 是golangci-lint,配置为默认启用全部 linter、禁用一份逐条给出理由的短清单(见 .golangci.yml 中
default: all与带注释的 disable 列表);"新代码预期是满足它,而不是累积豁免"。 - GitHub CodeQL 走默认配置,配置在仓库设置而非 workflow 文件里,所以检出中没有任何东西记录它——指南特意说明这一点,避免读者在 workflow 里找不到而误以为没有扫描。
- CI 对每次改动运行一次goreleaser snapshot:有意为之,把打包输入(systemd unit、包 scriptlets、归档布局)的失败拦在 CI 而不是留给真实发布。它为每个平台构建容器镜像但不发布——这是 Renovate 自动合并 base-digest 更新的门槛;刻意只跳过签名 stage。没有 SBOM stage:
go.mod/go.sum的模块图就是物料清单,它就在 provenance 证明所指向的提交里。
发布管线:签名、可复现与双 flavor
- 推送版本 tag 触发 release workflow;Taskfile 有从提交历史推导下一版本并推送 tag 的目标(Taskfile.yml)。tag 必须签名——workflow 的第一步拒绝 GitHub 不报告为 verified 的 tag;tag ruleset 使 release tag 不可变(不能更新、不能删除),防止通过重指 tag 复用签名。指南补充了推理链:ruleset 本身不检查 tag 签名(GitHub 的签名规则看提交而非 tag 对象),所以 workflow 里的检查才是执行点;release task 只打 main 分支顶点,workflow 拒绝不在 main 上的被 tag 提交。
- 管线从同一份源码构建两个二进制 flavor:完全静态的默认构建,与 cgo nvml flavor。包与主容器镜像显式钉死在静态构建上,因为 cgo 二进制悄悄混进 deb、rpm 或主镜像,就是发布一个"在其预期运行环境跑不起来的东西";nvml flavor 作为独立归档与带后缀的镜像 tag 发布。
- 可复现构建是刻意设计的:二进制中无构建路径、以 commit 时间作为所有时间戳、归档条目固定 owner 与 mode。owner 重要,因为归档条目记录构建者的 uid、gid 与用户名——首个这样配置的 release 只在 release runner 用户下可复现就是这个教训;mode 也要固定,因为检出文件 mode 跟随构建者 umask。另有独立 workflow 在两个 runner 上构建两次、任何校验和差异即失败,且有意在无 Go cache 下运行:cache key 只有
go.sum哈希,带 cache 的重建会恢复它本要检查的那次构建的编译包、只比较打包;两类 runner 都以同一用户构建,所以"user/umask 依赖"这一类只能靠固定 owner/mode 兜底——"不要把这两者当作冗余删掉"(该 workflow 对应 .github/workflows/reproducibility.yml)。 - 容器镜像同样可复现:commit 时间作为构建参数写入镜像时间戳,release 构建重写层时间戳;buildkit 自带的 provenance 与 SBOM 证明被关掉(都携带墙钟时间戳、每次都改变发布 index digest),由 GitHub 构建 provenance 替代。快照构建无法携带 exporter 选项(加载进 daemon 的跨平台构建),所以镜像可复现性靠人工核查:推往 scratch registry 的两次冷构建必须给出相同 index digest,而不只是相同本地镜像 ID。
- 产物双通道签名:Cosign 无密钥地签校验和文件(传递覆盖每个二进制、归档与包)、容器镜像与 OCI chart,绑定 release workflow 身份;GPG 密钥只为一件事存在——经典 Helm 仓库里打包 chart 的 provenance 文件,因为 Helm 的
--verify只接受它。只有 release job 在 goreleaser 发布任何东西之前导入该密钥,并校验导入密钥与 chart 宣称的指纹一致,不符即大声失败。chart 也在那里打包签名,另一个原因是:chart job 之后无需 secret、以 workflow token 发布,release environment(等待维护者批准)只被一个 job 进入,一次 release 只需要一次批准。密钥生成与轮换由 hack/generate-signing-key.sh 记录并执行。 - chart 发布两次:GHCR 上的 OCI 制品,以及
gh-pages分支上的 Helm 仓库;两者预期都要持续可用,撤掉任何一个都会破坏既有用户。 - 容器镜像基于distroless glibc:这是"值得维护的最小基础",不能无 libc——NVIDIA 容器运行时从宿主注入
nvidia-smi与驱动库,两者都动态链接 glibc,所以即使 exporter 二进制本身可以是静态的,镜像也必须提供 libc 及其 loader。这一点已在真实硬件上通过两种注入机制(CDI 与 legacy nvidia runtime)实证:无 libc 的基础镜像上注入的nvidia-smiexec 失败。手工拼 libc-only rootfs 能再省几 MB,但意味着永远自管这份 libc 副本及其安全跟踪,"这个选项被刻意核销了"。cgo nvml flavor 链接的是 release 构建 runner 的 glibc,因此基础镜像的 glibc 必须至少与 runner 一样新——换基础或升级 runner 时要重新核对的不变量。基础镜像不带版本 tag,所以按 digest 钉住、由 Renovate 保鲜、以 CI 构建镜像为门槛。镜像里没有 shell:派生镜像不能用 shell 形式RUN,需要 wrapper 命令的场景要用一个把 exporter 二进制拷到更完整基础上的镜像(安装指南有说明)。镜像以 uid 65534 运行,且USER指令必须保持数字形式:kubelet 无法对基于名字的镜像用户核验runAsNonRoot保证并拒绝启动容器——这也是基础保持按 digest 钉的普通镜像而非nonroottag 变体的原因。 - 一个打包暗坑被单独点名:nvml 镜像 tag 后缀解析为 semver pre-release。严格 semver 会把带 flavor 的 tag 排在同版本普通 tag 之下,而按字典序或推送时间排序的工具可能反过来选它——所以消费这些 tag 的自动化无论如何都需要显式过滤。
没有 GPU 时的开发方式
指南按设置成本从低到高给出三档:
- 用自定义命令 flag 让 exporter 打 fake
nvidia-smi二进制,回放语料中的任意 capture——适合解析器与字段工作; - 用 demo 后端运行 exporter——适合任何需要 NVML 专属族的场景,也是没有硬件时唯一能真正运行这些族的选项;
- 拉起 hack/compose 下的 compose 栈:模拟多台机器、每台同时以两种后端 flavor 提供服务,Prometheus、Grafana、Alertmanager 全接好——这是 dashboard 与告警工作的环境,其 README 记录了如何驱动特定状态、让特定告警触发。
一个对迭代很友好的细节:模拟机器配置每个采集周期重新读取,编辑一个配置在下一次 scrape 就生效,无需重启(这与 fake 每次 scrape 被重新调用、--config文件随之重读的机制一致,见 internal/captures/README.md)。
约定:提交、历史与依赖更新
- 提交遵循 conventional commit:
type(scope): imperative description标题加解释改了什么、为什么的正文。变更日志按 type 分组生成,所以 type 是功能性的而非装饰性的。 - 默认分支历史保持线性:以 rebase 或 squash 集成,绝不 merge commit;分支 rebase 到默认分支,而不是把它 merge 进来。
- 提交带 DCO sign-off。
- 任何地方不带 AI 署名:提交、PR 或别处,没有 "generated with" 行、没有 AI co-author trailer。
- 文档按受众拆分:README 负责定向,docs/ 承载安装、配置与指标参考,社区文件在 .github/;深而领域特定的文档放在它所描述的东西旁边,而不是
docs/里。 - 仓库根的 .bestpractices.json 持有 OpenSSF Best Practices 徽章问卷的答案:他们的网站从默认分支读取该文件并在表单中预填,所以项目工作方式(测试、发布、漏洞上报)的变化要反映在这个文件里而非网页表单上;bestpractices.dev 上的在线条目与该文件必须一起更新。
- 依赖由 Renovate 更新,配置为以分支形式自动合并符合条件的更新而不开 PR;.renovaterc.json 是哪些更新类别符合条件的权威。指南还留了一个排障提示:branch automerge 只在默认分支没有"合并前必须 PR"的规则时工作——如果自动更新开始以 PR 形式出现,先去找那条规则;必需的状态检查没问题,那才是实际把关者。
参考指针
指南结尾给出一张"去哪里看"的地图,全部是仓库内相对路径:
- README.md:项目是什么、为谁做。
- docs/INSTALL.md、docs/CONFIGURE.md、docs/METRICS.md:面向用户的参考。
- docs/SECURITY_MODEL.md:exporter 在安全上的承诺、信任边界及其如何被核查;docs/ROADMAP.md:计划做什么、以及刻意不做什么。
- docs/grafana/README.md:dashboard 查询与面板约定。
- internal/captures/README.md:capture 格式、贡献流程、capture 如何驱动测试。
- hack/compose/README.md:本地开发栈,包括如何诱发特定告警。
- charts/nvidia-gpu-exporter/README.md:chart 的 values 参考与部署说明。
- .github/CONTRIBUTING.md:面向贡献者的流程。
这份指南值得反复读的最后一句话在开头:"Keep it timeless."——它不维护快照,只维护地图与不变量;而上面每一条不变量,几乎都能在源码、测试或 CI 配置里找到对应的那行守卫。
- 可观测性
- 指标监控
【免费下载链接】nvidia_gpu_exporter
Nvidia GPU exporter for prometheus using nvidia-smi binary OR using NVML
相关推荐
CapRover 后端开发指南:基于 AGENTS.md 的架构分层、兼容性契约与本地验证流水线
CapRover 后端开发指南:基于 AGENTS.md 的架构分层、兼容性契约与本地验证流水线 CapRover 是一个刻意保持"小而精"的 PaaS 控制层
DevOps云原生运维Goa 仓库工程指南:读懂 AGENTS.md 中的 DSL、代码生成与开发契约
Goa 仓库工程指南:读懂 AGENTS.md 中的 DSL、代码生成与开发契约 本文基于 Goa 仓库根目录的 CLAUDE.md 及其指向的 AGENTS.
后端代码生成API设计微服务Daily UI Inspiration Capture 预期输出契约:从 6 行清单到可验证的每日 UI 灵感采集包
Daily UI Inspiration Capture 预期输出契约:从 6 行清单到可验证的每日 UI 灵感采集包 本文以 Skills 仓库中 daily
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考