半夜一点十七分,我被连续三条P0告警震醒。爬起来打开监控屏,满眼都是绿色:机器活着、接口通着、成功率没掉。可时间轴往前拉一点就会发现,从晚上八点开始,新用户下单链路里“从点击到支付”的中位时长默默涨了800毫秒。平台没挂,但手感明显不对——这种“什么都正常但又感觉不对”的夜晚多了之后,我和团队决定自研PLFM_RADAR这只平台雷达。
PLFM取自Platform,RADAR就是雷达。它不解决“服务器挂没挂”这种二值问题,而是要持续回答一个更难的问题:平台现在的状态,是它该有的状态吗?它会主动扫描资源、服务、业务、体验四个层面的信号,用基线和趋势判断异常,而不是只等某个阈值点亮红灯。这篇文章会把我们从需求梳理、指标建模、数据采集、异常判警到告警收敛的完整设计过程写下来。如果你正在做平台稳定性、业务监控或SRE方向,可以参考;如果你每天被告警噪音烦得够呛,想把手里的监控体系做一次升级,里面的不少思路也能直接用。
1. 立项那天的真实痛点:为什么静态监控救不了平台
1.1 传统监控能告诉我们什么
传统监控不弱,但它自带盲区。我见过很多团队的监控面板都长这样:CPU使用率、内存水位、磁盘IO、网络带宽,再配几个HTTP存活探针。这套东西对“机器坏了”特别敏感,对“平台病了”却几乎无感。一台机器CPU 100%与业务成功率掉到80%,之间隔着太多因果链:可能是流量突然暴增,可能是某个依赖超时,也可能是慢SQL拖住了连接池。资源指标只是表象,真正的异常往往藏在业务语义里。
举一个让我印象很深的事故:夜里支付回调队列积压,消费者线程疯狂重试,下游对账系统延迟越来越大。这时候CPU不高、内存不高、接口成功率还是99.95%,传统监控一条告警都不会出。但用户侧的感知是“支付成功了,优惠券半天不到账”。等到人工发现的时候,积压已经滚了快两个小时。这就是把监控建在“资源”上的代价:它能判断零件有没有坏,判断不了整台机器的驾驶体验。
所以当团队提出“能不能做一台自己的平台雷达”时,我第一个反应是支持而不是怀疑。监控体系不是缺曲线,缺的是对“异常语义”的理解:一条曲线涨了,到底说明业务在变好还是根基在坏;一个指标没动,是不是因为它太粗看不出伤口。PLFM_RADAR的定位,就是把“监控”两个字从“看数据”升级成“判断状态”。
1.2 从一次“假平安”倒推雷达需求
那次事故复盘的时候,我们列了一个清单:如果有一台雷达,它应该提前发现什么?答案最后收敛成四条:
- 发现要早于用户舆情,最好早于客服反馈。
- 能够区分“基础设施故障”和“业务体验劣化”,两者响应方式完全不同。
- 问题定位要有维度,不能只看总量,至少能拆到接口、版本、地域、用户分层。
- 告警要克制,宁可少而准,不要多而杂。
这四条看起来朴素,但每一条都在挑战传统监控的默认假设。比如“早于用户发现”,意味着必须盯业务指标,而业务指标天然有噪声;“有维度”,意味着监控对象要从机器变成多维时间序列。我们自研PLFM_RADAR的原因很简单:市面上的APM工具能解决其中一部分,但没有一个产品能按照我们想要的口径把所有信号打通。自己造轮子,成本可控,而且每一行代码都冲着上面的需求去。
2. 指标资产化:PLFM_RADAR到底在扫什么
2.1 四层指标模型
PLFM_RADAR的采集对象不是笼统的“信号”,而是经过定义的指标资产。我们把平台状态拆成四层。
| 层级 | 典型指标 | 主要采集方式 | 告警敏感度 |
|---|---|---|---|
| 资源层 | CPU、内存、磁盘IO、网络、GC耗时、线程池活跃数 | Agent/Exporter | 中 |
| 服务层 | QPS、RT、错误率、依赖超时、限流丢弃量 | 网关日志、埋点SDK | 高 |
| 业务层 | 订单量、支付成功率、转化漏斗、退款率、客单价 | 业务事件上报 | 高 |
| 体验层 | 白屏率、首屏时间、操作失败率、卡顿率 | 客户端SDK、合成拨测 | 中高 |
四层不是并列关系,而是因果链条:资源层出问题会传导到服务层,服务层劣化再传导到业务和体验。雷达要做的,就是同时捕捉链条上每一环的变化,看异常是从哪里“渗”进来的。
2.2 指标也得先有“户口”
很多团队的第一步其实是错的:把数据接进来就画曲线,画完就完事。真正能用好时序监控,要先做指标资产化——每个指标必须有唯一标识、统计口径、负责人、保活周期。
口径这件事最坑。比如“订单量”,不同团队理解就可能完全不同:是用户提交订单,还是支付成功订单?要不要含货到付款?要不要剔除取消单?口径不一致,同一个曲线在不同人眼里代表完全不同的业务信号,雷达判断再准也没用。PLFM_RADAR里每个指标都有一个录入模板,填清楚定义、单位、聚合方式、允许的缺失时间段,审核通过后才允许进入采集和判警链路。
2.3 基线画像:每个指标都有自己的“正常区间”
指标入了库之后,第二步是画基线。我们按接口、客户端版本、地域、用户分层等维度做拆分,为每个维度组合生成历史分布。这一步非常关键,原因也很简单:总量会被平均掩盖。
我见过一个经典案例:整体支付成功率稳定在99%,完全没触发任何告警;但按客户端版本拆分后,某个灰度版本的成功率只有61%。如果雷达只看总量,这个灰度事故可以躺到用户大规模投诉。PLFM_RADAR的判警进程会同时跑两个通道:全局通道负责感知“大盘波动”,维度通道负责判断“哪里波动”。维度异常往往比全局异常更重要,因为它直接指向根因。
这一层还决定了后续算法的复杂度:基线越细,异常检测就能做得越准,但存储和计算成本也越高。我们在指标进入基线前会做一次分层:核心业务指标做全维度基线,辅助指标只做全局基线,避免把所有指标都拖进高开销的计算。
3. 探针采集层:雷达波束是怎么发出去的
3.1 两种波束:主动拨测和被动埋点
PLFM_RADAR的采集层分两个通道。
主动探针是从雷达外部发射的“合成信号”——定时用真实用户路径去请求核心页面或接口,比如登录、加购、下单、支付回调。它不侵入业务代码,却最能反映用户在公网上的真实体验。CDN失效、证书过期、专线抖动、DNS解析变慢,这些问题被动监控经常没感觉,主动探针一测就露馅。
被动通道则是业务侧埋点和网关日志解析,拿到全量真实流量下的QPS、RT、错误率、业务事件。它的数据量大,信息全,能支撑细粒度维度分析,但有一个天然的盲区:埋点只能覆盖“服务已经收到的请求”。如果用户请求在接入层之前就被黑洞掉了,被动通道一无所知,这时主动探针就是最后的防线。
结论:两个通道都必须有,缺一个就会瞎一只眼。这也是PLFM_RADAR和很多纯APM方案的本质区别——它不偏科,主动与被动同时工作。
3.2 探针调度:别再同一秒打进来
主动探针的调度看起来简单,里面全是细节。最常见的问题是“齐射效应”:几百个探针节点同时发起请求,会制造一个虚假的流量尖峰,不仅影响被测系统,也会污染自己的统计数据。
我们在调度器里加了随机抖动(jitter)。每个探针任务在基础时间窗口内随机偏移执行,避免所有节点在同一时刻开火。伪代码如下:
import random import time def build_schedule(task, period_seconds, jitter_seconds): # period_seconds 为任务周期,jitter_seconds 为最大随机偏移 base = time.time() // period_seconds * period_seconds offset = random.uniform(0, jitter_seconds) return base + offset真实环境里这个抖动值一般设为周期的10%到20%。比如5分钟周期,抖动窗口30-60秒,既能打散请求,又不至于让采样点跨度太大。
3.3 时间对齐和乱序处理
第二个细节更难:时间对齐。被动通道有大量业务实例,如果各实例的系统时间不一致,上报数据就会出现“未来时间戳”和乱序。早年我们直接用实例本地时间打点,结果每次基线聚合都会带上脏数据,异常检测的准确性被明显拉低。
现在的做法是:所有上报数据在入口网关处统一打上“接收时间”,事件自带的业务时间作为辅助字段保留;入库前做一次乱序缓冲,把时间戳回拨的数据先放pending队列,等时钟追平后再写入。时间是不可逆的,这个教训我们是真金白银换来的。
注意:日志和埋点上报一定要区分“事件发生时间”和“平台接收时间”,两个字段都保留,否则修数据时毫无依据。
采样率方面,核心链路全量上报,长尾接口按比例采样;存储上用delta编码和变长压缩,降低原始点成本。采集层稳定之后,雷达才有资格谈判警——数据不准,算法越花哨越灾难。
4. 异常判警算法:从“阈值触发”进化到“趋势感知”
4.1 静态阈值的两种死法
最早版本我们用的是最常见做法:给每个指标配一个固定阈值,比如“RT超过500ms告警”。上线不到一周就发现它两头挨打:阈值定高了,业务已经烂掉半小时才触发;定低了,稍微一点流量波动就误报。更麻烦的是业务天然有周期性——凌晨两点和下午两点的QPS差了十倍,用同一个阈值根本不成立。
4.2 双窗口环比:和过去的自己比
PLFM_RADAR的核心判警算法是“双窗口环比”。简单说,拿当前五分钟窗口的指标,与同时段的历史基线窗口做对比。这个“同时段”很讲究:大促期和日常不能比,工作日和周末不能比,所以基线窗口一般取“昨天同时段 + 上周同一天同时段”的组合,再剔除大促和故障样本。
判断规则不是单纯看百分比,而是计算z-score:
[ z = \frac{x - \mu}{\sigma} ]
其中x是当前窗口均值,μ和σ是基线窗口的均值和标准差。z的绝对值超过阈值(比如3)就认为显著异常。这样设计的好处是:每个指标都有自己的“尺子”,流量大的指标和历史波动大的指标不会被一刀切。
def anomaly_check(current, baseline_mean, baseline_std, z_threshold=3.0): if baseline_std == 0: return current != baseline_mean z = (current - baseline_mean) / baseline_std return abs(z) >= z_threshold4.3 CUSUM:捕捉微小但持续的漂移
双窗口环比对突变很敏感,但有些问题是“温水煮青蛙”型的:缓存命中率从98%每天掉0.1%,连续掉一周,单个时间窗口环比很难判定异常。这类问题需要累积和算法(CUSUM)。
CUSUM的思路很直白:把每次偏离基线的量累加起来,允许正常噪声上下浮动,一旦累积偏移超过容忍上限就报警。它不追求“一跳看出来”,而是擅长发现“持续不对劲”。我们在缓存命中率、依赖成功率、队列积压量这类指标上启用了CUSUM,实际效果比纯环比好了不少。
4.4 多判据打分,而不是非红即绿
单一算法容易误判。PLFM_RADAR的判警模块把多个判据的计算结果汇聚成一个置信度评分,再决定告警等级。打分维度包括:
- 是否超过静态紧急阈值(硬指标)
- 双窗口环比的z-score
- CUSUM累积偏移
- 异常持续时长
- 多维度同时异常的占比
每个维度加权后输出0到100的分数。60分以下进观察名单,60到85走低级别告警,85以上才升级为P0/P1。这一步把一个“是不是异常”的二值问题,变成了“异常置信度有多高”的排序问题——下游告警收敛和值班路由才能有抓手。
5. 告警收敛和触达:别让雷达变成新的噪音源
5.1 指纹去重:同一件事只报一次
异常检测一旦变敏感,告警数量必然上升。如果没有收敛策略,PLFM_RADAR会从一个救命雷达变成一个新的噪音源。我们设计的第一个闸门是“告警指纹”。
每次告警事件生成时,用指标名、维度组合、异常类型生成一个指纹。相同指纹在聚合窗口内不会重复触发,而是持续更新状态;只有状态发生变更(恢复、升级、维度扩缩)才发新消息。也就是说,同一接口的RT异常在五分钟内就算波动三次,值班同学也只收到一条带状态更新的告警。
5.2 分级路由:怎么通知才算负责
收敛之后,下一步是把合适的告警用合适的通道送到合适的人。PLFM_RADAR的默认路由策略可以概括为:
| 等级 | 条件示例 | 通知通道 |
|---|---|---|
| P0 | 核心链路成功率下降超5%,持续超过3分钟 | 电话 + IM 群 + 值班调度 |
| P1 | 非核心服务RT明显恶化,持续超过5分钟 | IM 群 + 页面卡片 |
| P2 | 指标进入观察名单,置信度60-85 | 自动建单,次日同步 |
分级里有个被低估的小设计:持续时间。单点异常可能只是毛刺,持续N分钟才升级到下一级。我们在所有告警上都加了“持续确认”窗口,宁可晚一分钟,不错报一小时。
5.3 告警文案里直接写“哪里不对”
触达质量比通道更难做好。观察过很多系统的告警通知,普遍问题是只告诉值班“服务A成功率异常”,剩下全靠人工查。PLFM_RADAR生成的告警会从指标库里自动拉取维度TopN,直接跟在告警正文里。
告警示例:支付成功率异常(全局99.2% -> 84.7%),z-score: 5.8,异常持续4分钟。维度Top3:客户端版本2.18.3(成功率61%)、地域华东(成功率88%)、机型X20(成功率79%)。
这条告警出来后,值班同学不用打开任何面板就能判断优先排查发布和灰度,而不是从机房开始扫。这套“收敛+路由+维度化文案”组合,上线三个月后把每周告警数量从上千条压到一百出头,有效告警占比明显提升。
这套收敛策略的另一个好处是方便值班交接。告警记录自带聚合、状态、恢复时间,早班同事能在十分钟内把夜间所有事件过一遍,而不是翻上千条聊天记录。我们后来还给聚合窗口加了一个小功能:同一指纹的告警在恢复时会自动发一条“已恢复”消息,带出异常持续时长和当时的Top维度。很多监控系统只关心触发不关心恢复,这是不对的——没有恢复消息,值班同学永远不确定事情是否真的结束了。
6. 上线三个月踩过的坑和雷达抓到的真实问题
6.1 坑一:实例时钟回拨污染基线
前文提过时间对齐,这里展开讲它带来的事故。有一批业务实例的系统时钟和NTP服务器不同步,持续慢了两分钟。结果这批实例上报的“5分钟聚合数据”频繁落入错误的桶,基线窗口里的均值被拉到异常高位。等到时钟修正,基线又突然回落到正常值,导致后面几天连环误报。
排查过程很折磨:指标曲线看不出问题,直到我们把数据按实例ID染色,才发现异常数据来源于固定一批节点。修复分三步:采集端周期校准;网关入口对时间乱序数据做缓冲;判警层把超过接收时间N秒的样本标记为弱置信度,不参与基线计算。这个坑让我明白了,数据管道里的时间问题不解决,后面算法做得再漂亮都是空中楼阁。
6.2 坑二:全局正常,维度在流血(平均掩盖)
前面讲基线时提到过总量被平均掩盖,上线阶段我们真踩了一次。某天下班前,全局支付成功率曲线平稳得很,雷达没有告警;但维度通道里“Android 4.2以下系统 + 老版本App”的支付失败率已经爬到37%。原因是这部分用户占比太小,对总量几乎无影响。
从这之后,维度通道拥有和全局通道同等的优先级。判警进程不再只等全局异常,而是对每个高权重维度组合独立计算置信度。平均值的最大问题,就是把真实情况稀释掉,雷达必须学会“不看平均数”。
6.3 坑三:首次启用的告警风暴
上线第一天晚上,探针刚铺完,我们唤醒全部告警规则。结果凌晨那波小时级任务把某主库的IO延迟打上去,几百台机器同时触发磁盘等待告警。单指标按机器维度收敛根本没挡住——每个机器一条告警,直接轰炸了值班群。
后来加了集群级聚合:把同一集群内相同指标、相同根因假设的告警合并为一条,附带受影响节点数量。告警风暴的本质是粒度问题,收敛不是简单去重,而是找到正确的聚合粒度(集群而非单机、接口而非单Pod)。
6.4 雷达实打实抓到的三个问题
再讲三个有价值的真实案例,都是PLFM_RADAR主动发现的。
第一个是缓存雪崩前兆。某核心缓存集群的命中率四天里从98%缓慢掉到89%,CUSUM在累计偏移超过容忍线时告警。技术同事介入后发现是缓存过期时间配置被误改,流量大量穿透到DB。按静态阈值,可能要到DB扛不住才会触发告警——雷达提前了大概两小时。
第二个是灰度流量分桶错误。新版本上线后,全局错误率没有明显变化,但维度通道显示“客户端版本2.17.x + 接口/order/create”错误率在一小时内持续走高。拆开后发现灰度配置把部分新流量错误路由到了旧逻辑分支。没有维度通道,这个问题几乎不可能被主动发现。
第三个是专线故障。一天深夜,某机房到主站的专线出现间歇性丢包,被动监控里RT只是轻微抖动,服务层毫无异常。主动探针从公网侧探测发现该机房覆盖用户的“下单接口可用性”下降了15%,在用户投诉前生成了P1告警。
PLFM_RADAR从立项到稳定跑通用了三个月,前一个半月都在跟数据和告警噪音做斗争,真正把判警算法调稳反而快。如果让我重新做一次这类平台雷达,我会把更多预算花在指标口径、时间对齐和告警聚合地面上——这三件事决定了雷达的视野准不准,噪音大不大。