大概花了两周时间用下班后的零散时间把 PLFM_RADAR 这个项目从想法磨成了能跑的东西。说白了它就是给整个平台做了一双“天眼”——把所有服务、主机、中间件、数据链路的状态汇聚到一张雷达式的视图上让平台里正在发生什么、哪里在告警、哪个服务在抖动一眼就能扫出来。这篇文章就聊聊这个项目我是怎么拆解、怎么选型、怎么把代码落到实际环境里的包括踩过的那些坑。1. 项目概述为什么要做一个“平台雷达”先说清楚 PLFM_RADAR 到底解决什么问题。做过平台运维的人都有这种体验跑着一个几十个微服务加上一堆中间件的系统出了故障第一反应是“先看看监控”。但真到看监控的时候往往要翻好几个页面——Prometheus 看指标、Kibana 看日志、调用链系统看链路等把信息凑齐了时间已经过去十分钟。PLFM_RADAR 的核心思路就是把这类散落的信息收拢到一个统一视角里。它不是一个全新造轮子的监控系统更像是一个“聚合层 展示层”底层接各路数据源上层画一张形似雷达扫描屏的可视化界面。进入界面时你面对的是一个模拟雷达屏幕中心代表平台核心向外一圈圈扩散的是不同的服务/节点/模块光点表示健康状态轨迹线表示服务依赖或调用关系异常项会像雷达锁定目标一样被高亮框出。任何在平台里发生的明显异常基本能做到几秒钟内定位到具体模块而不是在一堆面板之间来回跳。这个项目适合谁来参考如果你是后端开发、SRE、DevOps 工程师或者正在维护一个小型但服务数量已经开始失控的平台那这个思路可以直接抄。如果你纯粹是个人做项目练手那它涉及的采集、存储、可视化、异步任务调度这些知识点也足够充实了。它的定位不是替代成熟监控系统而是做一个反应更快、更直观的“指挥视图”。需要说明的是项目本身并不是某个特定开源产品的代理而是我基于常见实践自行设计和实现的一套轻量方案。后文里出现的代码、参数、配置都是我在这类场景里反复测试后觉得合理且稳妥的做法你可以直接拿去用也可以根据自己的环境调整。2. 整体架构与方案选型2.1 雷达隐喻背后的设计逻辑在动手写代码之前我先把产品的交互形态想清楚了。雷达这个词不是随便起的它对应的是几个非常具体的能力需求。第一是“全局扫描”。雷达扫描一圈所有目标都在屏幕上不需要逐个页面去翻。对应到平台上就是以固定周期扫描所有被纳入监控的服务和节点把它们的健康状态、响应耗时、错误率、资源水位统一采集上来。第二是“目标锁定”。雷达发现异常目标后会用光标锁定并持续跟踪。对应到平台里就是异常发生时要能自动标记、关联上下文、保留现场并且随着时间推移展示出这个异常是变好了还是持续恶化了。第三是“距离与方位”。雷达能告诉你目标在哪个方位、距离多远。映射到平台中就是服务的依赖关系和调用拓扑。某个下游服务异常时从雷达视图上能直观看到哪些上游服务受到了影响报警就不再是孤立的“某服务 502”而是能够看到整条调用链上的波及面。这个设计逻辑决定了整个项目的架构走向必须有一个能够持续采集数据的轻量采集端必须有一个能够存储时序数据并做简单分析的存储层必须有一个能实时推送变化的前端通道最后才是雷达界面的渲染。采集层负责从服务探针、系统指标、日志源中抽取状态数据存储层负责写时序数据、保存告警事件、存储依赖关系分析层负责计算健康分、检测异常、生成趋势展示层负责把数据渲染成雷达视图提供实时刷新和交互2.2 技术栈选型为什么没有全上大厂套件说实话一开始我也纠结过要不要直接搭 Prometheus Grafana AlertManager 全家桶。这套东西成熟稳定社区资料也多。但仔细想了一下它们更多是“通用监控面板”思路距离“雷达联动视图”的表达方式还差一层定制化的壳。如果为了做这个项目而去二次开发 Grafana 插件成本反而更高。PLFM_RADAR 的技术栈我最终定成这样模块选型理由采集端 AgentPython 3 asyncio psutilPython 上手快asyncio 并发采集效率高psutil 直接拿系统指标数据上报HTTP JSON 批量上报简单直接不引入额外消息队列小规模平台下完全够用服务端Node.js Express单进程事件模型适合大量轻量请求写 WS 推送也天然顺手存储SQLite 内存环形缓冲时序数据量不大时 SQLite 足够热数据留在内存里保证查询速度前端Vue 3 Canvas 2DCanvas 画雷达扫描效果最灵活Vue 负责状态管理实时通道WebSocket雷达视图必须近乎实时刷新轮询达不到这个体验这套组合最明显的好处是部署成本极低。服务端加了 SQLite连数据库都不用单独装Agent 用 Python 写目标机器上只要有 Python 3.8 就能直接跑。对于一个几十台机器、上百个服务接口规模的项目来说这套东西的吞吐量绰绰有余单体部署反而省了无数运维心智负担。2.3 数据模型设计区分快数据和慢数据这是我做这个项目时最重要的一个设计决策也是我觉得值得展开讲的一点。雷达视图上展示的数据其实可以分为两类一类是几秒钟就要刷新一次的快数据比如 CPU 使用率、接口响应时间、错误计数另一类是没那么讲究时效的慢数据比如服务依赖关系、节点元信息、告警处置历史。如果两类数据混在一起存、混在一起取后果就是查询接口为了拿依赖拓扑这种几乎不变的数据也要反复扫描大表写数据时又因为要维护一堆冗余字段导致写入变慢。我把它们拆开了快数据走内存环形缓冲加 SQLite 定时落盘。环形缓冲长度为 120 个采样点每 5 秒采样一次的话就是保留最近 10 分钟的热数据查询起来都是纯内存操作。慢数据直接落 SQLite建好索引平时基本不更新只在服务注册或拓扑变化时写入。这个思路和很多时序数据库底层其实是相通的——热数据留在内存冷数据进磁盘按时间维度滚动淘汰。只是在这个规模下不需要动用专门的时序数据库用工程手段就能解决。3. 核心细节解析与实操要点3.1 健康分是怎么算出来的雷达视图里每个光点都对应一个健康分分数越高光点越亮、越偏绿越低就越偏红。这个健康分不是拍脑袋定的而是由几个因子加权得到的。我参考的是常见的服务健康度评估方式并结合了实际监控数据的可得性。每个采集周期里Agent 会为每个被监控目标计算一次原始分包含四块可用性占比 40%过去 60 秒内成功请求数占总请求数的比例延迟占比 30%当前 P95 延迟与基线延迟的比值资源占比 20%所在节点的 CPU 和内存水位的加权值错误率占比 10%5xx 错误占全部错误的比例具体计算的时候要注意不是所有情况下都能拿到完整数据。有些服务没有接口监控只有进程在跑这时候就退化为资源分加可用性降级处理。我在代码里对数据缺失做了兜底如果某个因子没有数据就把它的权重按比例分摊到已有因子上而不是直接给 0 分。否则一个没有接口监控的服务会永远显示为红色那雷达图就完全失去意义了。3.2 服务依赖关系怎么发现要画出雷达屏上的轨迹线前提是知道谁依赖谁。最硬核的做法是接链路追踪系统拿到完整的调用拓扑。但对于我自己的项目环境来说链路追踪本身还没有普及于是我用了两种比较轻的替代方案。第一种是静态配置。在服务注册的时候每个服务可以声明自己依赖哪些下游服务的地址、数据库、缓存。这个方案准确率高但需要人工维护漏了也就漏了。第二种是流量推断。Agent 会在本机抓取五分钟内的网络连接统计把当前服务对外部特定端口的连接频率做个排序高频连接的目标会自动生成一条“疑似依赖”的虚线。虚线会随着时间推进逐渐收敛如果连续多个周期都检测到同一个目标的连接就把虚线转成实线。说实话流量推断的准确率肯定赶不上链路追踪但在没有 APM 系统的情况下它至少能给你画出 80% 的主干依赖关系。等以后接入了真正的链路追踪系统这部分数据可以直接替换。3.3 异常锁定的实现方式雷达屏幕上那些被高亮框住并标注“LOCKED”的目标是整套系统里交互反馈最重的一个特性。它的实现逻辑是这样的后台分析模块会为每个服务维护一个状态机状态从 NORMAL 到 WARNING 到 CRITICAL 逐级转换。当健康分连续三个采样周期低于 60 分时会从 WARNING 升级为 CRITICAL并触发锁定事件。锁定之后前端就会用专门的框线把这个光点圈住同时后端开始记录这个异常的上下文信息开始时间、持续时长、相关指标变化序列、关联的依赖轨迹。这里有个细节我一开始踩了坑连续判定不能太敏感。最开始我只看了单个采样点低于阈值就触发结果服务偶尔抖了一下就直接锁定雷达屏上红框乱闪反而变成了噪音。后来改成了连续三次采样判定同时引入了一个“恢复滞后”机制——即使健康分恢复到了 60 分以上也要连续两次确认才能解除锁定避免反复横跳。4. 实操过程与核心环节实现4.1 采集端 Agent 的实现Agent 是整个系统里代码量最大也最琐碎的部分。每个被监控节点上跑一个 Python 进程主要做三件事采集系统指标、采集服务接口指标、上报数据。系统指标的采集直接用的 psutil这部分没什么技术含量但要注意采集的线程模型。如果纯粹按顺序采集几十个指标每轮耗时可能达到两秒甚至更多对于五秒一个采集周期来说开销偏大。我改成用 asyncio.gather 并发执行采集任务一轮采集从两秒降到了零点三秒左右。接口指标的采集需要每个业务服务暴露一个标准的 health 端点。如果服务本身没有这个端点Agent 会退化为进程检查和端口检查。端口检查我使用 socket 去连一下 TCP 端口不做 HTTP 请求避免给业务服务造成额外压力。上报的方式是 HTTP JSON 批量上报。攒够一二十条或达到五秒定时触发一次就把这一批数据打包 POST 到服务端的/api/ingest接口。选择批量上报而不是每条单发是为了减少 HTTP 连接建立的次数这也是小规模场景下性价比很高的优化。import asyncio import json import socket import time import urllib.request import psutil SERVER_URL http://plfm-server.local:8080/api/ingest NODE_ID node-01 INTERVAL 5 def collect_system_metrics(): cpu_percent psutil.cpu_percent(interval0.2) mem psutil.virtual_memory() disk psutil.disk_usage(/) return { node: NODE_ID, ts: int(time.time()), type: system, metrics: { cpu_percent: cpu_percent, mem_percent: mem.percent, disk_percent: disk.percent, }, } def check_tcp_port(host, port, timeout0.5): try: with socket.create_connection((host, port), timeouttimeout): return True except OSError: return False async def collect_loop(): while True: payload [collect_system_metrics()] # 这里补充服务探针采集结果追加到 payload data json.dumps(payload).encode(utf-8) req urllib.request.Request( SERVER_URL, datadata, headers{Content-Type: application/json}, ) try: with urllib.request.urlopen(req, timeout2): pass except Exception as exc: print(f[agent] upload failed: {exc}) await asyncio.sleep(INTERVAL) if __name__ __main__: asyncio.run(collect_loop())这里面有个细节值得说psutil.cpu_percent(interval0.2)是有阻塞的它会等 0.2 秒来做采样计算。如果你在 asyncio 的事件循环里直接调用它整个循环都会卡住。我的处理方式是把它包进asyncio.to_thread里执行让它在单独的线程里跑不阻塞主事件循环。4.2 服务端接入层与数据落地服务端用 Express 起了一个相对简单的 HTTP 服务核心接口就两个一个是/api/ingest接收 Agent 上报数据另一个是/api/snapshot给前端拉取全量状态快照。WebSocket 通道负责把后续的增量变化推给前端。数据落地分两个层次。内存里的环形缓冲我用一个简单的列表加游标实现每个 target 对应一个长度为 120 的环每次写入新采样点就把游标往前推一格超过长度就覆盖最旧的数据。这个结构写起来不到二十行但查询最近十分钟的数据时是纯内存操作速度快到可以忽略。SQLite 的写入我做了批量提取。每收到一批数据不会立刻 insert而是先缓存在内存 List 里攒够五十条或每十秒触发一次批量写入。SQLite 在事务里批量插入的速度比单条插入快一个数量级这个优化对降低 CPU 开销非常明显。// server/store.js 简化示例 class RingBuffer { constructor(capacity) { this.capacity capacity; this.buffer []; this.cursor 0; } push(value) { this.buffer[this.cursor % this.capacity] value; this.cursor 1; } latest() { if (this.cursor 0) return null; return this.buffer[(this.cursor - 1) % this.capacity]; } all() { const start Math.max(0, this.cursor - this.capacity); const result []; for (let i start; i this.cursor; i) { result.push(this.buffer[i % this.capacity]); } return result; } }SQLite 落库时我把表按“指标表”“事件表”“拓扑表”三张分开指标表做了(node, target, ts)的联合索引事件表里status和created_at分别建索引。这样查询最近告警和按时间范围拉取指标时走的都是索引范围扫描数据量在千万条以内不会有什么性能压力。4.3 雷达视图前端是怎么画的前端是整个项目里视觉效果最核心的部分也是最能体现“雷达”这个概念的模块。整体实现基于 Vue 3 的组件架构画布部分用的是 Canvas 2D API。雷达屏的静态元素包括同心圆网格线、角度刻度、中心原点。动态元素包括扫描扇区、目标光点、依赖轨迹线、异常锁定框。扫描扇区是一块从 -90 度开始旋转的扇形渐变区域用 CSS 变量驱动的 requestAnimationFrame 来控制旋转每帧旋转 0.5 度左右转一圈大概要两秒。要注意的是旋转逻辑不能放在 Vue 的响应式数据里否则每一帧都会触发虚拟 DOM 更新直接把性能拖垮。正确做法是让 Canvas 的绘制完全绕开 Vue 的响应式系统Vue 只负责传入数据快照动画循环里直接读取这些数据并绘制。目标光点的位置投影规则我简化成了按服务分组映射到不同半径的环带上核心中间件在第二环业务服务在第三环边缘节点在最外环。同一环带上按服务名哈希决定角度保证每个服务的角度长期稳定。这样当某个服务异常时扫一眼它的位置就能大致判断出它在平台里的层级和分组。function drawRadarFrame(ctx, snapshot, scanAngle) { // 先画静态元素同心圆、刻度 drawGrid(ctx, WIDTH, HEIGHT, RADIUS_MAX); // 再画依赖轨迹线 snapshot.links.forEach((link) drawLink(ctx, link)); // 画目标光点 snapshot.targets.forEach((target) drawTarget(ctx, target)); // 最后画扫描扇区 drawScanSector(ctx, scanAngle); // 对处于 LOCKED 状态的目标画锁定框 snapshot.locked.forEach((target) drawLockBox(ctx, target)); }WebSocket 推送的增量更新里我约定了几种消息类型target_update、target_lock、target_unlock、topology_change。前端收到消息后只修改对应的非响应式数据对象动画循环在下一帧自然就画出了新状态。实测下来五个节点的界面只要 Canvas 绘制跟得上整体流畅度没问题CPU 占用率在 8% 左右。但这里有个明显的瓶颈——目标数量超过两百个时每帧全量重绘的耗时就会上涨后面我加了一个简单的脏矩形优化锁定框区域只重绘矩形范围内而不是全屏能缓解一部分压力。4.4 异常检测与告警编排异常检测我采用的是“基线偏移检测 阈值规则”的混合策略。阈值规则好理解比如 CPU 使用率超过 90% 就告警接口 P95 超过 800ms 就告警。基线偏移检测稍微复杂一点系统会为每个服务保存过去七天同时段的指标均值作为基线当当前值偏离基线超过三个标准差时即使绝对值没有超过阈值也会判定为异常。这么做的好处是能抓到那些“平时 10ms 现在 80ms”的缓慢劣化问题。告警编排这一块我做了分级的收敛每类告警会先进入“预警告”状态如果在 30 秒内连续触发三次才升级为正式告警并锁定目标。同一目标的同类告警在半小时内不重复升级避免告警风暴。这个分级设计和雷达锁定机制是联动的锁定的目标会显示在雷达屏中央的“被跟踪目标列表”里点击任何一个锁定目标右侧会滑出一个详情面板展示它的实时指标曲线、异常开始时间、依赖链路和关联事件。告警最终要能把人叫醒所以我也接了一个轻量的通知通道走 Webhook 推到企业微信和钉钉机器人。通知文案里会带上雷达屏的链接让人直接点进去看。这里有一个我用下来觉得特别顺手的小技巧告警通知里不写一堆指标数值只写三句话——是什么问题、影响哪些服务、什么时间开始的。需要看细节的人自然会去雷达屏上点不需要的人看了也不会烦躁。5. 常见问题与排查技巧实录做这个项目踩了不少坑有些问题在官方文档里根本找不到答案只能靠日志排查和反复试验。我把最典型的几类问题整理成了一张速查表后续如果有人照着这个方案搭自己的版本可以直接对照排查。现象原因排查思路Agent 上报失败服务端收不到任何数据上报 URL 配错或目标机到服务端网络不通先用 curl 手动 POST 一条测试数据确认接口能通后再排查 Agent 日志前端长时间不更新数据WebSocket 连接断开后未自动重连检查服务端是否做了心跳保活前端增加断线重连机制间隔 3 秒重连一次雷达屏目标位置每次刷新都变目标角度映射没有使用确定性哈希把角度计算改成基于服务名的稳定哈希不要依赖对象遍历顺序内存占用持续上涨环形缓冲容量设置过大或者事件表无限增长环形缓冲控制在 120 以内事件表增加定期归档清理任务健康分大面积飘红但实际服务正常基线数据不足或因子缺数据未兜底刚上线时不要直接启用基线偏移检测先跑两天采集基线数据告警风暴同类告警反复触发缺少收敛机制恢复判定过于敏感增加连续 N 次采样判定和半小时间隔去重避免单个抖动引发连锁告警5.1 Agent 上报失败的几个隐蔽原因Agent 上报失败是最常见也最好排查的问题但有几个隐蔽原因很容易被忽略。第一个是目标机上的 Python 版本太老比如还在跑 Python 3.6那么代码里用的 f-string 嵌套引号和 asyncio 的一些新特性会直接语法报错。我在代码开头加了一个版本检查低于 3.8 直接打印提示退出算是把问题前置暴露出来。第二个是防火墙和代理。有些内网机器设了 HTTP 代理环境变量Agent 的urllib.request会默认走系统代理导致请求被代理网关拦掉。这个问题的表现很迷惑——日志里报的故障码五花八门有时是 502 有时是连接超时。排查下来发现只要在请求时强制不走代理就能解决。我在代码里加了显式的不使用代理逻辑确保内网直连。第三个是 Agent 进程被系统杀掉。如果没有用 systemd 或 supervisor 守护Agent 一旦因为内存问题被 OOM Kill 就不会自己起来。我写了一个简单的 systemd service 单元Restartalways加RestartSec5实测下来稳定性提升非常明显。5.2 前端性能的优化过程雷达屏前端的性能优化是我花时间最多的地方也确实走过弯路。初期我为了让代码结构清晰把所有指标数据都放进了 Vue 的reactive对象里结果发现每次 WebSocket 推送都要触发大批量的响应式依赖更新一帧动画还没画完下一帧又来了整个界面肉眼可见地卡顿。后面我明白了关键要紧Canvas 的绘制过程应该完全脱离 Vue 的渲染周期。我把数据快照对象用普通的非响应式对象保存需要展示的目标列表用一个原生 Map 维护Vue 只负责把快照的引用传给画布组件画布组件在 requestAnimationFrame 里读取这些数据并绘制。这样改完之后页面每秒六十帧的绘制非常稳定CPU 占用也大幅下降。还有一个优化是合并 WebSocket 消息。Agent 每五秒上报一批数据如果每条数据都单独推送一条 WS 消息前端要高频处理大量小消息。我改成服务端攒一个批次每两秒统一推一次快照增量。这样前端一帧动画正好拿一批新数据把“推送频率”和“绘制频率”解耦开观感舒服多了。5.3 SQLite 在大批量写入时的经验数据量上来之后SQLite 批量写入也是个容易出问题的点。早期我在每一条指标到达时直接执行 INSERT几百个服务同时上报时CPU 占用立刻飙升服务端接口响应时间也被拖长。我最开始还以为是 Express 的问题后来通过日志和慢查询定位到了 SQLite 写入频繁上。解决办法就是把单条插入改成事务批量插入并且把写入操作从请求处理链路里拆出来。现在架构是接口收到数据后只负责做校验和放入内存队列后台一个定时任务每十秒把队列里的数据统一写入一次 SQLite 事务。这样接口的响应时间稳定在十毫秒级别写入压力也被均摊了。SQLite 的 WAL 模式我也开了读写并发比默认的回滚日志模式好很多而且出现异常崩溃后数据恢复也更容易。6. 从雷达屏到自动化处置的扩展方向PLFM_RADAR 做到能用的状态之后身边同事问的最多的就是雷达屏能不能不只是“看”而是直接“做”其实这个方向我一直在想也已经在计划里了。当前版本的雷达定位是“观测”所有异常都需要人来决策但观测数据一旦足够完整就很自然地会往“自动化处置”演进。第一个可以落地的扩展是故障自愈。当雷达锁定到某个目标异常同时判定原因是资源耗尽时就可以触发预设的处置动作对目标所在节点做一次温和的扩容或者把流量摘掉等待服务自动恢复。这个能力不需要改架构只要在告警编排和动作执行之间加一层策略引擎。策略引擎的规则用最简单的 if-then 表达就行不要一上来就搞复杂的机器学习模型先把 80% 的常规场景用规则覆盖好剩下的再慢慢迭代。第二个可以扩展的方向是容量预测。雷达滚动环里保存的近期数据用线性回归或者很简单的移动平均预测未来半小时的资源水位准确率已经能应付大多数场景。当预测曲线显示某个节点将在二十分钟后 CPU 打满时系统可以提前在雷达屏上把对应目标标成橙色预警而不是等它真正打满之后再锁定。这种“预防式告警”的体验比事后告警好太多用户对系统的信任感会完全不一样。第三个方向是更丰富的依赖拓扑发现。目前流量推断只能画到端口级依赖如果想精确到接口级就需要 Agent 内部做轻量的请求采样。比如记录 HTTP 调用中的上游服务名和下游路径把这些信息上报后聚合成调用链统计。有了真正的链路数据雷达屏上那些轨迹线就不仅仅是装饰而是能真实反映故障传播路径了。出了问题时可以直接在雷达屏上从异常目标出发高亮所有被传播影响的服务这比我看过的不少商业 APM 的拓扑图都好用。这些扩展方向我都已经列进了后续迭代的清单里第一个版本先把“看得到、转得快、定位准”这个核心体验做好后续再加“动得了”的自动化能力。每一步都不追求大而全只保证解决当下最痛的问题。这也是我做这个项目最重要的一条心得监测系统做得再酷如果不能让人在最短时间里做出正确的判断和行动那它就只是个漂亮的大屏。我个人的感受是这类平台可视化项目最大的价值不在于技术堆得有多高而在于你是否能从使用者的角度重新审视那些已经习以为常的数据和界面。雷达这个隐喻帮我把“全局感知”“异常锁定”“依赖追踪”这三件运维里最核心的事情用一种直觉化的方式表达了出来。如果你也在维护一个服务数量开始变多的平台强烈建议你也试试——不需要一步到位先画出一个能看、能转、能锁定的雷达屏就已经能感受到监控体验的明显变化了。