用 Crossbar 打造三台 Jetson AGX Orin 实时监控面板
记录如何使用 Jetson tegrastats、Python WAMP 客户端、Crossbar 和 Next.js,为三台 AGX Orin 搭建一个可实时查看温度、GPU 负载和连通状态的监控页面。
用 Crossbar 打造三台 Jetson AGX Orin 实时监控面板
手上有三台 Jetson AGX Orin 时,最先遇到的问题往往不是“能不能跑模型”,而是:设备现在到底是不是在线?温度有没有升高?GPU 有没有真正工作?某一台停止响应时,能不能第一时间看出来?
这次我给三台 Orin 做了一个轻量的实时监控面板,最终页面位于:
整个方案以已经部署好的 Crossbar 为消息中枢,设备端使用 Python 采集指标并通过 WAMP 主动上报,云端网关保留每台设备的最新状态,Next.js 页面每 5 秒读取一次数据。
最终效果不是再为每台设备单独开放一个 Web 端口,而是让设备只负责上报,网页只负责展示,中间所有状态流转都通过统一的 Crossbar topic 完成。
1. 需求与最终结果
本次需要监控三台设备:
| 节点 | 局域网地址 | 页面名称 |
|---|---|---|
orin-28 |
192.168.1.28 |
AGX Orin 01 |
orin-29 |
192.168.1.29 |
AGX Orin 02 |
orin-30 |
192.168.1.30 |
AGX Orin 03 |
页面展示以下信息:
- 当前是否在线;
- 最近一次上报时间;
- GPU 3D 利用率;
- CPU 利用率;
- 内存占用率和已用/总内存;
- GPU、CPU、板卡和结温;
- 磁盘占用率;
- 设备运行时长;
- Crossbar 中枢当前是否连接。
采集周期为 5 秒。监控网关如果连续 15 秒没有收到某台设备的新快照,就把它标记为“失联”。这样可以把“设备从未上线”和“曾经上线但后来停止上报”区分开来。
2. 整体架构
最终链路如下:
┌────────────────┐
│ AGX Orin 01 │
│ agent.py │
└───────┬────────┘
│
┌───────▼────────┐ ┌────────────────────┐
│ AGX Orin 02/03 │ WSS │ Caddy │
│ agent.py ├──────►│ imeows.com/ws │
└────────────────┘ └─────────┬──────────┘
│
┌─────────▼──────────┐
│ Crossbar │
│ realm: imeows │
│ topic: snapshot │
└─────────┬──────────┘
│ WAMP subscribe
┌─────────▼──────────┐
│ Monitor Gateway │
│ latest state │
└─────────┬──────────┘
│ HTTP
┌─────────▼──────────┐
│ Next.js │
│ /api/monitor/nodes │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ /monitor 页面 │
└────────────────────┘
这里有三个关键设计。
第一,Orin 不需要把局域网端口暴露给网页。设备端只建立一条到云端的加密 WebSocket 连接,然后主动发布指标。
第二,浏览器不直接连接 Crossbar。网页通过 Next.js 的同源 API 读取网关数据,避免把 WAMP 认证细节放进前端,也让前端和消息系统保持解耦。
第三,网关只保存每台设备的最新快照。当前需求是实时状态面板,不需要为了显示“现在的温度”引入数据库和复杂的时序系统。
3. Crossbar 的 topic 与权限设计
监控数据使用一个固定 topic:
imeows.monitor.snapshot
每台 Orin 只有发布权限,监控网关只有订阅权限:
| 身份 | 作用 | 权限 |
|---|---|---|
monitor-agent-28 |
Orin 01 | 发布 imeows.monitor.snapshot |
monitor-agent-29 |
Orin 02 | 发布 imeows.monitor.snapshot |
monitor-agent-30 |
Orin 03 | 发布 imeows.monitor.snapshot |
monitor-gateway |
云端网关 | 订阅 imeows.monitor.snapshot |
每个 agent 使用独立的 WAMP ticket。ticket 只存在于设备环境文件和云端环境变量中,不写进源码、不提交 Git。
Crossbar 的角色权限采用最小授权原则。agent 不需要调用 RPC,也不需要订阅其他 topic;网关也不需要发布设备指标。这样即使某个客户端配置泄漏,影响范围也限定在监控 topic 内。
4. Orin 端:采集 Jetson 指标
4.1 为什么使用 tegrastats
Jetson 自带的 tegrastats 能直接输出 SoC 运行状态,包括:
- RAM 使用情况;
- CPU 核心利用率;
GR3D_FREQGPU 3D 利用率;- GPU、CPU、板卡、结温等硬件温度;
- 部分平台上的频率和功耗信息。
官方文档可以参考:NVIDIA tegrastats Utility。
agent 每次采集时启动一轮 tegrastats,读取稳定输出行,再用正则表达式提取字段:
RAM 2028/31920MB CPU [1%@729,0%@729,2%@729,0%@729] \
GR3D_FREQ 14%@408 \
GPU@51C CPU@53C Tboard@48C Tj@55C
不同 JetPack 版本的输出字段可能略有差异,因此解析器没有依赖某一整行的固定位置,而是分别匹配 RAM、CPU、GR3D_FREQ 和温度字段。
4.2 psutil 作为补充
tegrastats 很适合获取 Jetson 专有指标,但磁盘占用、系统运行时长等通用指标用 psutil 更方便。
因此采集端采用“优先使用 psutil、缺失时保留 tegrastats 数据”的策略:
- CPU 和内存优先读取 psutil;
- GPU 负载和硬件温度读取 tegrastats;
- 磁盘占用使用 Python 标准库;
- psutil 不可用时,CPU 和内存仍然可以从 tegrastats 结果中得到。
这种组合让 agent 不会因为某个可选 Python 包不存在,就完全失去上报能力。
4.3 WAMP 客户端
agent 使用 Autobahn 的 asyncio Component 连接 Crossbar。客户端明确指定 JSON serializer:
component = Component(
transports={
"type": "websocket",
"url": "wss://imeows.com/ws",
"serializers": ["json"],
},
realm="imeows",
authentication={
"ticket": {
"authid": "monitor-agent-28",
"ticket": "replace-with-node-ticket",
}
},
)
连接成功后,agent 每 5 秒生成一个快照并发布:
session.publish("imeows.monitor.snapshot", snapshot)
WAMP Component 自身负责连接生命周期;设备上的启动脚本还会在进程退出后重新拉起 agent,形成两层保护:连接断开时自动重连,进程异常退出时自动恢复。
4.4 上报数据结构
每台设备发布的 payload 统一使用 JSON 对象:
{
"schema_version": 1,
"node_id": "orin-28",
"name": "AGX Orin 01",
"address": "192.168.1.28",
"hostname": "ORIN-001",
"reported_at": "2026-08-27T10:14:38.240502+00:00",
"uptime_seconds": 86400.0,
"cpu_percent": 1.7,
"memory_percent": 6.3,
"memory_used_mb": 2028.0,
"memory_total_mb": 31920.0,
"disk_percent": 18.4,
"gpu_percent": 14.0,
"gpu_frequency_mhz": 408.0,
"temperatures": {
"gpu_c": 51.0,
"cpu_c": 53.0,
"board_c": 48.0,
"junction_c": 55.0,
"max_c": 55.0
},
"power_mw": null
}
其中 schema_version 用来给未来的字段升级留出空间。网关和前端只依赖稳定字段,不把 Jetson 命令的原始字符串直接暴露给页面。
5. 云端监控网关
5.1 MonitorHub
监控网关的核心是 MonitorHub:
- 使用
monitor-gateway身份连接 Crossbar; - 订阅
imeows.monitor.snapshot; - 根据
node_id把快照写入内存中的节点状态表; - 每次 HTTP 请求时计算快照年龄和节点状态;
- 把统一的 JSON 返回给 Next.js。
状态判断非常简单:
没有收到过快照 -> never
快照年龄 <= 15 秒 -> online
快照年龄 > 15 秒 -> offline
never 和 offline 分开很有用。前者表示设备还没有完成首次接入,后者表示设备之前正常工作过,但当前已经停止上报。
5.2 两个 HTTP 接口
网关提供两个接口:
GET /health
GET /api/monitor/nodes
健康接口用于容器健康检查:
{
"status": "ok",
"crossbar_connected": true,
"online_nodes": 3
}
节点接口返回完整的状态集合:
{
"generated_at": "2026-08-27T10:14:38.678712+00:00",
"crossbar_connected": true,
"summary": {
"total": 3,
"online": 3
},
"nodes": []
}
生产网关采用轻量的 Python HTTP 服务,并直接复用已有 Crossbar 镜像中的 Autobahn 运行环境。这样网关和 Crossbar 使用一致的 WAMP 依赖,容器职责仍然保持清晰:Crossbar 负责消息路由,Monitor Gateway 负责状态聚合。
6. Next.js 页面
6.1 同源 API 代理
浏览器访问的是:
/api/monitor/nodes
Next.js 服务端再把请求转发到内部网关:
http://monitor:8100/api/monitor/nodes
这一步有三个好处:
- 浏览器不需要知道 Docker 内部服务名;
- Crossbar ticket 不会进入浏览器;
- 页面只使用同源请求,不需要额外配置跨域。
6.2 页面轮询
监控页面加载时先显示三台预配置节点,然后每 5 秒请求一次 API:
useEffect(() => {
void refresh();
const timer = window.setInterval(() => void refresh(), 5000);
return () => window.clearInterval(timer);
}, []);
页面顶部显示集群摘要:
- 节点总数;
- 当前在线数;
- 平均 GPU 负载;
- 集群最高温度。
每台设备使用独立卡片展示详细数据,并用不同颜色区分 GPU、CPU 和内存利用率。接口暂时没有响应时,页面不会直接清空已有数据,而是显示提示并继续等待下一轮刷新。
7. Docker Compose 与 Caddy 部署
生产环境包含四个相关服务:
caddy 对外提供 HTTPS/WSS
crossbar WAMP 消息中枢
monitor 指标订阅与状态网关
web Next.js 页面和 API 代理
Caddy 保留原站点主页,同时增加 WebSocket 路由:
imeows.com, www.imeows.com {
encode zstd gzip
handle /ws {
reverse_proxy crossbar:8080
}
handle {
reverse_proxy web:3000
}
}
因此 Orin 端只需要连接:
wss://imeows.com/ws
页面仍然使用原来的:
https://imeows.com/monitor
云端敏感变量只保存在生产环境文件中,结构类似:
CROSSBAR_MONITOR_TICKET_28=replace-with-ticket-28
CROSSBAR_MONITOR_TICKET_29=replace-with-ticket-29
CROSSBAR_MONITOR_TICKET_30=replace-with-ticket-30
CROSSBAR_MONITOR_GATEWAY_TICKET=replace-with-gateway-ticket
MONITOR_STALE_AFTER_SECONDS=15
实际 ticket 不应出现在本文、源码、截图或 Git 提交里。
8. Orin 端的用户级部署
采集端不需要修改系统 Python,也不依赖系统级服务目录。每台设备使用自己的用户目录:
~/orin-monitor/
├── agent.py
├── run-agent.sh
├── node.env
├── agent.log
└── wheels/
node.env 只保存本机配置和本机对应的 ticket:
NODE_ID=orin-28
NODE_NAME='AGX Orin 01'
NODE_ADDRESS=192.168.1.28
CROSSBAR_URL=wss://imeows.com/ws
CROSSBAR_REALM=imeows
CROSSBAR_AUTHID=monitor-agent-28
CROSSBAR_TICKET=replace-with-node-ticket
TEGRASTATS_BIN=/usr/bin/tegrastats
INTERVAL_SECONDS=5
启动脚本负责三件事:
- 读取
node.env; - 使用文件锁避免重复启动多个 agent;
- agent 退出后等待几秒再次启动。
最后加入用户 crontab:
@reboot /home/wooley/orin-monitor/run-agent.sh # imeows-orin-monitor
这种方式足够支撑轻量采集任务,同时不会修改设备上其他服务。查看单台设备状态时,可以执行:
pgrep -af "orin-monitor/agent.py"
tail -f ~/orin-monitor/agent.log
9. 调通后的验证方法
9.1 检查 Crossbar 与网关
在云主机上查看容器:
docker compose ps
docker logs --tail 50 imeows-production-crossbar-1
docker logs --tail 50 imeows-production-monitor-1
网关健康接口应返回:
curl -fsS http://127.0.0.1:8100/health
9.2 检查公网页面和接口
curl -fsS -o /dev/null -w "%{http_code}\n" \
https://imeows.com/monitor
curl -fsS https://imeows.com/api/monitor/nodes \
| python3 -m json.tool
接口中最重要的三个字段是:
{
"crossbar_connected": true,
"summary": {
"total": 3,
"online": 3
}
}
9.3 检查 Orin 端
每台 Orin 的日志中应能看到类似信息:
Connected to Crossbar: node=orin-28 realm=imeows session=... authid=monitor-agent-28
随后访问节点 API,确认每个节点都有新的 last_seen 和 snapshot。本次最终验证结果为三台设备全部在线,页面能够持续显示 CPU、GPU、内存和温度数据。
10. 这套方案的边界
当前实现保存的是“最新状态”,不是历史时序数据。如果后续需要画温度曲线、统计 GPU 使用率峰值或查看过去 30 天的运行情况,可以把 Monitor Gateway 的内存状态替换成:
- Prometheus + node exporter;
- InfluxDB;
- TimescaleDB;
- ClickHouse;
- 或者一张专门的监控快照表。
当前页面也暂时没有操作设备的能力。重启、停止任务、切换模型等动作应该另行设计 RPC topic,并为每个动作增加审计、权限和幂等机制,不建议直接把控制能力混入只读监控 topic。
如果监控页面需要只允许管理员查看,可以在 /monitor 和 /api/monitor/nodes 外层增加 Firebase 登录校验。展示层、消息层和设备认证已经分开,后续增加访问控制不会影响 Orin agent 的采集逻辑。
结语
这次的核心不是做一个复杂的监控平台,而是先把三台设备的状态链路做短:
采集 → Crossbar → 网关 → 页面
Jetson 专有指标交给 tegrastats,通用系统指标交给 Python,消息传输交给 Crossbar,网页只消费统一 JSON。每一层都只做自己擅长的事情,最终得到一个足够轻、容易维护,也方便继续扩展的 AGX Orin 监控基础设施。
本次实现涉及的主要文件:
apps/web/app/monitor/page.tsx
apps/web/components/MonitorDashboard.tsx
apps/web/app/api/monitor/nodes/route.ts
services/monitor/app/hub.py
services/monitor/app/standalone.py
infra/orin-monitor/agent.py
infra/orin-monitor/run-agent.sh
infra/crossbar/production.json
infra/docker/compose.production.yaml
Conversation
正在读取留言…