这里介绍一下我最近写的一个小工具:gpu-ssh-monitor。它的目标很简单:通过 SSH 连接到一台机器之后,直接在终端里看到这台机器的 GPU 和系统状态。它不是一个完整的运维平台,也没有 Web 控制台,只是一个偏轻量、只读、面向实验室服务器的监控入口。

项目地址:Rhythmicc/gpu-ssh-monitor。代码量不大,但技术栈组合还挺有意思:Go + Wish 负责 SSH 服务入口,Node.js + node-pty 负责启动和捕获 TUI 程序,xterm/headless 负责把终端输出维护成可序列化的画面,再通过 Unix domain socket 分发给多个 SSH 会话。
为什么不是 Web 页面?
最开始做这个工具,是因为我经常只是想快速看一眼服务器的 GPU 是否空闲、显存占用如何、CPU 和内存状态是不是异常。为了这件事开一个 Web 服务、做鉴权、暴露端口、处理反向代理,多少有点重。既然本来就会 SSH 到机器上,那么让 SSH 本身成为入口会更直接。
这个设计也顺便带来一个比较清晰的边界:它是只读监控。用户输入不会被转发给 nvitop 或 btop,只用于退出,例如按 q 或 Ctrl-C。也就是说,连接进来的用户只能看 dashboard,不能借这个入口操作底层 TUI 程序。
整体结构
gpu-ssh-monitor 可以分成两层:
- SSH 层: Go 程序基于 Wish 启动一个 SSH server,监听默认的
23234端口。 - Dashboard 层: Node 脚本启动
nvitop和btop,把两个 TUI 拼成一个固定尺寸的终端画面。
默认布局是 130 列 × 69 行:上面 21 行给 nvitop,下面 48 行给 btop。这个尺寸不是为了追求自适应,而是为了让输出稳定、可预测,尤其适合在服务器监控这种场景里反复打开。
Go:用 Wish 做 SSH 入口
Go 这部分主要处理 SSH server 生命周期、连接管理和 dashboard daemon 的启停。项目使用 Wish 创建 SSH 服务,并挂上 activeterm、accesscontrol、logging 等 middleware。用户连进来后,middleware 会连接到本地 Unix socket,把 dashboard 的输出直接复制到 SSH session。
这里我比较在意的一点是资源生命周期:没有用户连接时,不运行 dashboard collector。Go 层维护了 active client 计数,第一个客户端连接时启动 Node dashboard 进程,最后一个客户端断开后停止它。这样服务器平时不会因为一个无人查看的监控程序持续占用资源。
另一个细节是进程组管理。Go 在启动 Node 进程时设置了独立进程组,停止时先发 SIGTERM,超时后再 SIGKILL。这是为了让 Node、nvitop、btop 以及它们的 pty 子进程能一起被回收,避免留下后台进程。
Node:用伪终端运行 nvitop 和 btop
Dashboard 层选择 Node.js,主要是因为 node-pty 和 xterm 生态用来处理终端画面比较方便。脚本会分别启动两个 pty:一个运行 nvitop,一个运行 btop。每个 pty 都被设置为固定的列数和行数,并使用 xterm-256color 和 truecolor 环境。
直接读取 TUI 程序的 stdout 通常不够,因为里面有大量 ANSI 控制序列、光标移动、清屏和局部重绘。项目里用了 @xterm/headless 创建无界面的终端模拟器,再用 @xterm/addon-serialize 把当前终端状态序列化成一段可以重新绘制的快照。这样就不用自己手写一个终端解析器。
画面合成与差分更新
每隔默认 500ms,Node 层会取一次两个 pane 的 snapshot,把上半部分的 nvitop 和下半部分的 btop 拼成一帧。为了减少闪烁,它不是每次都整屏重绘,而是比较当前帧和上一次发送给每个 client 的帧,只更新发生变化的行。
这个差分更新很朴素,但对终端 dashboard 来说已经足够有效:网络输出更少,SSH 客户端里也不会一直明显闪屏。代码里还会清理一些 alternate screen、鼠标模式和光标定位相关的控制序列,让最终展示更稳定。
多会话共享同一个 dashboard
如果每个 SSH 连接都单独启动一套 nvitop 和 btop,很容易造成重复开销,也会让不同用户看到的时间点不一致。gpu-ssh-monitor 的做法是让多个 SSH session 共享同一个 dashboard source。
具体来说,Node 进程在本地创建一个权限为 0600 的 Unix domain socket。Go 层每接入一个 SSH session,就 dial 这个 socket,并把 socket 输出转发给用户。Node 层维护 clients 集合,把同一帧 dashboard 差分发送给所有连接者。
配置与部署
项目通过环境变量配置,比较关键的包括:
SSH_HOST/SSH_PORT:SSH 服务监听地址和端口,默认0.0.0.0:23234。NVITOP_CMD/BTOP_CMD:指定nvitop和btop的路径。COLS、NVITOP_ROWS、BTOP_ROWS:控制 dashboard 尺寸。SSH_SNAPSHOT_MS:控制刷新间隔,默认 500ms。
部署上仓库里给了一个 systemd unit 示例。它假设项目放在 /opt/gpu-ssh-monitor,编译产物是 /opt/gpu-ssh-monitor/gpu-ssh-monitor,并设置了自动重启。实际使用时,最好把它放在已有的网络控制、防火墙或 SSH 反向代理之后,因为默认 username 不参与鉴权。
这个技术栈适合在哪里?
我觉得 gpu-ssh-monitor 适合的是一种很具体的场景:实验室或个人服务器已经有 SSH 访问方式,大家只是想快速看 GPU 是否空闲,不希望再维护一整套 Web 监控系统。它的优点是入口简单、依赖直观、只读边界明确;缺点也很明显,它不是 Prometheus/Grafana 那种可长期存储和查询的监控系统,也不负责告警。
从工程实现上看,这个项目比较有意思的地方在于它没有重新发明监控 UI,而是复用了 nvitop 和 btop 已经成熟的 TUI,再用 SSH、pty、headless terminal 和 socket 把它包装成一个可多人查看的服务。这个思路很小,但对我自己的日常使用来说已经足够方便。
评论