1. 项目缘起为什么需要一个属于自己的平台雷达PLFM_RADAR 这个名字拆开看其实很直白PLFM 是 Platform 的缩写RADAR 借用了雷达的概念——不停扫描周围环境、发现目标、跟踪目标、判断威胁并上报。说白了这就是一个给平台做扫描、发现、跟踪、预警的轻量级监控探针系统。我做这个项目的直接起因是被现有监控方案折腾得够呛。团队里跑了几十个微服务分布在好几台机器上之前用的是脚本 crontab的老办法每台机器上挂几个 shell 脚本定时 curl 一下健康检查地址挂了就发个告警到群里。这套方案在服务少的时候勉强能用但服务一多就露馅了——脚本散落在各台机器上改一个端口要登录好几台机器改配置告警逻辑五花八门有的人用钉钉机器人有的人用邮件还有人干脆只在日志里打一行字。更要命的是这些脚本只能告诉我服务挂了完全回答不了服务是不是在变慢、错误率是不是在悄悄上升这类更关键的问题。当时也评估过 Prometheus Grafana 这套标准组合坦白说确实是好东西但对于我们这种中小规模团队来说有点重。光是要维护一堆 exporter、处理服务发现规则、写 PromQL 告警表达式就得专门安排一个人去学去维护。我们的核心诉求其实很简单知道平台上的服务有哪些、活着没有、响应快不快、错误多不多出问题的时候能第一时间通知到人最好还能给一个简单的可视化界面让非技术同事也能看懂。于是 PLFM_RADAR 就这么诞生了。这个项目适合谁参考如果你也是那种服务规模在三五十个以内、团队没有专职运维、但又不想整天靠手动登录服务器查状态的场景那这套方案会比较贴合你的需求。它不需要你搭建一套完整的可观测性基础设施一台普通服务器甚至树莓派级别的小机器就能把核心功能跑起来。2. 整体设计雷达要扫什么、怎么看、怎么报2.1 核心需求拆解动手写代码之前我先把平台雷达要做的事情列成了一张清单避免做着做着就跑偏。对于一个雷达系统来说它至少要具备四个能力发现目标、跟踪目标、识别异常、上报情报。对应到 PLFM_RADAR 上这四个能力就变成了服务自动发现平台上到底有哪些服务在跑、周期性健康探测每个服务的存活状态和响应指标、异常趋势分析不只是判断挂没挂还要判断是不是在恶化、多渠道告警发现问题能第一时间找到对应的人。这四件事就是整个项目的主干后面的所有代码都是围着它们转的。有一点我想特别强调设计阶段一定要把探测频率和告警噪音的取舍想清楚。雷达如果每秒扫一次肯定能第一时间发现故障但代价是给业务服务增加了持续的请求压力而且高频率探测很容易产生大量抖动误报。如果五分钟才扫一次倒是省资源了可服务挂了五分钟才察觉对于线上业务来说这时间窗口已经够造成实际损失了。我最终的方案是分两档常规服务 30 秒探测一次核心链路服务单独配置成 10 秒一次这个后面会详细讲。2.2 技术选型不追新只求稳技术选型上我纠结过一阵子。一开始考虑过用 Go 写毕竟部署成一个二进制确实舒服但后来算了算实际功能量——服务发现、HTTP 探测、数据存储、告警推送、简单前端页面——用 Python 加几个库就能在更短的时间内实现同样的效果而且后续改起来方便。开发效率对我来说比那一点性能差异更重要毕竟这个工具本身的探测频率又不高Python 的异步 I/O 完全扛得住。数据存储这块我没有去上数据库而是用了 Redis。原因很简单雷达数据的特点是高频写入、低频读取、数据量不大Redis 的键值结构和过期机制天然适合这种场景。每个服务的探活结果存成一个带 TTL 的 Hash天然就是最近 N 分钟内的状态。更重要的是Redis 在团队里本来就是基础设施不需要额外维护一套数据库。我这里还踩了一个认知上的小坑后面在排查章节会细说。前端可视化我选择了最朴素的路子后端直接渲染 HTML 原生 JavaScript 刷新不引入任何前端框架。不是我不会用 Vue 或者 React而是这类内部工具的页面复杂度实在有限——一个服务列表、几个状态色块、一个简单的时间趋势图原生 JS 画个 Canvas 就能搞定。少一套 Node 构建链部署的时候就能少操一份心这个决策在后来的维护中帮我省了很多事。2.3 整体架构与数据流PLFM_RADAR 的架构可以用一条数据流来理解配置或注册中心 → 调度器 → 探测器 → 存储与评分 → 告警器 → 可视化面板。调度器是整个系统的心脏它维护着一张探测任务表每个任务包含目标服务地址、探测协议、频率、超时时间等参数。调度器按照各自的频率把探测任务丢给异步探测器去执行探测器拿到任务后发起实际的 HTTP 或 TCP 探测把结果状态码、响应耗时、错误信息写回 Redis同时送入异常评分模块。评分模块负责判断当前状态是否偏离正常基线如果触发了告警阈值就把事件推到告警队列由告警器统一执行去重、聚合和推送。最后Web 面板周期性地从 Redis 中读取最近的数据渲染成页面。这里有一个我认为很关键的设计决策探测和数据存储之间用队列解耦。探测器只负责测和写不负责判断和通知这样即使告警模块出了故障也不会影响探测数据的持续采集。后来有一次确实遇到了告警 webhook 配置错误导致推送失败的情况由于探测和告警是解耦的核心的监控数据并没有断修复推送配置后就恢复了这个设计救了我一次。3. 核心实现从心跳检测到异常评分3.1 服务注册与发现雷达怎么知道该扫什么雷达首先要解决的问题是扫描目标名单从哪来。我实现了两种发现方式实际使用中互为补充。第一种是静态配置文件注册适合那些地址相对固定的服务。配置文件里按服务名、环境、地址列表来组织格式类似这样services: - name: user-service group: core url: http://10.0.0.11:8080/health interval: 10 timeout: 3 expected_status: 200 contacts: [backend-oncall] - name: notification-service group: biz url: http://10.0.0.12:9090/actuator/health interval: 30 timeout: 5 expected_status: 200 contacts: [notification-owner]第二种是动态服务发现从已有的注册中心比如 Consul、Nacos 或者内网 DNS 的 SRV 记录拉取服务实例列表定时同步。这一步能自动发现新上线的实例也能感知到实例下线。对于没有注册中心的小团队甚至可以退而求其次让它定期去扫网段。我这里是优先对接 Consul因为团队里有人已经在用 Consul 管理服务配置了直接复用现有资产。两种方式并不冲突实际部署中我的建议是核心服务用静态配置确保它的探测地址是明确可控的非核心服务用动态发现减少人工维护。你仔细想想雷达的扫描目标如果本身就是动态变化的那你至少需要一个稳定锚点来保证系统不会因为注册中心异常导致整个雷达失明。3.2 探活机制除了活没活还要健不健康探活是雷达最基本的功能但活没活和健不健康其实是两个维度。我实现的探活探测不仅记录服务能否连通还采集三个核心指标响应时间、HTTP 状态码、响应体关键字。响应时间不用多说直接计时。状态码校验需要区分场景有些服务的健康检查端点返回 200有些返回 204 甚至 302所以配置里有一个expected_status字段允许自定义预期状态码。响应体关键字校验是后来加上的因为有些服务虽然返回了 200但实际上内部组件已经异常会在响应体里带出status: DOWN之类的字样。这种假阳性如果只靠状态码判断很容易漏过。探测的具体实现用了 Python 的aiohttp异步库把所有待探测目标并发执行。这里有一个参数值得注意超时时间必须比探测间隔小一个量级否则可能出现任务堆积。比如 30 秒探测一次的服务超时时间如果也设成 30 秒一旦某个服务出现慢响应探测任务迟迟不释放会导致后续探测排队产生雪崩式误报。我的经验是超时时间取探测间隔的三分之一到五分之一比较稳妥比如间隔 30 秒超时取 5 秒这就意味着你只关心服务能不能在 5 秒内有响应超过 5 秒就视为异常这个逻辑是合理的。3.3 异常评分算法用平滑均值识别温水煮青蛙只做实时探测还不够雷达真正的价值在于趋势判断。同样一个服务今天平均响应时间 200ms后天变成 400ms如果你只看单次探测结果它一直是正常的因为都在超时阈值以内。但站在平台视角这已经是一个需要关注的劣化信号了——就像体温从 36.5 ℃ 悄悄升到 37.8 ℃虽然没有烧到 39 ℃但你的身体已经在报警了。PLFM_RADAR 的异常评分模块用了一个非常经典但很实用的算法EWMA指数加权移动平均公式是ewma_new alpha * current_value (1 - alpha) * ewma_old其中alpha是平滑因子取值在 0 到 1 之间。alpha越大新数据权重越高对变化的反应就越敏感alpha越小历史数据权重越高曲线越平滑。我一般在 30 秒探测频率下取alpha 0.3这个值的意思是每次探测结果大约有 30% 的权重进入新的平均值历史数据的影响力逐渐衰减。你可以把这个机制想象成一个带记忆的滑动窗它比单纯的最近 N 次平均值更注重近期变化而且不需要维护一个窗口数组对内存极友好。有了平滑均值之后评分逻辑就变成先计算当前响应时间与 EWMA 基线的偏离程度再结合错误率最近 10 次探测中失败次数占比和趋势斜率EWMA 的变化方向综合打出一个 0 到 100 的健康分。健康分低于 80 进入预警状态低于 60 进入告警状态连续两次进入告警才真正触发通知避免偶发抖动骚扰人。这个二次确认机制非常重要是降低误报率的核心手段之一。3.4 告警通知让消息找到该找的人告警模块的核心不是发消息而是**把消息发到正确的人那里并且保持安静直到问题恢复**。我实现了通知目标配置支持钉钉/企业微信的 webhook也支持最朴素的邮件通知。每个服务在配置里指定了自己的contacts也就是这个服务出问题该通知谁从广播式轰炸所有人变成了定向通知责任人告警的干扰感一下就降低了很多。还有一个很实用的设计是告警去重与升级。同一个服务如果持续三分钟未恢复不会每分钟刷屏而是只在状态变化时发送比如进入告警发一次持续未恢复 5 分钟再发一次升级通知恢复再发一次。三十分钟内同一服务最多只发固定数量的消息防止告警风暴把有用的信息淹没。说实话很多监控工具最后被团队弃用的原因不是它不够准而是它太吵了没有人在连续一周被半夜告警轰炸之后还有耐心去看监控面板。4. 实操过程把 PLFM_RADAR 完整跑起来4.1 环境准备与依赖安装PLFM_RADAR 的运行环境要求非常简单我建议部署在一台独立于业务集群的小机器上避免监控系统和被监控对象同生共死的尴尬局面。一台 2 核 4G 的云主机、操作系统 Ubuntu 22.04 就足够了因为我们的探测频率不高实际的 CPU 占用通常连 10% 都不到。依赖方面Python 3.10 是硬性要求主要用到的库有五个aiohttp异步 HTTP 探测、redis数据存储、PyYAML读取配置、APScheduler任务调度、flask提供页面和接口。安装命令很简单pip install aiohttp redis pyyaml apscheduler flask如果是生产环境推荐用 venv 隔离避免污染系统 Python。我在开发机上用的是 conda在部署机上用 venv反正千万别图省事直接pip install --system往系统 Python 里装之后一旦因为某些依赖版本冲突把系统环境搞乱了你会后悔的。4.2 核心代码实现调度与探测调度模块是整个雷达的引擎。我用APScheduler来做异步调度每个服务注册成独立的作业间隔按各自的配置执行。核心代码不算长但牵一发动全身贴一段简化后的关键实现import asyncio import aiohttp from apscheduler.schedulers.asyncio import AsyncIOScheduler class RadarProbe: def __init__(self, service, redis_client): self.service service self.redis redis_client self.timeout service.get(timeout, 5) self.interval service.get(interval, 30) self.ewma None # 初始基线为空 async def check(self): url self.service[url] expected self.service.get(expected_status, 200) start asyncio.get_event_loop().time() try: async with aiohttp.ClientSession() as session: async with session.get(url, timeoutself.timeout) as resp: latency (asyncio.get_event_loop().time() - start) * 1000 body await resp.text() status_ok resp.status expected keyword_ok (self.service.get(keyword) or ) in body ok status_ok and keyword_ok except Exception as exc: latency -1 ok False self._update_ewma(latency if ok else None) score self._score(ok, latency) self._persist(ok, latency, score, url) if score 60: await self._raise_alert()这段代码里有几个细节值得展开说一下。第一我每次探测都新开一个ClientSession而不是复用全局 session。这样做的原因是探测间隔较长长连接复用的收益几乎可以忽略而新开会话可以避免因为某个服务的异常连接导致连接池污染从而影响到探测其他服务。单个会话里打开的连接如果被服务端异常关闭留在池子里就会产生大量ConnectionResetError误报我踩过这个坑。第二_update_ewma这个方法在探测失败时会传入None此时不更新 EWMA 基线只更新错误计数。这个设计逻辑是一次失败不应该立刻把正常基线拉低否则在频繁的短时抖动中基线会被污染成一个很低的水平导致后续真正异常的响应时间反而看起来正常。简单说基线只学习健康状态不学习病态状态这样异常就会显得更加扎眼。第三_persist方法把探活结果写入 Redis键名设计为radar:status:{service_name}字段包括ok、latency、score、timestamp并设置 TTL 为 30 分钟。这个 TTL 很关键它保证了 Redis 不会无限堆积历史数据也天然实现了超过 30 分钟没有新数据的服务视为失联的逻辑不需要额外写扫描任务来清理脏数据。4.3 异常评分与告警触发评分模块我单独拆成一个类这样便于单测。核心逻辑是对比当前探测值与 EWMA 基线的偏离程度加上错误率因素综合产出健康分。简化实现如下class HealthScorer: def __init__(self, alpha0.3, warn_threshold80, alert_threshold60): self.alpha alpha self.warn_threshold warn_threshold self.alert_threshold alert_threshold def update_ewma(self, ewma, current): if ewma is None: return current return self.alpha * current (1 - self.alpha) * ewma def score(self, ewma, current_latency, error_rate): # 响应时间偏离度当前值相对基线每偏离 50%扣 20 分 latency_score 100 if ewma and current_latency ewma: deviation (current_latency - ewma) / ewma latency_score - int(deviation * 40) latency_score max(latency_score, 0) # 错误率直接扣分错误率 20% 开始扣50% 以上归零 error_score 100 if error_rate 0.2: error_score - int((error_rate - 0.2) * 200) error_score max(error_score, 0) # 综合取两者较低者体现短板效应 score min(latency_score, error_score) return max(score, 0)为什么综合分取两者较低而不是平均分因为一个服务如果响应很快但错误率已经 50%或者错误率很低但响应时间已经慢了 10 倍两种情况中任何一种都代表了平台处于不健康状态。取低者能让告警逻辑更保守、更敏感在监控场景下宁可误报不可漏报通常是更安全的选择。平均分很容易把两个维度的问题互相抵消导致最终评分看起来还行但实际上平台已经出问题了。告警触发逻辑放在调度器的回调里。每个服务进入告警状态后会设置一个 Redis 标记radar:alerting:{service_name}标记本身也有 TTL默认三分钟。三分钟内如果评分仍低于告警阈值就发送升级通知一旦评分恢复正常立即清除标记并发送恢复通知。这个 TTL 机制非常巧妙地实现了告警保持期不需要额外的状态机管理全靠 Redis 过期时间驱动。4.4 可视化面板与告警测试Web 面板我用了 Flask 来渲染路由只有两个首页展示所有服务当前状态历史页展示单个服务的评分曲线。前端每 10 秒轮询后端接口/api/status拿到 JSON 数据后更新页面上的状态色块绿色代表健康、黄色代表预警、红色代表告警、灰色代表失联。这个设计足够直观连团队里的产品经理都能一眼看清平台现在是好的有点问题还是出大事了。后端接口代码很简单就是从 Redis 里批量读取各服务的状态字段、评分和最近 N 次响应耗时拼成 JSON 返回app.route(/api/status) def api_status(): result {} for name in service_registry.keys(): data redis.hgetall(fradar:status:{name}) result[name] { ok: data.get(bok) b1, score: int(data.get(bscore, 100)), latency: float(data.get(blatency, -1)), last_update: data.get(btimestamp, ), } return jsonify(result)部署完成后一定要做一次告警联动测试不要等真的出故障了才发现 webhook 地址配错了。我的做法是临时把 favorite 服务改成探测一个不存在的端口等告警推送到群里之后再改回真实地址确认恢复通知也能正常发出。这套先演练后上线的流程建议每个人都走一遍告警工具最大的坑就是平时不响真响的时候又因为配置问题没人收到那这雷达就等于白做了。5. 部署中踩过的坑与排查实录5.1 典型问题速查我把自己和身边同事在实际部署 PLFM_RADAR 过程中碰到的问题整理成了一张速查表建议直接收藏能帮你省下不少排查时间现象可能原因排查与解法所有服务全部显示失联Redis 连接配置错误或 Redis 被防火墙拦截先手动跑redis-cli ping验证连通性再看日志连接异常信息单服务频繁闪红超时时间设置过短与真实响应分布边界重叠调大超时时间到 P95 响应时间的两倍以上再看是否复现告警只发一次后不再发告警 TTL 与探测间隔不匹配导致标记一直未过期检查radar:alerting:*键的 TTL确保 TTL 与升级频率匹配响应时间数据大段缺失ClientSession复用导致的连接池污染恢复为每次探测新建会话或定期关闭陈旧连接评分骤降但服务正常EWMA 基线初始值为空首次数值直接成为基线服务注册后先静默采集 10 个样本再开启评分告警内容显示未知服务配置中服务名与 Redis 键名有空格或编码差异统一服务名规范加一个配置校验步骤强制无空格第一个问题是最常见的——很多人部署完成后发现雷达一片灰第一反应是检查探测代码结果折腾半天发现是 Redis 密码没配对。这其实反映了一个普遍心态我们总倾向于怀疑自己写的复杂逻辑却忽略了最基础的网络连通性。排查问题一定要从底层往上层查网络通不通、存储通不通、配置对不对、代码有没有 bug这个顺序不要反。5.2 两个值得单独讲讲的血泪坑坑一超时时间设置不合理导致的连锁误报。有一段时间我发现某个服务的告警特别频繁但实际登录服务器看业务一切正常。查了半天发现我给它配置的超时时间是 3 秒而这个服务在高峰期 P95 响应时间经常到 2.8 秒左右。这就意味着每隔几秒就会有一次探测踩到超时边缘产生一次误报。调大超时到 5 秒之后误报立刻消失了。这件事给我一个教训超时时间应该基于真实的响应分布来定不是拍脑袋填的。坑二探测频率和告警 TTL 的联动效应。有一版代码里我把探测间隔改成了 10 秒但告警标记的 TTL 依然是 5 分钟。调试的时候发现服务恢复正常后告警的恢复通知总是在正常后的几分钟才发出延迟非常明显。原因就是 TTL 太长服务恢复后评分虽然上去了但告警标记还没过期导致恢复逻辑检测不到当前处于告警中状态。后来我把 TTL 调整成探测间隔的 6 倍让标记最多在两次探测周期内必然过期探测越频繁告警状态翻转就越快这个联动关系就理顺了。5.3 监控系统本身的健康检查一个雷达如果连自己坏了都不知道那比没有雷达还要危险。我为 PLFM_RADAR 加了一个简单的自监控机制进程内部每 60 秒写一次心跳到 Redis 的radar:self_heartbeat键TTL 设置为 180 秒。如果外部巡检脚本发现这个键不存在说明雷达进程已经挂掉或者无响应这时候就不再依赖雷达自己的告警而是由独立的 crontab 触发一个最简单的邮件通知。这个最后的兜底不需要很复杂但必须有因为在一些极端情况下监控进程自己也可能被系统 OOM 杀掉。6. 最后再说几句实在话PLFM_RADAR 这个项目从最初的念头到稳定运行前前后后花了两周左右的业余时间。它远谈不上完美功能上比 Prometheus 那套体系差了不是一星半点但对我来说它恰好解决了一个实际问题用最小的成本让团队重新拥有了对平台的感知力。这种感知力很重要——当平台出问题你不再是最后一个知道的人很多线上的小事故就能在用户察觉之前被摁掉。如果你也想照着这个思路做一个自己的平台雷达我给你三个建议。第一先列清楚你要监控的对象和每类对象的告警联系人这个比写代码更花时间但绝对值得第二允许自己从最简单的版本开始哪怕只做心跳探测 微信群通知两步也比不做强第三把减少误报当做一个持续优化的目标来对待因为误报消耗的信任成本远高于漏报。我个人实际用了几个月之后的体会是最有用的并不是那个可视化面板而是把探测—评分—告警这条链路跑通之后带来的安全感。平台雷达的价值不在于技术多复杂而在于它让你的运维工作从被动救火变成了提前感知。以后我再扩展的话可能会给它加上简单的预测功能——基于历史响应数据做一个小时级的趋势预判争取在服务变慢之前就有所动作。不过那是后话了先把眼前这套用扎实比什么都强。