这两年我一个人维护着好几个线上服务——有给客户端提供数据的API网关,有跑定时任务的Worker,还有几个对外提供查询的数据库实例。说实话,最怕的不是功能写不出来,而是某个服务在凌晨三点悄悄挂掉,然后第二天被用户在群里@ 才后知后觉。PLFM_RADAR 这个项目,就是因为这种狼狈而产生的。
PLFM 取自 Platform 的缩写,RADAR 就是雷达的意思。合起来,它是一套“平台运行状态雷达”系统——定时去探测各个服务的存活状态、延迟、错误率、资源占用,把这些数据汇总成可视化的面板,并且在指标异常时通过钉钉、邮件等方式第一时间通知我。它的定位很明确:不抢 APM 全家桶的活,只做一件事,那就是让你知道你的服务现在健不健康、什么时候开始不健康、为什么不健康。
这个项目适合谁参考?我建议两类人重点看看:一类是自己手里有几个服务但没精力搭整套 Prometheus + Grafana 的独立开发者;另一类是团队里想快速实现“监测闭环”但不太想被复杂技术栈绑架的小组。文中的所有方案,我都是在真实部署环境下跑过一段时间的,踩过的坑、绕过的弯,都会尽量写清楚。
1. PLFM_RADAR 项目概述与设计思路
1.1 核心需求解析:你到底需要“监”什么
动手之前,先把需求掰开。PLFM_RADAR 想解决的问题有三层:
第一层是存活检测。服务是否还在运行,端口能不能连通,健康检查接口是否返回 200。这是最基础但也是最重要的,线上 80% 的事故都始于“某个服务默默退出了”。
第二层是性能监控。响应延迟有没有变慢,错误率是不是在攀升,系统资源 CPU、内存、磁盘是不是快打满了。这些指标是缓慢恶化的过程,等到用户报障时往往已经影响业务了,雷达需要在恶化初期就把异常信号拉出来。
第三层是联动分析。单个服务的指标异常未必能说明问题,但如果多个服务同时延迟升高、错误率上扬,那大概率是下游数据库出问题了。PLFM_RADAR 把各层服务的指标放在同一个时间轴上对齐,方便快速定位故障层级。
这三层需求对应到系统设计,就是三个模块:采集模块、规则引擎、展示面板。我见过很多人一上来就追求大而全,把消息队列、分布式链路追踪全塞进来,结果项目烂尾。其实监控系统最核心的价值是“先响起来”,至于定位根因,那是后续一步步精细化的事。所以 PLFM_RADAR 一开始的定位就是克制的:能探测、能告警、能看趋势,就够了。
1.2 为什么选择“自研轻量探针 + 规则引擎”而不是直接上 Prometheus
先给结论:如果你已经有成熟的 Kubernetes 环境,直接上 Prometheus Operator 是合理的。但我自己的服务分散在几台普通云服务器上,有的跑在 Docker 里,有的干脆是 systemd 托管的裸进程,强行引入 Prometheus 体系反而要维护一大堆 exporter,学习成本和运维成本都不低。
PLFM_RADAR 的做法是用 Python 写一个轻量探针,直接通过 HTTP 轮询各服务的健康接口,同时用 psutil 采集本机的 CPU、内存、磁盘、网络指标。这个探针采集到的数据写入一个时序数据库,规则引擎定期扫描数据,一旦触发阈值就走告警通知。这个方案的优点是:核心逻辑完全是可控的,指标口径、告警规则、通知渠道都捏在自己手里,出了问题你在 200 行代码内就能定位,而不是去翻一堆 YAML 配置和 operator 的文档。
当然,这个方案也有明显的短板,最主要是性能上限。HTTP 轮询的实时性远不如推模式,秒级监控想都不要想,我实际用下来 30 秒到 1 分钟粒度的探测比较合适。另外,如果未来服务数量到了几百上千,这个架构会先遇到瓶颈。所以我的建议是:服务数量在 30 个以内、追求快速见效的团队,用 PLFM_RADAR 的思路;如果规模更大,再平滑迁移到 Prometheus 体系不迟。
1.3 技术选型背后的几个关键决定
做技术选型的时候,我反复权衡了三组矛盾,这里直接分享结论。
时间序列存储,我最后用了 InfluxDB 1.8 的免费单机版。原因很简单:它自带数据保留策略,可以自动清理一个月前的旧数据,而且查询语法对“最近一小时的平均延迟”“最近五分钟的错误率”这类聚合计算支持得非常好。对比的话,用 SQLite 起步确实更轻,但数据一多查询越来越慢,过期数据还得自己写脚本清理,我踩过这个坑,不建议你走弯路。
前端可视化,我用的是 ECharts 的雷达图和折线图。雷达图天生适合展示多维度指标的综合健康度,一眼就能看出来哪个维度“塌”了;折线图则适合看趋势变化。我见过有人用 Grafana 做同样的事,当然很好,但 Grafana 那种表格化、搜索式的交互和“雷达图”这种直观形态相比,少了一种“整个平台的健康状况尽收眼底”的感觉。而且 ECharts 可以直接嵌到一个轻量 Flask 页面里,部署简单很多。
告警通知,我接的是钉钉群机器人。用 Webhook 发一条 JSON 就行,不需要申请额外的 SDK 权限,而且手机端推送很及时,基本能做到 10 秒内送达。如果你团队用企业微信或者飞书,原理完全一样,改下请求地址和报文格式而已。
2. 核心功能拆解:数据采集、指标计算与告警规则
2.1 数据采集探针的实现细节
探针是整个 PLFM_RADAR 的地基。我把它设计成一个常驻进程,每 30 秒执行一次采集任务。采集分两个维度:服务维度和主机维度。
服务维度最关键的是延迟和错误率。我用 requests 库请求每个服务的健康检查接口,记录从发出请求到收到响应的耗时,同时把 HTTP 状态码记录下来。这里有一个很多新手容易忽略的点:健康检查接口要区分“探测成功”和“业务正常”。比如一个接口返回 200 但业务逻辑已经异常,这时候 200 没有任何意义。所以我在被探测服务侧额外暴露了一个/health接口,内部会去检查数据库连接池、缓存连接等关键依赖,依赖异常时返回 503。这样雷达拿到的状态才是真实的服务健康度。
主机维度的采集相对简单,直接用 psutil 拿 CPU 使用率、内存占用率、磁盘剩余空间、网络收发速率。这里要特别提一下磁盘:很多监控系统把磁盘空间当成普通指标来展示,但我建议把根分区使用率的告警阈值设在 80%,因为日志文件、镜像、临时文件都会在一个晚上把磁盘塞满,一旦满了服务写不了日志直接就僵死,连监控本身的日志也会丢。
下面是一段简化后的探针代码,可以感受一下整体结构:
import time import requests import psutil SERVICES = [ {"name": "api-gateway", "url": "https://api.example.com/health", "timeout": 3}, {"name": "worker", "url": "https://worker.example.com/health", "timeout": 5}, ] def probe_services(): results = [] for svc in SERVICES: start = time.time() try: resp = requests.get(svc["url"], timeout=svc["timeout"]) status = resp.status_code except Exception: status = 0 elapsed_ms = (time.time() - start) * 1000 results.append({ "service": svc["name"], "status": status, "latency_ms": round(elapsed_ms, 2), "ts": int(time.time()), }) return results def probe_host(): return { "cpu_percent": psutil.cpu_percent(interval=1), "mem_percent": psutil.virtual_memory().percent, "disk_percent": psutil.disk_usage("/").percent, } def main(): services_data = probe_services() host_data = probe_host() write_to_influxdb(services_data + [host_data])这套探针我在三台不同供应商的云服务器上跑过,稳定性没有问题。唯一要注意的是psutil.cpu_percent(interval=1)这个调用会有 1 秒的阻塞,如果你把采集间隔压到 10 秒以下,探针自身消耗会变大,所以采集间隔别低于 15 秒。
2.2 指标计算:为什么不能直接拿“原始值”做告警
探针写进数据库的是每分钟的多条原始样本,但告警规则要用的往往是聚合后的指标。这中间有两个关键计算:滑动窗口平均值和同比环比变化率。
滑动窗口平均值的逻辑很简单:取最近 5 分钟或最近 15 分钟的延迟数据,算一个平均值或者 P95 分位数。为什么不用单次探测值直接做告警?因为单值抖动太厉害了。我自己就遇到过:某个服务本来很健康,一次 GC 暂停让请求延迟从 50ms 跳到 2 秒,如果用单值做阈值,一晚上能收到二三十条误报;但用 5 分钟窗口平均值,那次抖动会被平均掉,只有持续恶化才会触发告警。所以 PLFM_RADAR 所有告警规则的输入都是聚合值,而不是原始值。
同比环比变化率要稍微复杂一点。比如某个定时任务每天凌晨固定跑一个小时,这期间 CPU 飙升是正常现象;又比如某个电商页面每天早上 10 点迎来流量高峰,错误率略涨也在预期内。这种情况下固定阈值完全失效。我的做法是把当前窗口的平均值和前 7 天同一时段的平均值做对比,算出一个变化倍率,超过正常波动范围才告警。
def calc_ratio(current_avg, baseline_avg): if baseline_avg <= 0: return None return current_avg / baseline_avg说实话这个逻辑一开始我做得比较简单,只是硬编码了一个“前移 24 小时”的窗口。后来发现节假日和高峰时段会误报,才改成“前 7 天同时段”,数据量够大之后准确率提升非常明显。
2.3 告警分级与通知策略的实战配置
告警最怕的是“狼来了”。如果规则不准,每天推一堆无关紧要的消息,大家很快就麻木了,真正出事的时候反而没人当回事。所以我设计了一个三级告警体系。
P0 级别:服务完全不可用,或者错误率持续超过 10%。这个级别的告警会在 1 分钟内重复推送,直到人工确认才停止。
P1 级别:延迟超过阈值、资源接近瓶颈。推送一次,如果 15 分钟内没有恢复,再追推一次。
P2 级别:趋势性预警,比如磁盘使用率超过 75%、错误率缓慢上升。合并成每日摘要,早上推送。
告警规则我直接写在 Python 字典里,而不是搞一套 DSL。很多人觉得规则配置得做成页面,但实际我用了很长时间发现,改配置走代码发布流程反而更严谨,至少每次改动都有记录,不会出现“谁在页面上偷偷改了阈值”这种事。
ALERT_RULES = [ { "name": "api_gateway_down", "metric": "service_status", "condition": "==", "value": 0, "window": "5m", "severity": "P0", "target": "api-gateway", }, { "name": "api_gateway_latency_p95", "metric": "latency_p95", "condition": ">", "value": 800, "window": "5m", "severity": "P1", "target": "api-gateway", }, ]3. 实操过程:从零到一搭建 PLFM_RADAR 完整流程
3.1 环境准备与模块划分
我实际的部署环境是两台 2C4G 的云服务器,一台放被探测的业务服务,一台跑 PLFM_RADAR 本身。系统都是 Ubuntu 22.04。如果你设备资源紧张,甚至可以把雷达部署在同一台机器上,只是要提防一个坑:雷达自身占用资源过高影响被监控服务,那你看到的数据就不真实了。
整个项目可以拆成四个子模块:
probe/:独立运行的采集探针,写好之后直接 nohup 启动。storage/:InfluxDB 的初始化脚本,负责建库建表和数据保留策略。engine/:规则引擎,定期查询 InfluxDB 并执行告警判断。dashboard/:Flask 应用,提供 HTTP 接口把数据渲染到前端页面。
模块之间通过数据库和 HTTP 接口解耦,互不依赖。这也方便后续扩展,比如你想新增一台业务服务器,只需在 probe 的配置里加一条服务地址,完全不用碰其他模块。
3.2 第一次部署冒烟测试的完整过程
第一次跑通整套流程,我大概花了半小时,这里把步骤原样贴出来。
第一步,安装基础依赖:
apt update && apt install -y python3 python3-pip pip3 install influxdb requests psutil flask第二步,初始化 InfluxDB 数据库。我用的是 InfluxDB 1.8,因为它可以直接通过 HTTP API 打数据,不需要额外的写入协议字段。
influx -execute "CREATE DATABASE plfm_radar" influx -execute "CREATE RETENTION POLICY \"rp_30d\" ON \"plfm_radar\" DURATION 30d REPLICATION 1 DEFAULT"数据保留 30 天足够了。由于告警和看板都只要最近的数据,超过 30 天的旧数据不仅没用,还在消耗磁盘,这条策略建议无论如何都要加上。
第三步,编写探针配置。先在一个独立的目录里把services.json写好,把你要监控的所有服务地址列进去。我当时的清单大概是:API 网关、用户服务、订单服务、定时任务 Worker、MySQL 实例、Redis 实例。
第四步,启动规则引擎。注意一定要让探针先跑几分钟,攒下至少一个窗口期的数据再启动引擎,否则引擎查不到数据会一直空转。
第五步,打开看板。Flask 默认跑在 5000 端口,访问首页就能看到雷达图。第一次看到所有维度都亮起来的时候,还是挺有成就感的。
3.3 看板页面与雷达图的核心实现
ECharts 雷达图适合展示“多维指标是否均衡”。我把每个服务定义成一个雷达图上的多边形,维度包括:可用性、延迟健康度、错误率健康度、资源健康度。每个维度先把它原始值映射到 0~100 分,分数越高越健康。
映射逻辑要解释一下:延迟健康度的映射不是线性的,而是“在 100ms 以内满分,500ms 以上零分,中间线性递减”。为什么这么设计?因为用户对延迟的敏感度不是线性的,从 10ms 涨到 100ms 用户几乎没感觉,但从 400ms 涨到 800ms 已经非常卡顿了。所以我把映射曲线做得“低区平坦、高区陡峭”,能更早反映体验恶化。
前端核心代码大概长这样:
var chart = echarts.init(document.getElementById('radar')); chart.setOption({ series: [{ type: 'radar', data: [{ value: [score.availability, score.latency, score.error, score.resource], name: serviceName }] }] });说实话前端这块在我的项目里是最“轻”的部分,因为数据已经在后端算成了打分,前端只负责渲染,连状态管理都不需要。如果你不想纠结前端,直接用 ECharts 官方实例改改就行,十分钟就能出一个能看的版本。
3.4 通知渠道接入钉钉机器人的配置示例
钉钉自定义机器人的接入非常直接。先在群里添加一个自定义机器人,拿到一个 Webhook 地址,然后向这个地址 POST 一段 JSON。
import requests def send_alert(rule_name, message): webhook = "https://oapi.dingtalk.com/robot/send?access_token=xxxx" body = { "msgtype": "markdown", "markdown": { "title": "PLFM_RADAR 告警", "text": "### " + rule_name + "\n\n" + message, }, } requests.post(webhook, json=body, timeout=5)这里有两个细节:一是机器人安全设置里最好启用“加签”,把密钥拼进 URL,防止 Webhook 地址泄露后被恶意刷消息;二是通知内容里一定要带上当前时间、目标服务名、实际值、阈值四个信息,不然你收到消息还得再登服务器查一遍数据,多耽误事。
4. 常见问题与排查技巧实录
4.1 时间窗口错位导致的误报
这个坑是我上线第一周就遇到的。探针每 30 秒写一条数据,规则引擎每 5 分钟执行一次窗口聚合。因为数据写入到 InfluxDB 有网络延迟和时钟偏差,窗口切分的瞬间会出现“上一分钟的数据还没写进来”的问题,导致窗口内数据点稀疏,平均值被低估或直接没有数据。
排查思路并不难:把告警日志和 InfluxDB 里的实际数据放在一起对比,就会发现告警触发的那一刻窗口数据其实是空的。解决办法是我在规则引擎里加了一个“延迟 60 秒再聚合”的策略。也就是查询窗口的时候,只查当前时刻往前推 6 分钟到 1 分钟之间的数据,给写入留出缓冲。这个修正解决了我大量的误报问题。
4.2 高基数指标导致查询变慢
PLFM_RADAR 初期为了图方便,把服务名作为 tag 写入 InfluxDB,然后在每一条测量记录里都带上service标签。随着服务增多,tag 组合膨胀得很快,查询性能肉眼可见地下降,看板打开一次要好几秒。
后来我做了两个优化:第一,把不需要聚合的标签(比如 Master 节点名)从 tag 挪到 field,减少索引量;第二,合理设置保留策略,旧数据压缩周期调短。优化之后查询基本回到毫秒级。你的系统如果规模不大,这个问题可能不会触发,但一旦开始卡,优先查 tag 的基数准没错。
4.3 告警风暴:一个根因引发连环告警
最吓人的一次是整个平台挂了,PLFM_RADAR 在五分钟内推了 40 条告警。原因是一条数据库连接线程池耗尽,导致所有的 API 服务都延迟超标、错误率飙升,每个服务各触发一条 P1。那阵子我真的被手机震到心慌。
后来我加了依赖抑制规则:如果数据库节点已经触发 P0 告警,那么在数据库恢复前,上游服务的 P1 告警自动进入“静默”状态,改成记录日志不推送。实现逻辑也不复杂,就是一个优先级队列,引擎在推送前先检查是否有更严重级别的告警处于未恢复状态,有就先打断。这个机制上线后,告警量降了 80%,真正出事的时候反而更容易聚焦根因。
4.4 探针进程静默退出
有段时间雷达面板上的数据突然变稀了,链路健康度迟迟不更新。我查了半天,发现是探针进程因为未捕获异常退出——具体是某个被探测的服务返回了非法 JSON,我的探针在解析时抛了 ValueError。这个问题很愚蠢但是很典型:监控系统自身没有守护机制,反而成了最先失明的那双眼睛。
解决方案有两条,我都做了。一是在探针代码的所有外部调用外层加 try except,保证单点失败不会拖垮整个采集进程;二是用 systemd 托管探针进程,并设置Restart=always,确保挂掉后能自动拉起。
[Unit] Description=PLFM RADAR Probe [Service] ExecStart=/usr/bin/python3 /opt/plfm_radar/probe/main.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.target这条建议真的别省,监控系统本身的高可用和故障恢复,和业务系统同等重要。
4.5 指标口径不统一越到后面越头疼
团队里不同人写的服务,健康检查接口返回的字段名不一样,有的叫status,有的叫success,甚至有一个服务把健康状态放在响应头里,我用 requests 接的时候差点没骂人。前期我都是单独适配,代码里全是 if 分支,丑到没法维护。
后来我统一了一个约定:所有被 PLFM_RADAR 纳管的服务,健康检查接口一律返回 JSON,格式固定为{"status": "ok", "db": true, "cache": true}。新服务上线前,必须对照这个格式调整。这是整个项目里最有价值的一条“行政命令”,它让探针代码简洁了非常多。以后你要做类似系统,建议第一条就立这个规矩。
5. 后续演进:让 PLFM_RADAR 更智能、更开放
5.1 从监控到预测:接入趋势外推
当前系统的逻辑是“事后告警”,数据异常了才通知你。下一步我想加一个简单的趋势预测模块:基于过去 7 天同一时段的指标曲线,用线性回归或者简单移动平均预测未来 30 分钟的值,如果预测值会超过阈值,提前发一个 P2 级别的预警。
这个功能听起来高大上,其实实现起来很轻。监控数据本身是强周期性的,工作日和周末、白天和凌晨的曲线形状大不相同,只要按“礼拜几 + 时段”分组,预测准确率就不会太差。我不建议一开始就上 LSTM 之类的深度学习模型,数据量不够,训练成本高,还不如一个带季节性的统计学模型实在。
5.2 从平台到生态:把日志与告警联动起来
告警只能告诉你“服务延迟高了”,但延迟高到底是 SQL 慢查询、GC 停顿,还是流量超限?下一次迭代我打算在告警消息里附上关键日志片段,具体做法是把服务日志接入 Loki,告警触发时根据时间窗口去 Loki 拉取对应的 WARN 和 ERROR 级别日志,拼进通知正文。这一步做完,告警就不再只是一句“狼来了”,而是顺手告诉你狼从哪个方向来的。
5.3 给想要落地这套系统的朋友几个建议
第一,先跑通最小闭环再优化细节。我见过太多人第一版就想做到分布式、高可用、可视化酷炫,结果迟迟上不了线。你的第一步目标应该只是“某个服务挂了,我能收到通知”,哪怕页面丑得像命令行输出都行。
第二,保持指标体系稳定比追求丰富重要。指标不是越多越好,每多一个指标,就多一份采集和计算的维护成本。先把“延迟、错误率、资源使用率”这几个核心指标守住,再图扩张。
第三,告警规则要定期复盘。每周花 20 分钟看一遍这周触发的告警,把那些“事后看完全没必要”的规则改掉或者删掉。监控系统的价值不在于告警条数多,而在于每一条都值得被点开。
第四,尽量别用单点部署。PLFM_RADAR 本身就是用来守护服务的,结果它自己挂了,整个监控体系就瞎了。我的实践是用 systemd 拉起探针和规则引擎,同时把 InfluxDB 定期备份到对象存储,一旦服务器硬件故障,能在半小时内恢复到新机器上。
说实话,PLFM_RADAR 并不是一个技术难度多高的项目。它真正的价值在于把“监控、告警、可视化”这个闭环用最简单可靠的方式串了起来。我从开发、部署到上线迭代,前后也就花了一个周末的时间,但之后一年多的时间里,它至少帮我提前发现了十几次可能酿成事故的隐患——建连慢、磁盘吃紧、内存泄漏,每一个都是这种轻量监控系统能轻松抓住的东西。
我个人体会最深的一点是:监控系统要设计得足够“笨”。不要依赖任何外部复杂组件,不要搞花哨的分布式链路,一个探针、一张表、一组规则,就足够解决大部分服务守护问题。业务在增长,系统在演进,等哪天真需要更重的监控体系时,PLFM_RADAR 留下的统一健康检查口径和告警分级模型,也会是一个很不错的起点。