凌晨一点四十七分,手机在床头柜上震了两轮。第一轮是告警通知:支付网关P99从80ms飙到2.3s。第二轮是同事在群里@我:结算服务超时率已经到15%。我打开公司的监控大屏,屏幕上十几个服务的曲线纠缠在一起,整整看了五分钟,愣是说不清"到底哪个才是源头"。这种"告警来了但不知道先看哪里"的状态持续了大半年,直到我花三周时间写了套内部工具,名字就叫PLFM_RADAR。PLFM是我们研发平台(Platform)的代号,RADAR没玩缩写梗,它就是字面意思——雷达。这套系统把平台上所有微服务、宿主节点、中间件实例和它们之间的依赖关系,持续扫描、捕捉异常、定位源头。这篇文章把整套系统的设计思路、关键实现和踩过的坑完整拆一遍,适合平台负责人、SRE、后端工程师参考,尤其适合那些觉得"市面监控方案总是差口气"的人。
1. 为什么叫"雷达"而不叫"监控":从告警轰炸到源头定位
1.1 一个连锁故障让我彻底受够了老方案
去年夏天有个特别典型的场景。搜索服务的一个节点因为磁盘IO异常开始变慢,连带调用它的推荐服务P99上升,推荐服务的熔断器触发后开始快速失败,又导致依赖推荐服务的运营后台大量报错。传统监控平台在一个服务维度上会产生七条告警:磁盘IO告警、搜索P99告警、推荐服务P99告警、熔断器开启告警、运营后台错误率告警……七条消息轮流轰炸值班群,等人来人工串联因果时,已经过去四分钟。四分钟看起来不长,但对于一个日订单量过百万的平台,这就是上万次失败请求的窗口。
PLFM_RADAR的设计逻辑完全不同。它把每个服务、节点、中间件当成雷达屏幕上的一个目标,每30秒全量扫描一次,一旦发现某个目标偏离健康基线,立刻检查它的依赖链路上游是否也有异常。如果确认上游先坏,就把下游的所有告警合并成一条"源头定位"消息推出去。这就是"雷达"和"监控"的本质区别:监控回答"什么坏了",雷达回答"哪里先坏"。
1.2 四个"不做",让项目活了下来
我做这个项目的过程里,最大的教训其实是克制。很多内部工具死就死在功能铺太开、维护成本失控。所以PLFM_RADAR从第一天就画了四条红线:
- 不做完整的APM链路追踪,不采集每个请求的Span。服务之间调用的依赖关系、调用量、延迟分位、错误率这些聚合数据就够了,单次请求内部经过哪里是全链路可观测平台的职责。
- 不做日志采集与检索。日志归日志平台管,雷达只在告警产生时把对应的日志查询条件拼好,附在告警消息里,值班人一键跳转。
- 不打算重新发明告警通知渠道。飞书、短信、电话这些触达方式复用公司现有的告警网关,雷达只负责生成"带上下文的事件",不负责送达。
- 不做花哨的可视化。雷达的展示端只有一个大屏加一份日报摘要,核心诉求是"让值班人在一屏之内看懂现状",不是做数据可视化比赛。
1.3 三类使用者的不同打开方式
这套系统的使用者大致分三类。平台运维和值班工程师每天早上的第一件事是看雷达的"晨检摘要",那里汇总了夜间发生但未自动恢复的事件;研发团队负责人关心的是自己名下服务一周内的"健康评分"变化,雷达每周生成一份趋势报告发给各团队;架构组做容量规划时,需要跨服务、跨时段的整体指标对比,雷达保留180天的聚合数据,可以随时拉出任意服务过去半年的P99曲线。这三类需求听起来差异很大,但都建立在同一份数据资产之上,这也是我坚持把底层数据模型做扎实的原因。
2. 整体架构:采集、存储、检测、触达四层如何咬合
2.1 四层模型是怎么推出来的
PLFM_RADAR的架构没有用什么高深理念,就是从数据生命周期推出来的四层:采集层负责把分散在各处的状态变成统一格式的样本;存储层解决"样本怎么低成本地放、怎么快速地取";检测层解决"从数据到结论";触达层解决"结论怎么让该知道的人知道"。
这里面最核心的一条原则是:检测引擎绝不能直接调用采集端的API,所有检测逻辑必须跑在存储层的数据快照之上。这么定有两个实际原因。一是解耦,采集端形态变了不影响检测逻辑,比如后来我们把主动探测的HTTP客户端从单线程改成线程池,检测层一行代码都没动。二是可回溯,每次检测告警产生时,记录当时的完整数据快照范围,怀疑误报了可以随时用同样的逻辑重跑一遍历史数据。没有这条原则,后面第4章的误报率治理根本无从谈起。
2.2 采集层双模设计:主动探测加被动上报
主动探测部分,雷达每30秒对平台资源注册表里的每个HTTP/HTTPS端点发一次HEAD请求,记录状态码、响应耗时、首字节时间。为了不让探测本身干扰业务,主动探测的并发控制在8个worker内,单目标超时3秒,超时直接记为超时样本,不计入P99统计。
被动上报部分,接入了平台SDK的服务在本地聚合指标,每15秒上报一次。所谓聚合,是指SDK内部维护一个环形缓冲区,先算出一个15秒窗口内各指标的P50、P95、P99和MAX,再把这个"时间桶"发出去,而不是把一个一个的原始采样点全量上报。这两种模式为什么并存?因为被动上报依赖业务侧改造,存量服务如果来不及接SDK就有覆盖盲区;主动探测不依赖业务改造,只要有注册地址就能探,但只能覆盖"端点可达性"这一层,拿不到服务内部的多维指标。两套叠加,覆盖率才能到95%以上。
2.3 存储层选型:时序库加对象存储的混合方案
指标类数据进了开源的时序数据库,保留90天,超过90天的原始指标降采样后转存对象存储,保留180天。事件类数据——告警、异常、恢复、状态变更——放在关系型数据库里,按天分表。这个混合方案看起来很朴素,其实是经过了对比的:时序库擅长高基数多标签查询,但对"某一天所有服务总共发生了多少次P99突刺"这种关系型聚合极度不友好;反过来,事件表按天分好表,一条SQL就能做这种聚合。所以我的结论是别追求"一个库装一切",每个组件干它最擅长的事,数据之间的关联靠统一的时间戳和实体ID对齐。
3. 核心数据结构:实体、样本、依赖关系三件套
3.1 实体注册表:雷达屏幕上的光点
雷达的基本对象叫"实体",一共三类:service(平台上的微服务,标识用app_id)、node(宿主机器或Kubernetes节点,标识用node_id)、infra(中间件实例,比如MySQL、Redis、Kafka,标识用instance_id)。每个实体有一组静态属性,比如所属团队、环境、机房、部署版本,还存在关系型数据库里;还有一组动态状态,比如当前健康状态、最近心跳时间、当前运行版本,这些缓存在内存哈希表加Redis里。之所以动态状态不走数据库,是因为每次全量扫描要快速判断"这个实体的上次心跳是否超时",走缓存能省掉大量重复查询。
3.2 样本时间桶:统一的指标落盘格式
主动探测和被动上报的数据经过统一格式化后,变成一条样本记录,字段包括:时间戳(秒级)、实体ID、指标名、指标值、维度标签(JSON格式)。为了控制存储量,做了两级聚合:15秒原始桶保留7天;5分钟聚合桶按实体ID加指标名加维度标签,聚合成avg、max、min、P50、P95、P99,保留90天。有一个量化数据值得参考:当时平台上有210个服务、680个节点实例、85个中间件实例,每15秒一个桶,单条样本约200字节,原始桶的日增量大概是3到4GB。这个量级在现代硬件上不算大,但如果不做聚合,一年下来接近1.5TB,查询会越来越慢,日志审计也不好做。
3.3 依赖关系表:自动挖掘,绝不人工维护
因果推断的基础是依赖关系,但这份关系表绝不能靠人工维护。研发同学提交的依赖清单往往滞后于真实调用关系,而且经常漏掉那些"隐性依赖"——比如某服务虽然不直接调另一个服务,但通过消息队列间接依赖。PLFM_RADAR的做法是从被动上报的聚合桶里自动挖掘:每个服务每15秒上报的桶里带有"本服务调用了哪些下游服务、各多少次、总耗时多少"的字段,按天归并后生成依赖方向。比如服务A的桶里持续出现对服务B的调用记录,依赖表就写入A→B这条边,并记录近7天平均调用量,这个量在后面计算异常传播影响范围时非常重要。
4. 异常检测引擎:三类算法的组合拳
4.1 静态阈值:简单但真不好配
对CPU使用率、磁盘占用率、消息队列积压数这类有绝对安全边界的指标,直接配静态阈值。比如磁盘水位超过85%就必须预警,这不是因为85%有什么科学依据,而是因为这个水位留给运维的处置窗口大约还有两小时——超过这个点再做扩容或清理往往已经来不及。
静态阈值最大的坑是怎么定值。我强烈建议不要拍脑袋,拿过去两周的数据反推:取每个指标两周内的P95曲线,找出倒数第二波高峰的值作为阈值的底,再留20%到30%的余量。这样定出来的阈值基本能避免"白天正常、晚上误报"的尴尬。有朋友会问为什么不留更大余量,因为余量越大,真实异常被发现的时间越晚,"近实时"就名存实亡了。
4.2 动态基线:EWMA加周周期解决忙闲不均
业务型指标,比如QPS、P99延迟、错误率,用固定阈值会死得很难看。一个活动页平时QPS只有300,活动期间飙到8000,如果按固定5000阈值,活动前半小时就该误报了。我们的做法是给每个指标维护三条EWMA(指数加权移动平均)曲线:分钟级衰减系数0.3、小时级衰减系数0.05、天级衰减系数0.01。检测时用小时级EWMA作为预测基线,拿分钟级EWMA的实际值和基线对比,偏差超过历史标准差的3倍就判定异常。
用3倍标准差而不是固定百分比,是专门为了照顾低基数指标。一个请求量本来就很小的内部管理接口,某分钟内从10次变成30次,固定百分比会认为暴涨200%触发告警,但3倍标准差的标准下,这个波动还在正常范围内,不会打扰值班人。我在第一版就吃过这个亏,后来把十多个规则从"百分比"改成"标准差"之后,误报率直接砍了一半。
4.3 突变点检测:轻量CUSUM抓慢性恶化
动态基线能解决绝对数值异常,但解决不了趋势突变。举个例子,某个指标在一小时内从P50 50ms缓慢爬升到80ms,每一步都不超基线阈值,但整体趋势明显在恶化。这种慢变问题靠阈值和基线都发现不了,必须用累积和(CUSUM)检测。实现起来不复杂:每个检测周期计算当前偏差相对历史均值的增量,把增量累加起来,一旦累积量超过设定上限就触发。关键参数是允许漂移量和控制限,我们是按指标历史方差的0.5倍和3倍来初始化的,上线后微调过一轮。这个算法的代价是需要额外保存一份累积量状态,内存开销可接受,但收益是把"发现时间"从突变后的三小时缩短到了十五分钟。
4.4 上游联动判定:怎么回答"哪里先坏"
检测引擎发现一个服务异常之后,并不会立刻发告警,而是先查依赖关系表,把该服务的所有上游实体拉出来,逐一检查它们在过去30分钟内是否有异常标记。如果发现某个上游在这之前已经异常,就把当前服务的告警状态标记为"次生异常",推送文案写成"检测到服务B异常,可能由上游服务A异常引起,A的异常发生时间为X,建议先排查A"。
这期间有个坑值得多说两句:依赖链路过深时会连环误判。A异常导致B异常,B又导致C异常,如果C的检测比B早一秒,上游联动可能把B误标为C的次生。所以实现上按异常发生的时序排序之后,只保留最早发生的那一跳作为根因候选,同时限制联动深度最多3跳,超过3跳不再向上追溯。处理完这两点之后,联动判定的准确率才达到了能上线给值班人用的水平。
5. 告警触达与误报治理:如何把打扰压掉八成
5.1 告警分级:让人的注意力按重要度分配
告警分三级,这个分级直接决定了触达方式和响应时限:
| 级别 | 定义 | 触达方式 | 响应时限 |
|---|---|---|---|
| P0 | 核心链路不可用、数据丢失 | 电话加IM加短信 | 15分钟 |
| P1 | 非核心服务可用性受损、关键指标越界 | IM加短信 | 30分钟 |
| P2 | 边缘指标越界、疑似异常 | 仅IM,进入"待观察"列表 | 24小时 |
分级的意义在于不让所有告警共享同一条通道。我见过很多团队把全部告警都发到同一个值班群,结果值班人为了避免漏掉真正的P0,只能高强度盯着所有消息,时间一长反而麻木。分级之后,P2告警默认不打扰任何人,只在次日晨检摘要里汇总,真正的P0反而更容易被第一时间注意到。
5.2 聚合、静默与抑制:三招对付告警风暴
告警风暴是每个监控系统的死敌,PLFM_RADAR用了三个策略。第一,同实体同指标在未恢复期间只允许存在一条活跃告警,后续触发只刷新"最后一次触发时间"和"累计触发次数";第二,同一依赖链上的多条告警合并为一条事件,标题格式是"源头A→影响面B/C/D";第三,已知的变更窗口,比如发布单、配置变更单,自动触发静默,静默期间不产生新告警,但异常仍然记录在案,用于发布后的回归分析。回看数据,这三个策略合计让值班群的告警条数减少了大约八成,其中告警合并贡献最大。
5.3 反馈闭环:误报标记和阈值自动修正
每一条告警都带一个"是否有用"的反馈入口,值班人可以用一个操作标记误报、确认有效或无法判断。每天跑离线任务统计近30天各规则的误报率,连续三天误报率超过30%的规则自动进入"观察模式"——告警降级为P2且不触达,同时把该规则涉及的指标阈值自动上调5%。这套闭环的价值在于让系统有自我纠偏能力,而不是靠人力反复去调整规则。上线三个月后统计,全量告警的误报率从初始的40%降到了4%出头。贡献最大的两块就是第4章里的动态基线和上游联动判定:前者砍掉了大批"白天忙时正常波动"的误报,后者砍掉了大批"看起来像自身问题、实际是上游先挂"的误报。
6. 关键实现细节与踩坑记录
6.1 第一期调度器为什么慢:固定节拍替代回收队列
主动探测调度器的第一版用的是"等上一个任务完成再发下一个"的模型,结果某个探测请求卡住3秒超时之后,整个扫描节奏全部滞后,有的服务要10分钟才被探到一次。后来改成固定节拍模型:一个定时器每30秒生成一批待探测任务投进无界队列,工作线程池自动从队列取任务,不管上一批有没有探完,下一批照常生成。滞后导致的重复探测问题,通过给每个目标打"上次探测时间戳"来过滤,队列里发现目标时间戳已经被更新就放弃执行。这个改动让最差扫描间隔从10分钟稳定回到30秒以内,主动探测的"实时性"才算真正成立。
6.2 时间戳对齐:时钟偏差差点毁了P99窗口
被动上报的SDK在客户端本地按15秒聚合,但客户端和服务端存在时钟偏差,有的桶时间戳是整点零分,有的是整点零七秒。检测层做窗口聚合时,如果严格按15秒边界切窗口,会碰到大量桶落在边界外,导致窗口覆盖不足,算出的P99值偏低甚至出现负值异常。解决方法是:写时序数据时不直接按客户端时间戳落盘,而是由服务端收到样本后做一次"重分桶",把样本按服务器时间对齐到最近的15秒整数倍边界。重分桶会带来一点数据归并误差,边界上的样本可能被归到相邻桶,但换来的是一致性和可计算性,这个代价完全值得。
6.3 高基数标签失控:差点把存储打爆
有段时间我们在探针上报里加了一个"请求路径"标签,比如path=/api/user/info这种,结果时序序列数量从几十万涨到几亿,存储直接告急。教训很直接:实体维度(服务、节点、中间件)可以做标签,业务维度(请求路径、用户ID、订单号)禁止进时序标签。业务明细数据该进日志进日志,该进数据仓库进数据仓库,雷达需要的只是聚合结果。后来我们把所有业务维度标签全部删除,存储量立刻降回原来的水平,检测性能也恢复正常。这个坑写在这里,希望同行别重复踩。
6.4 告警规则上线前必须做的回归测试
很多监控系统上线即翻车,是因为规则没有经过历史数据回放验证。PLFM_RADAR在新增或修改任何检测规则时,都必须先用过去30天的数据做回放,对比"规则会产生的告警数"和"当时值班人实际处理的告警数"。如果回放结果显示某些天会产生超过50条告警,而实际上那几天平台很平稳,这条规则就不合格,需要继续调参。这个机制看似增加了一点工作量,但它避免了"新规则上线当晚就炸群"这种灾难,长期看省下的值班精力远大于投入。
7. 系统上线一年后,运营层面真实发生的三件事
7.1 值班手机的夜间轰炸变安静了
最直观的变化是值班手机变安静了。以前P0级告警一周少说三四次,上线后半年里真正需要人工介入的P0级事件只有两次。不是说平台故障变少了,而是雷达把很多原先表现为"多处异常"的故障,在源头阶段就发现了,并且通过联动判定把下游的连锁告警全部吞掉,只推一条源头消息。技术人员最反感的是无意义警报,当值班人逐渐意识到"雷达推来的告警基本都查有实据",他们对告警的信任度才真正建立起来,响应速度也快了。
7.2 发布后的"半小时静默观察窗"成了研发新习惯
雷达接入发布系统后,每次发布完成会自动生成一个30分钟的观察窗,期间雷达不推送新增告警,但会把异常记入发布事件详情。研发团队自发形成了习惯:发布完不看GitHub动作日志,而是先看雷达的"发布影响报告",里头有P99曲线、错误率、依赖调用量这几样关键数据是否在预期范围内。这比传统的"盯着监控大屏手动对比"效率高不少,也减少了"发布后谁都不确定到底有没有问题"的焦虑感。
7.3 数据比人记得准:复盘时时间线救了场
有一次复盘一个Kafka消费堆积的事故,人工回忆是"大概下午三点开始的",但雷达的事件时间线明确显示堆积从14:52就开始了,而且14:52:30那条生产者端的P99突刺才是一切的源头。这件事上了复盘文档之后,大家才意识到:故障复盘里最大的谎言就是"我觉得是从那个时间点开始的"。雷达的价值不止于实时告警,它留下的完整事件时间线对事后分析同样值钱。这也是我反复强调"事件时间线必须作为一等公民存储"的原因。
写这套雷达的过程中,我最深的体会是:监控类系统的价值不在功能多,而在"能不能让一个人在一分钟内搞清楚现状"。PLFM_RADAR的功能清单摊开看并不惊艳——主动探测、时序存储、动态基线、告警聚合,每一块都是成熟方案。但围绕"定位源头"这个核心目标重新组合之后,值班体验的改善是质变级的。
如果你也想搭一套类似的系统,我建议别急着铺大而全的框架,先想清楚三个问题:第一,你平台上现在有多少实体是"看不见"的?第二,你现在的告警里有多少是"同一件事翻来覆去地说"?第三,如果只能让值班人看一屏,你最希望那一屏显示什么?把这三个问题的答案写下来,再动手,可能两周就能跑出第一个有价值的原型。
最后分享一个小技巧:雷达类系统一定要把"事件时间线"当作一等公民来保存,千万别只存指标不存事件。指标只能告诉你"异常了",事件时间线才能告诉你"事情的先后顺序",而先后顺序恰恰是定位根因最直接的突破口。这个坑我踩过,希望你不用再踩一遍。