自研监控告警系统PLFM_RADAR:设计、落地与踩坑调优
凌晨两点十七分,手机在床头柜上震得嗡嗡响。我摸过来一看,是业务方在群里@我:下单接口超时率飙到百分之八十,用户已经开始骂了。我一边往书房跑一边打开监控页,看到的却是满屏绿色——系统指标一切正常,没有任何告警。那一瞬间的感觉,比被骂还难受。
这就是 PLFM_RADAR 这个项目最初的由来。PLFM 是我们对自研平台底座的习惯叫法,Platform 的简写;RADAR 是跑在它上面的那套探针与告警系统。它做的事情说白了就一件:把主机、接口、队列、数据库这些散落的运行数据统一采集起来,用规则引擎实时评估,异常发生的第一时间推给你,而不是等你被用户投诉叫醒。
这篇复盘想把 PLFM_RADAR 从设计到落地、再到踩坑调优的过程完整讲一遍。适合谁看?维护自研系统但不想引入重型监控全家桶的团队,被告警误报烦到想砸电脑的值班同学,以及正准备从零搭一套轻量可用的监控预警体系的个人开发者。东西不算复杂,但里面的取舍和教训,每一条都是真金白银换来的。
1. 一套自研RADAR的由来:现成方案为什么不够用
刚开始我们团队也不是没想过直接用开源监控方案。试过主流的时间序列监控组合,装起来倒是顺利,Grafana大屏也漂亮,可真用起来就发现不对劲了。节点多、配置庞杂不说,告警规则写起来绕,更难受的是它把“监控”和“我们自己的业务语义”完全割裂开了——我只想知道“下单接口的P99为什么涨了”,而工具只会告诉我“某台机器CPU高”,这两个信息之间隔着一整条排查链路。
1.1 现成方案的四道坎
第一道坎是粒度。通用监控方案默认关注的是机器级别,CPU、内存、磁盘这些。可线上出问题往往是业务链路级别的,比如某个网关节点到某个微服务之间的连接池耗尽,机器指标看起来几乎没变化,但业务已经半死不活了。第二道坎是定制告警逻辑的成本。我们要的规则不是简单的“大于阈值就报警”,而是要考虑滑动窗口、同比基线、连续确认次数。在通用方案里写这种逻辑,要么依赖它自带的DSL,要么自己写插件,学习和维护成本都不小。
第三道坎是数据出口。监控数据落到了别人的存储里,想和我们的发布系统打通、想在告警里附带最近的操作记录,就得额外写同步脚本。第四道坎更现实——资源占用。一整套监控全家桶跑下来,光 agent 和相关组件的内存就要占掉好几个G,对我们这种资源不算宽裕的自建环境来说,太奢侈了。
1.2 决定自研的边界和预期
所以后来我们想明白了一件事:我们需要的不是一个监控平台,而是一根“平台雷达”——要能探到异常、分清严重级别、把人叫醒、把该带的上下文带齐。刚好我们团队的技术栈全家桶是 Python,对异步处理和轻量存储都很熟,于是决定自己攒一个。PLFM 作为底座,提供任务调度、配置下发和统一鉴权;RADAR 专注做采集、判定、通知三件事。
这个决定最关键的边界是:我们不打算做完整可观测性平台,那是个大坑。RADAR 的定位是“告警雷达”,不是“数据仓库”。它只需要保留最近一段时间的原始数据用于基线判断,更详细的历史分析还是交给日志系统和未来的时序存储。想清楚这条边界,整个项目的复杂度一下子降了一大截。
2. 整体架构:PLFM底座加RADAR探针,数据从哪来、到哪里去
PLFM_RADAR 的整体结构分四层,我画不出来漂亮的架构图,用文字描述大概是这样:最下层是采集层,由一堆探针组成;中间一层是 RADAR Core,做数据标准化、窗口计算和规则判定;再往上是通知出口和存储;最顶层是配置和展示入口,直接嵌在 PLFM 的管理后台里。
2.1 探针分组与采集对象
探针分四组。主机探针负责 CPU、内存、磁盘IO和网络包量,用 psutil 库实现;应用探针打进业务服务里,采集 QPS、错误率、P99延迟和活跃连接数;依赖探针关注 Redis 命中率、消息队列积压数和数据库慢查询数;网关探针则专门盯入口流量和状态码分布。每组探针的采集周期不一样,主机30秒,应用10秒,依赖和网关60秒。
为什么采集周期要区分?这是被实际教训逼出来的。应用探针周期拉太长的话,接口抖动两分钟就过去了,你根本采不到峰值;但主机指标变化相对平缓,采集太密集只会徒增IO开销。10秒这个值比较适合捕捉大多数线上抖动,又不至于让存储爆炸。
2.2 RADAR Core 的职责分工
RADAR Core 是项目的核心进程,部署在独立的服务节点上,它内部拆成五个模块:接收器只做一件事——收探针上报的数据,统一解析成标准格式;环形缓冲负责保存最近 N 个周期的点位,N 默认是 30;规则引擎每隔一个判定周期扫描一次缓冲,跑我们预设的规则集;状态管理器记录每条告警的当前状态(触发中、已确认、已恢复、静默中);通知器负责把告警内容渲染成消息并推送到各个渠道。
数据流是单向的:探针上报到接收器,接收器写进环形缓冲,规则引擎读缓冲算指标,命中规则后把告警对象交给状态管理器去重判断,最后由通知器发出去。这里没有复杂的消息队列,因为采集量不大,单机完全扛得住。我当时特意压过测试,RADAR Core 在每秒处理 2000 个点位时,CPU占用才 6% 左右,完全不用担心性能。
2.3 存储设计:轻量但别省掉历史
存储层我们用 SQLite 做了个简单设计。考虑到规则引擎需要拿“最近30个点位”和“昨天的同一时段”做对比,所以必须持久化一部分数据。SQLite 在 WAL 模式下,读写并发比想象中好不少。每到整点,RADAR Core 会把当前小时的高频指标聚合降采样成分钟级数据存到另一张表,超过7天的旧数据自动清理。
这套极简方案的代价是历史分析能力弱,但换来的是零额外依赖、部署简单。对于告警系统这个场景,我个人的观点是:别为了“以后可能要做分析”而一开始就把存储搞得大而全,真有需要的时候再引时序库也不迟。
3. 探针指标设计:采什么、怎么采、为什么这样采
指标设计是整个 RADAR 的灵魂。采少了,异常根本看不见;采多了,存储和带宽先扛不住。我们最终定下来一套“三层指标模型”,每一层对应一种故障形态。
3.1 三层指标模型
系统层指标管物理资源,包括 CPU 使用率、系统负载、内存占用、磁盘IO等待时间和网络重传率。这一层的核心用途是兜底,当业务层看不出问题时,回头查系统层往往能发现是某台机器正在被拖垮。应用层指标管业务健康,包括 QPS、5xx错误率、P99延迟、活跃连接数和超时次数。依赖层指标管外部组件,Redis 命中率、MQ积压数量、数据库连接池使用率和慢查询数量都在这里。
层与层之间不是孤立的。RADAR 里每一条告警都自带层级标签,当多个层同时告警时,状态管理器会尝试按“依赖层→系统层→应用层”的顺序做关联,把最底层的异常作为根因候选。这个逻辑写得比较朴素,但确实让值班同学少了不少排查弯路。
3.2 指标计算的两个隐藏坑
先说 P99 延迟怎么算。刚开始我图省事,直接在环形缓冲里存每个周期的平均延迟,然后拿平均值当 P99 用。结果就是大量告警报了又恢复、恢复了又报,因为平均值对长尾根本不敏感。后来改成分段直方图,在探针端按延迟区间递增计数,RADAR Core 端按分位数还原。内存开销增加不大,但告警的准确率提升是肉眼可见的。
再一个是错误率的分母问题。错误率 = 错误数 / 总请求数,这个式子本身没错,但要注意分母的窗口。如果窗口内有流量突降,总请求数变小,错误率会被动冲高,造成误报。后来我们把规则改成:错误数和总请求数需要同时超过下限阈值才允许判定错误率异常。比如 QPS 低于 50 时,错误率指标直接跳过,避免波动噪音。
3.3 探针自身的保护机制
探针也是程序,也会出问题。一个非常重要的设计是“本地熔断”:每个探针上报的数据若连续失败超过5次,探针自己进入降级模式,只保留采样但不再网络传输,避免因网络抖动导致数据堆积和内存泄漏。同时 RADAR Core 对“探针失联”这件事本身有一套告警,探针心跳超过 90 秒没有上报,会触发 P1 告警——因为监控自身失效,往往比业务故障更可怕。
4. 告警规则引擎:滑动窗口、基线判定与误报抑制
规则引擎是 PLFM_RADAR 里最容易写烂的部分。最初版本我把所有规则都做成“固定阈值”,比如 CPU 超过90%就告警。上线第二周就来了个下马威:一台编译机在跑大型构建时 CPU 长时间满负荷,业务完全正常,告警却响了一夜。从那天起我就明白,固定阈值这套思路根本不适合动态环境,必须引入窗口和基线。
4.1 滑动窗口与Z-Score
我们用滑动窗口配合 Z-Score 来做异常判定。每个指标维护两个窗口:短窗口保存最近 5 分钟数据,长窗口保存最近 30 分钟数据。规则引擎每分钟计算一次短窗口当前值与长窗口历史均值的偏离程度,偏离程度用标准化得分表示:
z = (current_mean - long_window_mean) / long_window_std当 z 的绝对值大于 3 且持续 3 个判定周期,才判定为异常触发告警。为什么用 Z-Score 而不是简单百分比?因为不同指标的自然波动范围完全不一样——CPU 本身波动很大,接口时延却应该相对稳定。Z-Score 等于把每条指标都按照它自己的“历史性格”归一化了,动态基线一出来,编译机满 CPU 再也不告警,而接口时延稍微抖一下马上就会被抓住。
4.2 连续确认与状态机
判定逻辑不是算出来就发告警,而是要经过一个状态机。每个指标实例有一个状态:NORMAL、PENDING、ALERTING、ACKED、RECOVERED。当 z 持续越界时,状态从 NORMAL 变到 PENDING,记录第一次越界的时间;再连续两个周期越界,PENDING 变 ALERTING,触发通知;一旦 z 回落,状态进入 RECOVERED 并推送恢复消息。这个机制我们内部叫“三振出局”,有效过滤了单点毛刺。
状态机最重要的副产品是“告警去重”。同一个指标在 ALERTING 状态下,即使后续周期仍然越界,也不会重复发通知。系统会每 10 分钟发送一条“仍在持续”的汇总消息,直到恢复或被人为确认。这一条规则直接终结了告警轰炸的乱象,值班同学的手机终于清净了。
4.3 静默窗口与学习模式
还有两种特殊模式值得说一下。静默窗口是指在配置的时段内抑制指定指标的告警,比如每周固定的发布窗口、业务低峰期的大促预演。学习模式则是在规则刚刚上线时使用,RADAR 只记录判定结果但不真正发通知,方便我们用一周时间验证规则的合理性和准确率。这个模式对初调基线特别有用,强烈推荐任何一个做告警系统的人都加上。
5. 告警通道与值班闭环:让每条告警都能被认领
告警发出去只是开始,真正难的是让每条告警都能被认领、处理和闭环。PLFM_RADAR 在通道这块下了不少功夫,因为单纯“发个消息到群里”这件事根本不算告警系统。
5.1 分级通知策略
告警分成三级。P0 指核心业务不可用或资损风险,必须立刻响应,通知渠道是电话加群内@所有人,同时触发企业微信群机器人;P1 指重要功能受损或可用性明显下降,发邮件加群内@责任人;P2 指一般性异常或资源水位超限,只在看板上标记并在值班群累计报送。分级是倒逼出来的,不然所有告警都按最高优先级发,所有人很快就会对通知脱敏。
通知消息的模板也设计得比较讲究。内容包括:告警指标名、当前值、基线值、首次触发时间、持续时长、涉及的服务与实例列表,以及一个直达 PLFM 控制台的链接。带上这些上下文,值班同学不需要先打开监控页面自己去查,很多 P2 告警看到消息就能直接判断要不要处理。
5.2 ACK确认与升级机制
告警发出后,如果 15 分钟内没有人确认,状态管理器会把告警升级一级,P2 变 P1,P1 变 P0。确认是通过控制台按钮或回消息指令完成的,确认时必须填写处置备注。这些备注会追加到告警记录里,后续复盘时一目了然。我们内部对确认还有一种约束:确认不代表解决,只是“我看到了”。真正关闭告警的唯一条件是状态回落到正常区间。
这里有一个设计细节挺关键:告警对象在 ALERTING 状态下,每隔 30 分钟会重发一次摘要到值班群,但不会再触发一次 P0 电话。因为电话轰炸会破坏人的判断力——我见过凌晨值班同事因为被连续电话轰炸,误判操作把缓存全清掉的真实事故。
5.3 恢复通知与告警关闭
z 值回落后,状态机进入 RECOVERED,系统发一条恢复消息并自动关闭告警。如果同一个指标在恢复后 10 分钟内再次触发告警,状态管理器的连续告警计数会标记为“复发”,这种告警会被自动附加一个强烈的视觉标记,并且在通知消息里直接显示“昨日至今已复发 N 次”。这个设计来自一次惨痛教训:有个问题修了一次但没根治,每周都要复发两三次,值班同学已经麻木了。有了复发标记,负责人被强制关注,问题才被真正根除。
6. 上线半年的真实踩坑记录:六条最值得说的教训
规则引擎和通道都上线后,真正折磨人的才刚开始。我把半年里踩过的坑挑了六条,按“症状→根因→解决”的方式写出来,每一条都对应着一次半夜被叫醒的经历。
6.1 时钟偏移引发的幽灵告警
有一个节点上报的时间戳总比别的节点晚十几秒。在滑动窗口计算时,晚到的数据被算进错误的窗口,导致 z 值剧烈跳动,触发了一堆根本不存在的告警。排查了很久才发现是那台机器的系统时钟偏移。解决方式是双管齐下:采集端全部使用本机单调时钟加上报序号,时间戳统一由 RADAR Core 在接收时加盖;同时在运维侧强制部署时间同步服务,把这台机器从“偏执名单”里清出去。
6.2 GC暂停让探针自己假死
应用探针是跟着业务服务跑的,业务服务发生长时间 GC 暂停时,探针也会被冻住。症状就是:业务 GC 卡顿确实发生了,但探针没来得及上报,RADAR 反而报了一条“探针失联”的告警,把真正的问题给掩盖了。后来在探针里做了两点改造:独立线程池跑采集任务,与主业务线程解耦;采集线程单次执行超过 500ms 就强制放弃并记录丢弃次数。这样探针失联告警还要加一条辅助条件——只有丢弃次数同时超标时才触发。
6.3 流量突增时的误报放大器
618 大促演练那阵子,我们把一个新的下游服务接进了 RADAR。它的延迟基线学了两天,结果大促流量一来,P99 飙升到平时的五倍,整个告警群瞬间刷屏。但这次根本不是故障,是流量模型变了。解决方式是给关键服务配置一个“学习窗口冻结开关”:在做流量预期变化的操作前,手动把基线冻结在稳定时段的数值上,避免异常流量污染基线。本质上是给动态基线加了个人工保险。
6.4 SQLite 写锁导致的数据丢点
最初 SQLite 用默认的 journal 模式,多个探针同时高频写入时,偶尔会出现database is locked,然后一批点位直接丢弃。丢点最坑的地方不是少数据,而是让滑动窗口出现空洞,导致标准差计算偏差,误报又来了。解决:切换为 WAL 模式,写入改成批量追加,接收器每秒钟攒一批再入库。实测写入吞吐从每秒几百跳到几千,再没出现锁错误。
6.5 P99 统计口径不一致引发的扯皮
这事技术上不难,但在协作上很痛。业务部门反馈 P99 和我们对不上,同一时间同一接口,两边看到的数字差得离谱。排查发现我们统计的是网关出口的延迟分布,业务方统计的是应用内部处理时间。双方都觉得自己没错,但口径完全不是一回事。解决方式是给每个指标加“口径字典”,字段里明确标注统计阶段和计算方式,并且把口径说明直接渲染到告警消息里。看似是技术问题,本质是团队协作的共识问题。
6.6 发布引发的告警雪崩
有一次深夜灰度发布,发布脚本配置错误,新版本服务不断启动又退出,整个集群的活跃连接数像过山车一样抖动。RADAR 的规则引擎按指标发告警,十几台机器几十个指标瞬间全触发,把告警通道打崩了。后来在状态管理器里加了“全局熔断”和“按服务聚合”两道保险。全局熔断:同一服务 5 分钟内超过 20 条独立告警时,只发一条“服务异常风暴”汇总告警并进入风暴模式;按服务聚合:同一服务的多条告警合并成一条,附带明细清单。雪崩场景从此再没发生。
7. 下一步演进:从规则到智能,从单机到分布式
PLFM_RADAR 跑到第六个月的时候,我们已经离不开它了。但我也清楚它的边界在哪——规则引擎本质上是用已知的问题模式套未知的运行状态。这套逻辑胜在稳定和可解释,输在发现不了那些没见过的新型异常。下一步的演进我大致有三个方向,写出来算是给自己的行动清单,也为同样在做类似系统的同学提供一个参照。
7.1 轻量时序异常检测替代部分规则
我不打算一步到位上复杂的深度学习,而是想用孤立森林和高斯分布建模先替换掉一部分规则。计划是对每一条核心指标训练一个离线模型,在线推断时给出异常概率,超过阈值的再交给规则引擎去确认和发告警。这套“模型初筛+规则确认”的组合,既能发现未知异常模式,又能保住规则引擎的可解释性。成本也可控,因为只对几十条核心指标做建模,不是全量数据。
7.2 联邦式部署与多机房探针
目前 RADAR Core 是单节点的,如果以后真要跨机房部署,单点问题会变得很突出。计划是把 Core 改成可联邦的模式:每个机房本地跑一套 RADAR Core,只负责本机房探针,通过上层轻量级汇聚节点把跨机房的告警合并。探针配置从汇聚节点统一下发,但告警判断全部在本地完成,避免跨机房网络抖动影响判定。
7.3 告警关联与根因定位的雏形
前面说过状态管理器已经有粗粒度的层级关联。想把它做成真正的关联推断:当一个应用层指标告警时,自动拉取同时段的依赖层和系统层数据,计算时间顺序和相关程度,生成一条“疑似根因链”附在告警消息里。这个功能不追求完全自动化,只要能把值班同学的排查范围缩小 70%,它的价值就已经远超投入了。
做 PLFM_RADAR 这大半年,我最大的体会是:监控告警系统真正难的不是技术,而是对抗噪声、对抗麻木、对抗“狼来了”的心理效应。每一条告警都必须有明确的处置动作和确认机制,否则它只是噪音。把一个看起来平平无奇的探针系统迭代到让团队“离不开”,靠的不是炫技,而是把每一个误报、每一次半夜唤醒都当成一次系统升级的契机。如果这套思路和数据流设计对你有哪怕一点启发,那这个复盘就值了。