全局监控系统gods-eye-view:架构设计、核心实现与踩坑实践

监控系统可观测性实时大屏 本内容遵循CC 4.0 BY-SA版权协议
烨空 烨空 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要

1. 为什么我要做一个叫 gods-eye-view 的全局监控系统

最近 gods-eye-view 这词在技术圈子里越来越常出现,有人拿它形容无人机航拍的俯瞰角度,有人用它指代游戏里的第三人称视角。但我在自己的项目里,给它的定义很朴素:把散落在各个系统里的指标、日志、状态和告警,汇总进同一个时间轴,用一张大屏看完所有东西。我最初做 gods-eye-view 的原因非常实在——团队每个月都要因为“哪个系统先出问题”吵上几架,每个人打开不同的监控页面,谁也说服不了谁。于是我想做一个内部工具,让所有人在同一种视角下对齐事实。

这个项目从第一行代码跑到今天,已经过了半年。它没有多高深的技术,但踩了一堆数据库、浏览器、网络层面的坑。这篇文章会从需求、架构、核心实现到线上故障,完整梳理一遍,尤其适合正在做运维监控、数据可视化大屏、或者公司内部平台的同学。如果你是第一次接触这类系统,也能从中看到一条从零开始搭全局观察层的完整链路。

1.1 核心需求:不是“多个看板”,而是“一个视角”

我们在做的系统,其实和很多公司的情况一样:已经有 Prometheus 监控服务器指标,有日志平台,有业务后台的实时订单看板,还有第三方云厂商自带的监控报警。表面上每个环节都有工具,但真出事的时候,负责排查的人要在四五个系统之间来回切换。譬如某个支付的错误率上升,你很难立刻知道是数据库慢查询、容器内存不足、还是上游 API 超时,因为三个现象的监控数据分散在不同地方。

所以我定义 gods-eye-view 的第一条原则是:不替代任何已有工具,只做“统一视角层”。它需要完成三件事:

  • 把异构数据源的时序数据采集过来,做标准化处理;
  • 在内存里维持实时滑动窗口,让前端能秒级看到最新趋势;
  • 在一张可自定义的大屏上,把服务器、业务、网络、告警都放到同一个时间刻度里。

“上帝视角”在这里不是玄学,而是数据口径的统一。同一个时刻,A 系统的数据库主从延迟是 3 秒,B 系统的队列堆积是 1200 条,普通看板只会分别显示这两个数字,但 gods-eye-view 会把它们按时间对齐,并标记出它们之间的因果关系线索。这就是我想要的全局视角。

1.2 项目边界:我明确不做的几件事

很多人一听“统一监控”就兴奋,想把我做的东西和 Grafana、夜莺、Zabbix 比。我的建议是,别这么比。成熟监控系统在采集、存储、告警规则引擎上已经做得足够好,再造一个轮子意义不大。gods-eye-view 的定位更像一个“指挥官视角”:数据采集继续依赖 node_exporter、Prometheus 这些基础组件,历史数据也继续存在 ClickHouse 里,我只负责把大家需要同时看的信息拉到一个地方,并处理好实时的关联展示。

这样划分边界有几个实际好处。第一,团队不用迁移现有采集体系,接入成本低;第二,我们的开发量集中在上层抽象和前端交互,不会陷入指标采集的细节泥潭;第三,系统和底层监控解耦,以后就算把 Prometheus 换成其他采集端,上层视图也能平滑迁移。

2. gods-eye-view 的整体架构与关键数据流

整个系统分为四层:接入层、规约层、存储层、展示层。层与层之间通过标准 JSON 结构通信,任何一层替换掉都不会影响其他层。这也是我半年迭代下来最满意的一点,因为最开始只有接入层和大屏页面,中间两层完全是被踩坑逼着加出来的。

2.1 四层结构:接入、规约、存储、展示

接入层负责接收各种来源的数据。目前主要支持四种方式:HTTP Webhook(用于业务系统主动上报)、Prometheus Webhook(用于接收告警事件)、Kafka 消费(用于大数据量的链路追踪和业务日志)、以及定时拉取数据库视图(用于一些老系统没有 API 的场景)。

规约层是整个系统的核心。它会将不同来源的数据统一成“实体-指标-事件”三元组模型,同时完成单位换算、字段改名、时区校正、标签补全。比如 Kafka 里的订单延迟时间单位是秒,而 HTTP 上报的接口耗时单位是毫秒,规约层会统一成毫秒,并打上 source=kafka 的标签。

存储层分成两块:热数据放在 Redis,冷数据落在 ClickHouse。Redis 主要存最近 30 分钟的原始采样点和高频聚合结果,供大屏消费;ClickHouse 用来做历史回放、趋势分析和报表导出。

展示层是浏览器端的大屏,通过 WebSocket 接收服务端推送的实时数据,使用 Canvas 绘制时序图,使用虚拟列表渲染告警列表。它不直接查数据库,所有需要的数据都来自服务端维护的一个“实时快照”。

2.2 技术选型:为什么用 ClickHouse 和 Redis,而不是一套数据库打天下

我在选型时认真对比过几套方案。最早想直接用 MySQL,但很快发现时序数据的写入模式是写多读少、按时间范围扫描,MySQL 的 B+ 树在这种场景下又贵又慢。后来也考虑过直接用 Prometheus 存所有数据,但它更适合指标类数据的标签查询,对业务事件明细和历史回放的支撑不够灵活。

下面是我当时的对比结论:

存储方案 最适合的场景 不适合的场景 我的结论
MySQL 关系型业务数据、维度表 高并发时序写入、大量聚合查询 只存配置和用户权限
Prometheus 指标抓取、基础告警 复杂事件分析、长时间跨度汇总 继续作为数据源之一
ClickHouse 海量事件明细、时间范围聚合 高频点查、单条更新 冷数据存储和回放查询
Redis 高频读写、实时窗口缓存 数据持久化、复杂查询 热数据窗口和发布订阅

这套组合在部署后给我的感受是:Redis 负责“现在发生了什么”,ClickHouse 负责“刚才发生了什么”,两条链路互不干扰。实时链路不依赖 ClickHouse,所以即使历史存储挂了,大屏仍然能继续展示实时状态。

2.3 一条告警从产生到上屏的完整旅程

一个实际例子能很好说明整个架构。假设一台服务器的 CPU 使用率超过 90%,node_exporter 抓取到这个指标后,Prometheus 的告警规则会触发,把告警推送到 gods-eye-view 的 Webhook 接口。

接口收到原始 JSON 后,规约层把 Prometheus 的标签名映射成标准字段:instance 变成 entity.name,alertname 变成 event.type,severity 映射成 1/2/3 三个级别。然后这条事件被写入 Redis 的最近一小时事件列表,同时发布一个 Pub/Sub 消息。服务端有一个常驻订阅进程,收到消息后把最新状态推送到所有前端页面。

前端根据 entity.name 找到对应的 CPU 指标卡片,把数值标注成红色,同时在告警列表里新增一条记录。如果 5 分钟内没有再次触发同类型告警,前端会自动收起高亮,避免告警列表被刷屏。整个过程从数据到屏上约 1 秒,可以满足大部分监控场景。

3. 核心实现:数据模型、聚合与渲染

这一部分是可复现的核心中的核心。gods-eye-view 没有用什么神秘算法,但数据模型设计得比较克制,聚合和渲染也做了针对性优化。

3.1 数据模型:Entity、Metric、Event 三元组

我将所有数据抽象成三个对象:

  • Entity:代表一个被观测的对象,比如一台服务器、一个数据库实例、一个订单服务;
  • Metric:代表某个实体的某个指标在某个时刻的数值,比如 CPU 使用率 95%,时间戳 2024-06-01T10:00:00Z;
  • Event:代表某个实体在某个时刻发生的离散事件,比如告警触发、部署完成、配置变更。

用 TypeScript 描述大概是这样的:

TYPESCRIPT
interface Entity {
id: string;
name: string;
type: string; // server | database | service
attributes: Record<string, string>;
}
 
interface Metric {
entityId: string;
name: string; // cpu.usage | db.latency | queue.length
value: number;
unit: string; // percent | ms | count
timestamp: number; // epoch milliseconds
tags: Record<string, string>;
}
 
interface Event {
id: string;
entityId: string;
type: string; // alert | deploy | config_change
level: 1 | 2 | 3;
message: string;
timestamp: number;
payload?: Record<string, unknown>;
}

这套模型的好处是,所有数据源在规约层都会被映射成这三个接口。前端展示时,不需要关心数据来自哪个监控系统,只需要按照 entityId 和 metric.name 来组织视图。后续要增加新数据源时,只需要写一个新的规约插件,核心代码完全不用动。

3.2 滑动窗口降采样:既要实时又要省内存

实时大屏最怕的不是数据量小,而是数据量大了以后内存爆炸。一台服务器如果每 5 秒上报一个点,一小时就是 720 个点,1000 台服务器就是 72 万个点。如果全部存前端,浏览器肯定吃不消。所以我在服务端和前端都做了滑动窗口降采样。

服务端为每个实体+指标维护一个环形数组,数组长度固定为 150,代表最近 5 分钟(每 2 秒一个点)的数据窗口。新增数据到来时,覆盖数组中最旧的位置;同时计算当前窗口内的最大值、最小值和平均值,作为 5 分钟总览。

JAVASCRIPT
class RingBuffer {
constructor(size) {
this.size = size;
this.buffer = new Array(size).fill(null);
this.head = 0;
this.count = 0;
}
 
push(value) {
this.buffer[this.head] = value;
this.head = (this.head + 1) % this.size;
if (this.count < this.size) this.count++;
}
 
stats() {
if (this.count === 0) return null;
let max = -Infinity;
let min = Infinity;
let sum = 0;
for (let i = 0; i < this.count; i++) {
const v = this.buffer[i];
if (v > max) max = v;
if (v < min) min = v;
sum += v;
}
return { max, min, avg: sum / this.count };
}
}

前端收到窗口数据后,再按视图宽度做一次降采样。比如大屏上一条折线图只有 800 像素宽,我就把 5 分钟 150 个点缩放成 800 个视觉点,如果原始点比像素多,就取每段的平均值。这样既能展示趋势细节,又不会让浏览器渲染出比屏幕还多的线条。

3.3 大屏渲染:三千个指标不卡顿的写法

一开始我用 ECharts 画所有折线图,数据量小的时候很流畅,但当我接入第二个数据源、同屏指标超过一千个之后,ECharts 内部的 DOM 和 Canvas 切换开始出现明显卡顿。后来我决定自绘图表,只保留最需要的折线图、柱状图和热力背景。

具体做法是,用 HTML5 Canvas 2D API 绘制所有图表。每条折线对应一个 Canvas 图层,避免频繁清空整个画布导致闪烁。背景网格单独画在一层,只有缩放或拖拽时才重绘。折线数据通过 requestAnimationFrame 批量更新,而不是每来一条 WebSocket 消息就立刻重绘。

实测下来,在 Chrome 里同屏渲染三千个指标点,帧率能稳定在 50 帧左右。这种方式虽然放弃了 ECharts 的现成交互(比如缩放、tooltip),但对于内部大屏来说,实时性和流畅度远比交互复杂度重要。

3.4 阈值告警与告警合并:不重复轰炸

gods-eye-view 的告警逻辑也不复杂,但有一个关键优化:事件去重。刚上线的时候,因为测试数据误触发了告警规则,团队群一晚上被刷了几百条消息,后来我加了一个合并窗口。

每条告警事件生成时,会以 entityId + metricName + type 作为去重 Key。在告警未恢复前,相同 Key 的事件只会推送一次。只有当规约层收到一条 event.type = "recovery" 的消息后,才会重置这个 Key,允许下一次告警再次推送。同时,所有告警按级别设置了防打扰时间窗,比如 P1 级告警在 5 分钟内最多推送 3 次,P3 级告警在 1 小时内最多推送 1 次。

这段逻辑看起来简单,但是线上事故恢复后,经常会出现大量抖动事件。如果没有合并窗口,前端列表会被瞬间刷满,真实的关键告警反而被淹没。

4. 部署落地时踩过的四个坑

任何监控系统都要经历真实环境的毒打。gods-eye-view 上线后的前两周,几乎每天都有一个“鬼故事”,这里挑四个最有代表性的复盘。

4.1 浏览器时区不一致导致“上帝视角”出现时间错乱

现象是:服务器端看到的数据时间是 UTC,前端展示却变成东八区,所有图表曲线都整体偏移了 8 小时。排查了半天,发现不是后端算错时间戳,而是前端 new Date() 时默认调用了浏览器本地时区,把 UTC 时间戳格式化了两次。

解决方法是所有接口返回的时间字段统一使用带时区的 ISO 8601 字符串,前端用 dayjs.utc 解析后,再显式转换成目标时区展示。图表横轴则一律使用毫秒时间戳,只有鼠标悬停时的 tooltip 才做本地化展示。这样至少保证了“上帝视角”里的时间轴在全球任何地方的浏览器上看起来都是对齐的。

4.2 WebSocket 重连风暴把后端打挂

第一版服务端只是简单地在 WebSocket 断开时马上接受客户端重连,结果某天办公楼网络波动,几十个大屏页面同时掉线又同时重连,后端瞬间涌入大量连接请求,内存和线程池直接被打满。

解决方案是客户端做指数退避重连:第一次断开等 1 秒,第二次等 2 秒,第三次等 4 秒,最长不超过 60 秒,并加 0~3 秒随机抖动。服务端则在连接恢复后,根据客户端传来的 lastEventId 只推送断线期间的增量数据,而不是全量重推。这样既能快速恢复,又不会因为重连造成后端风暴。

4.3 内存里的环形数组越积越多

这个坑非常隐蔽。随着接入的实体越来越多,每个实体每个指标都会在内存里创建一个 RingBuffer。按理说实体下线或删除后,这些缓冲区应该被回收。但某天我查内存占用,发现服务端堆内存涨到了 3GB,明显不对劲。

翻代码后找到了原因:规约层只负责创建缓冲区,却没有在实体不再上报数据时主动清理。后来我加了一个引用计数和空闲回收机制:每个实体超过 30 分钟没有新数据,就会自动标记为闲置;超过 2 小时的闲置实体,其所有指标缓冲区都会被销毁。这样内存占用稳定在 800MB 左右,不会无限增长。

4.4 Redis 连接池被打满

实时推送功能的实现方式是每个 WebSocket 连接持有自己的 Redis 连接,用来订阅最新数据。大屏数量一多,Redis 连接数直接冲上几千,连接池耗尽,导致其它实时链路也连带阻塞。

我现在的做法是:所有 WebSocket 连接共享一个 Redis Pub/Sub 订阅进程,这个进程收到新数据后,再用内存队列分发给对应的前端会话,前端最终只和 Node.js 进程保持一个长连接。这样 Redis 连接数从几千降到个位数,推送吞吐量反而提高了。

5. 从个人盘点工具到团队平台的演进

gods-eye-view 一开始是我个人的排查辅助工具,后来团队其他同事觉得好用,才开始往平台化方向演进。这个过程中,权限模型、自定义视图和通知集成是三个最核心的变化。

5.1 多团队隔离:只让每个团队看到自己的视角

当平台被多个团队使用时,第一件事就是做数据隔离。我在 Entity 上增加了一个 teamId 字段,所有资源都归属到某个团队。用户角色分为管理员、普通成员、只读访客三种。管理员能创建视图、管理团队成员和配置数据源;普通成员只能管理自己创建的视图;只读访客只能浏览被授权的看板。

实现上并不复杂,在规约层和查询层都强制带 teamId 过滤条件,前端再根据用户的角色隐藏无关入口。这套权限模型虽然简单,但对内部工具来说已经足够。

5.2 自定义 Dashboard:让用户自己组“上帝视角”

每个团队关注的重点不一样,让研发自己拼看板比我来统一维护更高效。我实现了两个组件类型:指标卡片和事件流表格。用户可以从左侧的数据源列表里拖一个实体+指标到画布上,形成一个卡片;也可以把告警事件流放到页面底部,按级别筛选。

视图本身存储成一个 JSON 配置,类似下面的结构:

JSON
{
"id": "view-001",
"name": "支付链路总览",
"teamId": "team-pay",
"layout": [
{ "type": "metric", "entityId": "pay-db-01", "metric": "db.latency", "x": 0, "y": 0, "w": 4, "h": 2 },
{ "type": "event", "entityId": "pay-svc-02", "eventType": "alert", "x": 0, "y": 2, "w": 6, "h": 4 }
]
}

前端根据这份 JSON 渲染组件位置,以后要加新组件,只需在渲染器里多注册一个类型,不用改核心逻辑。

5.3 告警订阅:企业微信、钉钉、邮件一个都不能少

团队内部使用场景里,消息通知渠道特别分散。有人习惯看企业微信,有人习惯收邮件,还有人希望极端故障时收到电话。我抽象了一个 Notifier 接口:

TYPESCRIPT
interface Notifier {
send(event: Event, target: User): Promise<void>;
}

每个渠道实现一个 Notifier 适配器,管理员在视图配置里绑定“订阅规则”:比如当某台数据库实例的 P1 告警被触发时,同时发送到企业微信和邮件;如果 10 分钟未恢复,再触发电话渠道。因为告警合并已经做了去重,所以每个通知渠道也不会被轰炸。

5.4 我下一步想做的事

平台稳定运行之后,我自己的主要精力在补两块东西。一是“历史回放”功能,把 ClickHouse 里存的冷数据按时间轴重新演播,这样复盘线上故障时,能让所有人清楚看到当时的全局状态,而不是只盯着静态截图。二是“异常预测”,简单做法是根据过去 7 天同时段的指标均值做基线,当当前值超过基线一定倍数时,提前产生一条提醒事件,而不需要等到硬阈值被触发。

目前这两块还在迭代中,但 gods-eye-view 的核心价值已经体现出来了:让任何人在任何时间点,都能用同一个视角看清整个系统正在发生什么,而不必在多个工具之间来回跳转。如果你也想做类似的东西,我的建议是别一上来就规划大而全的平台,先找一个自己每天都会被它烦到的场景,专注解决那一个视角,然后再慢慢扩展。

相关推荐

基于Python+Open CV的手势识别算法设计

伴随着人工智能时代的到来,人机交互的领域也逐渐成为研究的一大方向;其中,手势识别是人机交互领域的一项关键技术,自此手势识别也迎来了一波高潮,近几年无论是在消费领域、电子领域、数码领域、家电领域甚至汽车领域上,都能或多或少的见到手势识别的身影。 手势识别在设计智能高效的人机界面方面具有至关重要的作用, 目前手势识别已应用到手语识别、智能监控、到虚拟现实等各个领域,手势识别的原理都是利用各种传感器(例如红外、摄像头等)对手部的形态进行捕捉并进行建模,形成一个模型信息的序列帧,而后将这些信息序列转换为机器能够识别的相对应指令(例如打开、切换菜单、移动等)来完成控制。 采用Python的集成开发环境Pycharm进行本次课程设计,在Pycharm中进行需要库(模块)的下载,调取电脑摄像头,按帧读取摄像头采集到的头像,形态学处理,图像旋转(由于摄像头采集到的图像是镜像,需要用cv2.flip将摄像头采集到的图像进行处理),选取图片中固定位置作为手势输入,用红线画出手势识别框,基于hsv的肤色检测,进行高斯滤波,找出轮廓,求出图像中手势的凹凸点,手指间角度求取。

努力学习前端的小陈

ROS-CV-ImageConverter:将ROS图像转换为OpenCV(cv :: Mat)类型的图像

ROS-CV图像转换器 依存关系 cv_bridge(ROS软件包)

Willis Wang

计算机视觉图像处理-CV2与图像插值算法

用最近邻插值与双线性插值原理完成图像缩放 最近邻插值,是指将目标图像中的点,对应到源图像中后,找到最相邻的整数点,作为插值后的输。缺点:使用最近邻插值算法处理图像放大时,在图象中可能出现明显的块状效应 双线性插值,在两个方向上做三次线性插值。使用双线性插值算法处理图像放大时,在图象中可能出现马赛克 代码 import cv2 img = cv2.imread(’./xiabang.jpg’, cv2.IMREAD_UNCHANGED) #缩放比例 scale_percent = 0.3 width = int(img.shape[1] * scale_percent) height = int

weixin_38697274

基于水平集CV模型的图像分割(Matlab代码)

基于水平集CV模型的图像分割(Matlab代码),全套代码

小宇QAQ

导向滤波(guide_filter)的python实现

导向滤波(guide_filter)的python实现,可以结合暗通道;理论实现比较好的图像去噪效果。

zachzhan_g
返回顶部
烨空
博客等级 码龄10年 53粉丝 1,472原创