监控图表的实时性不是靠某一个库实现的,而是传输通道、渲染方式、连接治理三层配合的结果。在企业级 AI 网关项目里,我们给模型性能监控大盘定下的方案是:指标刷新走 SSE 单向推送,渲染端用 ECharts 的增量追加接口避免全量重绘,前端再做心跳、断线重连和页面失焦暂停。这套组合让网关各模型的请求速率、首 Token 延迟、错误率等指标在 1 秒内反映到图表上,页面在万级数据点下也不掉帧。下面拆开讲每一层怎么做。
一、实时方案的选型:为什么监控大盘选 SSE
先把候选方案摆在一张表里对比,再看我们的取舍依据。
| 方案 | 通信方向 | 端到端延迟 | 服务器开销 | 复杂度 | 适配场景 |
|---|---|---|---|---|---|
| 短轮询 | 客户端定时拉取 | 取决于间隔 | 高,大量无效请求 | 最低 | 分钟级刷新 |
| 长轮询 | 半双工,hold 住请求 | 较好 | 中,连接管理复杂 | 中 | 兼容性受限的准实时 |
| SSE | 服务端单向推送 | 好 | 低,单一长连接 | 低 | 指标、通知、日志流 |
| WebSocket | 全双工 | 最优 | 低 | 较高 | 双向交互、在线协作 |
在我们网关项目的压测环境里,同一条指标链路端到端延迟分别是:WebSocket 约 23ms,SSE 约 45ms,HTTP 短轮询 2 秒间隔下接近 210ms。监控数据只有服务端到客户端一个方向,用 WebSocket 属于性能浪费,SSE 基于原生 HTTP,不需要单独维护握手协议,还能复用在网关已有的鉴权链路上,所以最终定为 SSE。
1.1 网关监控里 SSE 的接入方式
网关后端用 Spring Boot 输出 SSE 流,前端用浏览器原生 EventSource 订阅,比轮询省掉大部分请求开销。示意代码如下:
const source = new EventSource('/gateway/api/metrics/stream');
source.onmessage = (event) => {
const point = JSON.parse(event.data);
appendToChart(point);
};
source.onerror = () => {
// 连接断开后按退避策略重连,见下文心跳小节
scheduleReconnect();
};
二、连接治理:心跳、重连与页面生命周期
SSE 长连接最怕的是静默断开——服务端容器回收了连接,前端却以为还活着,图表就停在旧数据上。
按下面这组步骤处理连接生命周期:
- 服务端每 30 秒向连接内写入一条心跳注释行,前端收到即视为连接存活;
- 前端在
onerror触发后做指数退避重连,间隔从 2 秒起步,上限 30 秒; - 用
document.hidden监听页面切后台,隐藏时主动断开连接,回到前台再重建,省掉后台无效心跳; - 多张图表共用一个 EventSource 实例,连接数从「图表数」降到 1。
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
source.close();
} else {
reconnect();
}
});
三、渲染端:增量追加而不是全量重绘
实时图表最大的性能陷阱在渲染层。ECharts 的 setOption 每次调用都会重新解析全部数据、重算布局、重绘画布,数据量到几千条后主线程明显卡顿。官方推荐的大数据量方案是 appendData:只追加新数据点,只渲染增量图形元素,性能差距可达一个数量级。
3.1 setOption 与 appendData 的差异
| 对比项 | setOption | appendData |
|---|---|---|
| 更新范围 | 全量解析重绘 | 仅追加新数据 |
| 数据量上万 | 主线程阻塞、卡顿 | 保持流畅 |
| 是否重算整体布局 | 是 | 否 |
| 适用场景 | 配置或数据整体变化 | 时序流式追加 |
网关监控大盘的折线图按下面方式维护一个滑动窗口,窗口只保留最近 2000 个点,避免无限增长。
const chart = echarts.init(container);
function appendToChart(point) {
seriesData.push([point.timestamp, point.value]);
if (seriesData.length > MAX_POINTS) {
seriesData.shift(); // 丢最旧的数据点
}
chart.appendData({
seriesIndex: 0,
data: [seriesData[seriesData.length - 1]]
});
}
3.2 低频数据直接走 setOption
不是所有指标都值得走增量链路。网关的模型列表、API Key 用量这类秒级或分钟级变化的数据,直接用 setOption 按 5 秒间隔刷新即可,接口聚合好一次返回,反而比流式更简单可靠。实时性按数据重要性分档,而不是一刀切全部上 SSE。分档刷新还附带请求量收益:后台同时挂几十个模型监控窗口时,全量轮询会显著抬高网关负载,我们把低频接口收敛成一个批量接口,一次返回全部模型的元数据与用量,前端按需取用,比逐模型各自轮询少一个量级的请求。
四、后端采样与推送节奏
监控的实时性上限由后端采样周期决定。网关后端用定时任务每 5 秒聚合一次各模型请求计数、延迟分位和错误率,写进内存环形缓冲,再通过 SSE 广播。推送内容只带增量或最新聚合值,前端不关心历史:
@Scheduled(fixedDelay = 5000)
public void pushMetrics() {
Map<String, ModelMetric> latest = metricsService.snapshot();
String payload = objectMapper.writeValueAsString(latest);
sseEmitterMap.values().forEach(emitter -> {
emitter.send(SseEmitter.event().data(payload));
});
}
五、监控数据的时间对齐
SSE 推送的是各模型的最新聚合值,前端不能假定同一帧里所有指标的时间戳一致。网关后端给每条推送附上采样时间戳,前端以时间戳而不是到达顺序为准排序入图,网络抖动导致的乱序推送不会画成锯齿。跨天、跨周查看历史趋势时,明细接口返回按分钟或小时预聚合的序列,前端一次性 setOption 渲染,与实时流共用同一套坐标轴配置,切换视图只更新 series 数据而不重建图表实例,避免整页闪烁。
六、落地时容易踩的两个坑
第一个坑是数据点爆量。推流不加窗口裁剪,页面挂一小时就会积累上万个点,此时即便用增量渲染,交互也会明显变慢,必须在上游就做滑动窗口或降采样。第二个坑是网络代理截断 SSE。经过 Nginx 这类代理时,不配置 X-Accel-Buffering: no 或等价关闭缓冲的参数,数据会被攒批推送,实时性直接从秒级劣化成几十秒,排查起来很隐蔽。
常见问题(FAQ)
Q1:监控图表什么时候必须上 WebSocket?
需要前端回传控制指令(如调整采样频率)时,才值得升级为 WebSocket 全双工。
Q2:SSE 断线后重连会丢数据吗?
服务端缓存最近一次聚合快照,前端重连成功先拉快照再续流,可避免断档。
Q3:多张图表共用一个流,怎么区分数据归属?
推送内容带 chartId 字段,前端按 ID 分发到对应图表的追加逻辑。