2025 年 2 月,我从一个空目录开始搭设备状态矩阵 APP。第一次提交很大:命令入口、配置、路由、模型、SQLite、TDengine、WebSocket、缓存、分页和矩阵计算都在里面。

这种“大提交”现在看并不理想,但它保留了当时一个很真实的认知过程:我一开始以为自己在做实时推送,后来发现其实是在做一个小型读模型。

页面上的一个格子从哪里来

矩阵的一行可能代表设备,一列可能代表业务指标。格子的颜色却不只取决于最新采集值,还可能受以下信息影响:

信息 来源 变化频率
设备与模型关系 关系库/配置
最新指标值 时序库/MQTT
基线计算 分析任务
未处置告警 告警库
用户权限 权限服务/缓存 低但敏感
在线状态 心跳与采集时间

把这些数据在每次 WebSocket 推送前全部查询一遍,功能可以完成,代价是推送越频繁,数据库越忙。

“当前状态”其实是一份物化视图

后来我开始把矩阵看成事件驱动的当前视图:

  • 采集值到达,更新指标状态;
  • 告警产生,提升对应设备或指标等级;
  • 告警处置,重新计算展示状态;
  • 权限变化,重新过滤用户可见范围;
  • 设备离线,根据最后采集时间改变状态。

WebSocket 不负责计算所有事实,它只负责把已经聚合好的视图推给客户端。这种职责划分能让实时链路更清楚,也方便定位到底是“状态算错了”还是“消息没有推到”。

WebSocket 自己也会制造问题

连接数量增加后,需要处理的事情包括:

  • 心跳和读写超时;
  • 浏览器异常退出后的连接清理;
  • 同一连接并发写导致的数据帧问题;
  • 慢客户端队列不断积压;
  • 服务关闭时连接和后台任务如何退出。

权限尤其容易被忽略。如果只在握手时检查一次,用户角色变化后,旧连接仍可能收到不应看到的数据。我们后来为矩阵补了权限适配,也经历过因为联调临时关闭、再重新开启权限的反复。这说明权限不是外围功能,它应该进入实时视图的生成过程。

几个月后的回看

后续提交里又陆续修了告警等级映射、时间戳、基线告警不展示、时间转换和定时刷新性能。它们不是互不相关的小 Bug,而是在不断校正“当前状态”的定义。

实时系统最难的并不是毫秒级推送。真正困难的是:当数据来自不同时间、不同存储和不同业务模块时,系统是否能解释页面为什么呈现这个状态。