Table of Contents
Toggle1. 项目概览
这是一个非常轻量的 Go 单体服务,职责只有一件事:定时调用 nvidia-smi 采集 GPU 指标,然后通过 HTTP 接口和内嵌 Dashboard 页面展示出来。
项目当前没有拆分多层目录,也没有引入第三方依赖,核心逻辑几乎全部集中在一个文件里:
cmd/server/main.go
这意味着它的优点是结构简单、启动快、依赖少;缺点是随着功能增加,单文件会很快变得难维护。
2. 当前目录结构
基于当前仓库,和业务直接相关的文件主要是:
cmd/server/main.goconfig/config.yamlgo.mod
3. 代码框架
3.1 单文件服务结构
main.go 里主要包含以下几类内容:
- 数据结构定义
- Dashboard HTML 模板
- 服务启动逻辑
- 定时采集逻辑
- HTTP 处理器
- GPU 数据采集与解析
- JSON 输出和数值转换辅助函数
也就是说,这个项目本质上是一个:
采集器 + 内存快照 + HTTP 服务 + 前端模板
4. 核心数据结构
4.1 gpuStat
用于表示单张 GPU 的采集结果,字段包括:
IndexNameUtilizationMemoryUsedMBMemoryTotalMBTemperatureCPowerW
这些字段最终会被序列化成 JSON,给前端和接口调用方使用。
4.2 snapshot
用于表示某次完整采集的快照:
CollectedAt:采集时间GPUs:当前所有 GPU 的状态LastError:最近一次采集错误
设计上,这个结构是“接口输出模型”,后端每次采集后都会整体替换它。
4.3 monitor
type monitor struct {
mu sync.RWMutex
snapshot snapshot
}
它是整个服务的状态容器:
snapshot保存最新一份 GPU 状态mu用来保护并发读写
这是项目的状态中心。
5. 服务启动步骤
main() 的启动步骤比较清晰,可以按顺序理解:
5.1 解析命令行参数
interval := flag.Duration("interval", 2*time.Second, "poll interval, e.g. 2s")
addr := flag.String("addr", ":8080", "http listen address")
支持两个参数:
-interval:采集周期,默认2s-addr:监听地址,默认:8080
5.2 初始化服务状态
service := &monitor{}
service.refresh()
这里会先执行一次 refresh(),目的是服务刚启动时就有一份初始快照,而不是等第一个定时器触发后才有数据。
5.3 创建可取消上下文
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
这一段用来监听系统退出信号:
SIGINTSIGTERM
收到信号后,后台采集循环和 HTTP 服务都会走优雅退出流程。
5.4 启动后台采集协程
go service.run(ctx, *interval)
这里启动一个 goroutine,按固定周期执行 GPU 采集。
5.5 注册 HTTP 路由
mux.HandleFunc("/", service.handleDashboard)
mux.HandleFunc("/healthz", service.handleHealthz)
mux.HandleFunc("/api/v1/gpus", service.handleGPUs)
当前只有 3 个路由:
/:返回 HTML Dashboard/healthz:返回健康状态/api/v1/gpus:返回 GPU 快照 JSON
5.6 启动 HTTP Server
server.ListenAndServe()
同时还起了一个 goroutine 监听 ctx.Done(),在收到退出信号后调用:
server.Shutdown(shutdownCtx)
这就是标准的 Go 优雅停机模型。
6. 后台采集流程原理
后台采集的核心在 run() 和 refresh()。
6.1 run()
ticker := time.NewTicker(interval)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
m.refresh()
}
}
逻辑很直接:
- 创建一个周期定时器
- 每次 tick 到达时执行一次
refresh() - 收到退出信号时结束循环
6.2 refresh()
refresh() 做 3 件事:
- 调用
collectGPUStats()采集 GPU 数据 - 组装成新的
snapshot - 用写锁替换旧快照
关键点是“整体替换快照”,而不是边采集边更新某几个字段。这样做的好处是:
- 接口读取到的数据是一致的
- 不容易出现半更新状态
- 并发模型简单
7. GPU 采集原理
采集核心在 collectGPUStats()。
7.1 调用外部命令
exec.Command(
"nvidia-smi",
"--query-gpu=index,name,utilization.gpu,memory.used,memory.total,temperature.gpu,power.draw",
"--format=csv,noheader,nounits",
)
也就是说,这个项目并不直接调用 NVIDIA SDK,而是依赖系统里已有的 nvidia-smi。
这带来两个前提条件:
- 机器必须安装 NVIDIA 驱动
nvidia-smi必须在 PATH 中可执行
7.2 CSV 解析
命令输出后,代码使用:
csv.NewReader(strings.NewReader(string(output)))
解析为多行记录,每一行代表一张 GPU。
7.3 字段转换
每条记录长度必须是 7,否则报错:
if len(record) != 7 {
return nil, fmt.Errorf("unexpected nvidia-smi record length: got %d, want 7", len(record))
}
然后分别转换成:
- int
- float64
- string
最终拼成 []gpuStat。
7.4 容错方式
mustAtoi() 和 mustAtof() 转换失败时直接返回 0,不会中断整个流程。
这种策略的特点是:
- 服务更稳定
- 单个字段异常不会导致整次采集失败
但副作用也很明显:
- 某些解析问题会被静默吞掉
- 数据错误不一定能被第一时间发现
8. HTTP 接口原理
8.1 /api/v1/gpus
逻辑最简单:
- 加读锁
- 读取当前快照
- 输出 JSON
这是真正给前端使用的主数据接口。
8.2 /healthz
健康检查的判定规则是:
- 如果
LastError == "",返回200 ok - 如果
LastError != "",返回503 degraded
返回内容包含:
statuscollected_atgpu_countlast_error
这个接口适合被:
- 反向代理
- 容器探针
- 监控系统
用于判断服务是否正常采集。
8.3 /
根路径直接输出 dashboardTemplate,没有独立前端工程,也没有静态资源目录。
也就是说前端是“后端内嵌模板页面”。
9. Dashboard 页面工作原理
页面模板完全写在 Go 源码字符串里,通过:
template.Must(template.New("dashboard").Parse(...))
在启动时完成解析。
前端逻辑是纯原生 JavaScript,没有任何框架。
9.1 首次加载
页面加载后立即执行:
load();
9.2 定时刷新
然后每 2 秒执行一次:
setInterval(load, 2000);
9.3 load() 做了什么
load() 的步骤如下:
- 调用
/api/v1/gpus - 把返回 JSON 解析成对象
- 更新 GPU 数量和采集时间
- 渲染每张 GPU 卡片
- 如果有
last_error,显示降级状态 - 如果请求失败,显示断连状态
9.4 页面呈现模型
前端不会保留复杂状态,而是每次拿到完整快照后整体重绘卡片区域。
这种方式适合当前这种小体量页面,因为:
- 数据量很小
- 实现简单
- 不需要引入状态管理
10. 并发与线程安全设计
这个项目的并发模型非常简单:
- 一个 goroutine 周期性写快照
- 多个 HTTP 请求并发读快照
通过 sync.RWMutex 实现:
refresh()用写锁handleHealthz()/handleGPUs()用读锁
这套设计对当前场景是足够的,因为:
- 写频率低
- 每次写入内容很小
- 读请求只读内存
11. 运行步骤原理
如果从源码运行,实际步骤是:
- Go 编译
cmd/server/main.go - 进程启动后解析参数
- 立即执行一次 GPU 采集
- 启动定时采集 goroutine
- 启动 HTTP 服务监听端口
- 浏览器访问
/ - 页面每 2 秒轮询
/api/v1/gpus - 页面展示最新 GPU 快照
这就是完整的数据流闭环。
可以简化为:
nvidia-smi -> Go 后端采集 -> 内存快照 -> JSON 接口 -> Dashboard 轮询展示
12. config/config.yaml 当前状态
仓库里虽然存在 config/config.yaml,但当前代码没有读取它。
也就是说这个配置文件现在只是“声明性文件”,没有参与实际运行逻辑。当前真正生效的是命令行参数:
-addr-interval
这说明项目处于一个“预留配置文件,但尚未接入”的状态。
13. 当前实现的优点
- 没有第三方依赖,部署简单
- 后端和前端都很轻量
- 数据流短,调试成本低
- 使用内存快照,接口响应快
- 有基本的健康检查接口
- 有优雅停机逻辑