
作为一个常年跟平台稳定性打交道的人我越来越觉得监控和感知是两回事。去年我们团队做了一套内部代号叫 PLFM_RADAR 的平台健康雷达系统初衷很简单不想再被 Prometheus 告警、ELK 日志、SkyWalking 链路和一堆自定义脚本折腾得团团转想要一个能像雷达一样持续扫描平台状态、提前发现风险的东西。这套系统上线后我们确实抓到了几个靠人肉巡检根本发现不了的问题也踩了不少坑。这篇就把 PLFM_RADAR 从信号选取、判定逻辑到落地实现和事故复盘的过程完整梳理一遍给同样在跟平台稳定性较劲的朋友做个参考。1. 为什么我放弃堆监控工具转向做一个平台雷达先说说背景。我们的平台是典型的多服务分布式架构前后端加起来二十多个线上服务依赖 MySQL、Redis、Kafka 和几个外部接口。跟大多数团队一样监控工具是慢慢长出来的指标监控用 Prometheus Grafana日志走 ELK链路追踪那套我们也让业务接了。表面上看什么都不缺真正出问题时才发现工具越多反而越乱。最典型的场景是这样的某个服务 CPU 飙高Prometheus 会先报一条告警紧接着日志系统因为业务报错暴增又刷出一堆 ERROR 告警链路系统再给你来几条慢请求提示。同一个故障三个体系各报各的值班同学要在多个系统之间来回切换靠人脑把碎片拼出全貌。这个拼接过程往往要十几分钟而故障恢复的黄金时间早过了。PLFM_RADAR 的核心思路就是把分散的信号采集和统一的态势判断拆开。它不替代 Prometheus 或 ELK而是在它们之上做一层融合感知把指标、日志、事件三类数据汇聚到一个判断引擎里用统一的规则去识别平台现在是不是处于异常状态。打个比方以前是给每个器官都配一个化验单PLFM_RADAR 则是那个把所有化验单汇总起来、给你一个综合诊断结果的医生。这个定位决定了它的设计原则信号要全但不要杂。只采集对平台健康状态有判别力的信号避免把监控系统变成数据垃圾桶。判定要快但不能乱报。核心规则必须能在秒级完成判断同时要有抑制、去重、收敛机制防止告警轰炸。看整体而不是看单点。单个指标超标未必是故障多个指标联动异常往往才是真问题雷达的价值就在这种关联识别上。适合参考这套做法的团队是那些已经有一定监控基础、但被告警疲劳和跨系统排查折磨过的中小型平台团队。如果你的监控体系才刚开始搭我建议还是先把基础指标采集做好雷达这类融合层等数据积累到一定程度再做否则容易变成空中楼阁。2. 三类核心信号的设计指标、日志、事件各取所需PLFM_RADAR 的信号来源分为三类每一类在我们的实现里都有明确的取舍。这个取舍决定了雷达的有效半径也直接决定了后面告警的质量。2.1 指标信号为什么我坚持用百分位数而不是平均值指标是雷达最基础的信号源。我们最终圈定了四个必采指标请求延迟、错误率、吞吐量QPS/TPS、资源饱和度CPU、内存、磁盘 IO、连接数。这四个指标也被称为 USE/RED 方法论的核心组合——RED 关注请求侧的速率、错误和延迟USE 关注资源侧的使用率、饱和度和错误两者搭配就能覆盖平台的大多数异常形态。但这里有个特别容易踩的坑延迟指标如果看平均值会被长尾请求严重误导。举个例子某接口 80% 的请求都是 50ms 以内但 5% 的请求慢到 5 秒平均值可能才 300ms 左右看起来一切正常。而采用 p99 百分位数意味着只要 1% 的请求变慢就会被捕捉到。我们实际跑下来p99 对用户体验的还原度远高于平均值。所以 PLFM_RADAR 的延迟指标统一用 p50、p95、p99 三档告警主要挂 p99p50 用于趋势观察。指标采集频率我们也做了分层设计信号类型采集频率存储周期说明核心业务指标QPS、错误率、p9910 秒15 天实时性要求高用于告警判定资源指标CPU、内存、磁盘60 秒30 天变化相对缓慢用于根因分析依赖系统指标DB、Redis、Kafka30 秒15 天覆盖中间件健康状态采集频率不是越高越好。我们之前把资源指标也调成 10 秒一次结果是存储成本翻了三倍但告警效果几乎没变化。资源指标本身的惯性很强——CPU 飙高到触顶往往是一个渐进过程60 秒的采样粒度足够捕捉。2.2 日志信号只保留能参与判断的日志日志要不要纳入雷达的判断引擎我的结论是要但必须精简。全量日志流进判断引擎既不经济也没必要。PLFM_RADAR 的日志接入做了两道过滤第一道是级别过滤。ERROR 级别的日志全量采集WARN 级别只采集带有特定业务标记的部分比如超时、重试、降级等关键字INFO 一般不进雷达。第二道是滑动窗口聚合。日志进入雷达后不逐条判断而是按 1 分钟窗口聚合统计 ERROR 条数、错误类型分布、涉及的服务实例数。这个设计的目的是识别错误是孤例还是爆发式增长——单条 ERROR 可能只是偶发网络抖动但 ERROR 条数在 1 分钟内翻了 20 倍这绝对是需要立即响应的信号。2.3 事件信号发布和配置变更是最容易被忽略的雷达盲区很多人做监控只盯着指标和日志忽略了事件数据。但在我们的实践中发布、配置变更、扩缩容、数据库迁移这类操作恰恰是故障的最大诱因。PLFM_RADAR 会从发布系统和运维平台同步关键事件包括服务版本发布、配置变更、实例上下线、Kafka topic 调整、数据库结构变更等。事件数据本身不直接产生告警但它参与关联判定。比如规则里有一条某个服务发布后 10 分钟内错误率超过阈值且持续 15 分钟触发发布相关异常告警。如果没有事件数据做锚点这条规则根本无法定位到发布动作。这类事件信号本质上是在帮你建立变更-异常的因果链条排查速度会快非常多。3. 从信号到告警三层判定逻辑让雷达既有灵敏度又有准确率信号采集进来之后最核心的问题就是怎么判断现在该不该告警这是 PLFM_RADAR 最花心思的地方。我们分了三层逻辑逐层递进。3.1 第一层静态阈值——基础但必须谨慎静态阈值是最朴素也最容易出问题的判定方式。我们踩过的坑是凭感觉定阈值。比如CPU 超过 80% 告警这个值对 IO 密集型服务来说可能太晚了而对某些计算型服务来说完全正常。正确做法是先用历史数据找基线。我们花了大概两周时间把各个服务的指标按小时维度打点用过去 30 天的历史数据计算出每个指标的正常波动区间然后在这个区间顶部加一个安全余量作为初始静态阈值。这样定出来的阈值是每服务每指标独立的而不是全平台一个值。比如订单服务的错误率基数是 0.1%阈值可以定在 0.5%而一个内部工具的基线可能就是 0.5%阈值就得定在 2%。3.2 第二层趋势判定——对付缓慢恶化的问题静态阈值应对突发性故障很有效但对付温水煮青蛙式的缓慢恶化几乎无能为力。一个服务的延迟从 100ms 慢慢涨到 500ms连续涨了两周峰值始终没超过阈值业务却不声不响地变慢了。这类问题只有趋势判定能发现。PLFM_RADAR 的实现方法是基于滑动窗口的移动平均线加标准差检测。具体逻辑对每个核心指标保留最近 30 个数据点5 分钟窗口计算移动平均值和标准差。如果当前数据点超过移动平均值 2 倍标准差且连续 3 个窗口都保持这个偏离就触发趋势告警。这套逻辑的原理是正常指标的波动符合统计规律偏离到 2 倍标准差以上又是持续性出现说明系统状态确实发生了结构性变化。这里我补充一个细节趋势判定一定要设置最短持续时间至少 3 到 5 个窗口。否则偶发的毛刺会频繁触发把告警系统的信誉消耗殆尽。我们最开始没加这个约束上线一周就收到一堆趋势异常告警全是瞬时抖动造成的。3.3 第三层关联规则——雷达真正的价值所在关联规则是 PLFM_RADAR 区别于普通监控告警的核心。我们通过规则引擎将不同维度的信号组合起来识别单指标告警无法发现的复杂故障模式。举一个实际配置过的规则错误率上升 延迟升高 发布事件存在。这三者同时满足时系统会生成一条疑似发布引发故障的高优先级告警并自动带上最近一次发布记录、涉及服务、变更内容和发布人信息。相比单个错误率告警这条关联告警直接把排查方向指给值班同学了。另一个我们常用的关联模式是依赖系统异常传播。比如 Kafka 消费堆积会导致下游写库延迟进而引起接口 p99 上升。单独看接口延迟你可能误以为是应用层问题但雷达通过多个信号的时间相关性判断先看到消费堆积信号再看到数据库写入延迟最后才看到接口 p99 飙升就会生成一条以Kafka 堆积为根因的告警而不是只报接口慢了。这套三层逻辑的完整判定流程可以概括为静态阈值层做秒级快速筛查命中则进入下一层确认。趋势判定层过滤瞬时抖动识别持续性异常。关联规则层综合所有信号生成带根因线索的最终告警。4. 落地实现技术选型、关键代码与调试经验4.1 整体技术栈不为炫技只为轻量可维护PLFM_RADAR 的技术栈很朴素我们刻意避免引入太重的东西。核心组件如下数据采集Prometheus 的 exporter 体系指标采集 Filebeat日志采集 自研 Python 事件同步脚本从发布系统拉事件数据。判断引擎Python Redis。Redis 用来存滑动窗口的指标数据Python 负责读取、计算、判定整个引擎是无状态的可以水平扩展。规则存储简单的 YAML 配置文件配合一个配置管理脚本。没有上规则引擎因为我们的规则量级在几十条以内数据库化的规则管理反而增加维护成本。告警出口企业微信机器人、钉钉机器人、邮件三个通道。告警分优先级P0 走电话加企业微信强提醒P1 走企业微信P2 只发邮件汇总。为什么用 Python 而不用 Go纯粹从性能角度看Go 确实碾压 Python但我们的判定逻辑需要频繁调整Python 的迭代速度对我们更友好。实际压测下来Python 引擎处理 2000 个指标的判定单实例 CPU 占用不到 5%完全够用。4.2 趋势判定的核心代码实现下面这段是我们趋势判定模块的简化版核心逻辑注释里写了关键设计考量import redis import json import time class TrendDetector: def __init__(self, redis_host127.0.0.1, window_size30, std_multiplier2.0, min_consecutive3): self.r redis.Redis(hostredis_host, decode_responsesTrue) self.window_size window_size # 滑动窗口点数5分钟/30个10秒点 self.std_multiplier std_multiplier # 标准差倍数2.0表示偏离均值2倍标准差 self.min_consecutive min_consecutive # 连续偏离窗口数低于此值不告警 def _push_point(self, key, value): 将新数据点推入滑动窗口同时删除过期点 ts int(time.time() // 10) # 按10秒一个时间片聚合 pipe self.r.pipeline() pipe.zadd(key, {ts: value}) # 只保留最近 window_size 个时间片 pipe.zremrangebyscore(key, -inf, ts - self.window_size) pipe.execute() def _window_values(self, key): 获取当前窗口内全部数据点 return [float(v) for v in self.r.zrange(key, 0, -1)] def check_trend(self, metric_key, value): self._push_point(metric_key, value) values self._window_values(metric_key) if len(values) self.window_size: return None # 窗口未填满时不判定避免冷启动误报 mean sum(values) / len(values) variance sum((v - mean) ** 2 for v in values) / len(values) std variance ** 0.5 # 标准差接近0时退化处理直接用均值本身做基线 threshold mean (self.std_multiplier * std if std 0.1 else mean * 0.2) if value threshold: self._consecutive_count(metric_key, True) if self._consecutive_count(metric_key) self.min_consecutive: return {trend_up: True, mean: mean, current: value, threshold: threshold} else: self._consecutive_count(metric_key, False) return None def _consecutive_count(self, metric_key, incrementNone): count_key ftrend_count:{metric_key} if increment is True: return self.r.incr(count_key) elif increment is False: self.r.set(count_key, 0) return 0 return int(self.r.get(count_key) or 0)这块有两点经验要提一是标准差为 0 时的退化处理非常关键。一些长期平稳的指标比如某服务的错误率长期为 0一旦出现哪怕 0.1% 的错误标准差计算会把它放大成巨大波动。我们用均值乘 0.2 作为最低基线就是为了避免这类指标在 0 附近波动时误报。二是窗口填充期要主动跳过否则系统刚启动那五分钟会因为数据不满窗算出偏差巨大的均值和标准差产生一批伪告警。4.3 关联规则的实现用时间窗口做信号对齐关联规则的难点在于不同信号的时间戳很难完全对齐。我们采用的是时间窗口对齐方案规则引擎以 1 分钟为粒度检查每个信号在该分钟窗口内是否命中条件。比如发布相关异常规则核心逻辑是这样的rule_name: release_related_incident description: 服务发布后10分钟内错误率超标且持续15分钟 condition: event: type: release service: {service} within_minutes: 10 metrics: - metric: error_rate service: {service} op: value: {service_error_threshold} duration_minutes: 15 logic: AND priority: P0 notify_channel: [wechat_dingtalk, phone]这里{service}变量会在运行时被实际服务名替换。规则引擎先扫描最近 10 分钟有没有发布事件如果有再检查对应服务在此窗口内的错误率数据是否持续超标。只有两个条件都成立才触发 P0。这个设计大大减少了发布后偶发抖动造成的误报因为持续时间这一条就能过滤掉很多瞬时问题。4.4 调试阶段最容易忽略的问题PLFM_RADAR 调试期我印象最深的是时区问题。我们的日志服务默认记录的是 UTC 时间而事件同步脚本用的是本地时间。两个时间相差 8 小时关联规则从一开始就齐刷刷地失效——发布事件找不到对应的日志异常反而错误地把白天的正常业务波动关联到了凌晨的发布事件上。排查了整整两个小时后才意识到是时区戳问题。统一协调世界时存储、展示层再转换这是雷达系统从一开始就该定的规矩。另一个调试经验是影子模式。正式切换告警前我们让雷达跑了一个月的影子模式——所有判定结果只记录到日志里不真发告警。这一个月里我们把每条判定结果跟实际情况做对比校准了十几条规则的阈值和时间参数。没有这一步直接上线告警一定会把人逼疯。5. 真实事故复盘一次 Kafka 堆积PLFM_RADAR 的完整行动时间线讲完设计用一个我们实际经历的事故复盘来看看雷达到底怎么工作的。这个故障不是特别严重但非常有代表性。某个周五下午订单服务同步数据到数仓的消费任务突然变慢。传统监控场景下团队会先收到 Kafka 消费延迟告警然后各自打开大盘去找原因大概率要花二十分钟以上。而 PLFM_RADAR 的关联规则在 3 分钟内就定位到了根因方向完整时间线如下14:32指标信号捕捉到订单服务 p99 延迟从 180ms 上升到 420ms静态阈值告警触发进入趋势确认阶段。14:33日志信号出现预警ERROR 条数在 1 分钟内从每分钟 12 条跳到 210 条涉及的服务实例从 1 个扩散到 8 个。14:34雷达识别到 Kafka 消费堆积信号消费 lag 从几十条飙升到两万多条。14:35关联规则引擎完成判定生成一条 P1 告警内容明确标注根因疑似为下游数据库写入性能下降导致的消费阻塞并附带了 Kafka 消费组、数据库实例、最近 10 分钟慢查询数等上下文信息。14:37值班同学根据告警定位到数据库连接池打满确认是另一个业务线的批量任务把数据库 CPU 占满直接暂停该任务。14:42消费恢复p99 回落雷达自动解除告警状态整个过程 10 分钟恢复。事后复盘时团队的一致感受是这个故障的技术难度不高但如果没有雷达的关联提示仅凭告警列表去逐个排查很可能要花 30 分钟以上。雷达真正的贡献是把定位根因这个环节的时间压缩掉了而不是替代人做决策。它告诉你在哪个方向找具体怎么修还是人来定。6. 告警疲劳是雷达的头号杀手误报、漏报与收敛策略雷达系统做得再好如果告警质量不行最终会被团队无视。我想重点聊聊误报、漏报和告警收敛这三件事这是我们上线半年以来体会最深的部分。6.1 误报宁可漏报不可误报不一定做告警系统的人都听过宁可漏报不可误报这句话但我的实际体会是这句话只对了三分。漏报的代价是错过故障后果可能是几十万业务损失误报的代价是消耗信任后果是长期来看告警没人看、真故障也被忽略。两者都是灾难。我们的处理策略是分级治理。P0 级告警直接影响核心业务可用性宁可误报也要全覆盖因为它的代价实在太高P1 和 P2 级告警则相反严格控制误报率因为它们数量大误报会快速透支信任。具体做法上P0 告警只在多个独立信号同时确认时才触发我们已经讲过关联规则了P1 告警要过趋势确认P2 告警基本只做汇总每天一封邮件不实时打扰。这套分级策略上线后告警量下降了 60%P0 告警的准确率维持在 85% 以上我认为在监控告警这个领域要接受一个现实完全消灭误报是不现实的把误报控制在可接受的代价内才是务实的。6.2 漏报案例一条被业务增长掩盖的异常我们漏过一次比较严重的告警。某服务的 QPS 在一个月内自然翻了三倍业务增长CPU 使用率跟着涨但一直在静态阈值以下——因为阈值是按上个月 80% 峰值定的而新峰值才到 65%。结果这个服务被压得越来越慢直到某个流量尖峰时直接雪崩连告警的机会都没有。这个案例给 PLFM_RADAR 带来的改进是阈值必须周期性自动校准。我们后来加了一个每周自动更新基线阈值的任务把过去 7 天的数据作为新基线并在阈值变化超过 30% 时强制人工确认。这套机制上线后再也没出现阈值被远远落在后面的情况。6.3 告警收敛让同一条故障只响一次告警收敛是防止告警轰炸的关键技术手段。我们实现了四级收敛机制时间戳合并同一服务同一规则在 10 分钟内重复命中合并为一条告警状态变成已重复 N 次。依赖收敛当一个告警被判定为另一个告警的根因时只保留根因告警。判断依据是事件时间先后和依赖关系图。风暴抑制如果 5 分钟内告警数量超过 20 条自动进入静默风暴模式只发一条疑似大规模故障爆发的通知后台把全部告警积压成一份清单等风暴结束后统一汇总。静默时间窗夜间 2 点到 5 点P2 级别告警自动延后到早上 9 点合并发送避免值班同学被低优先级告警反复叫醒。这套收敛机制上线后告警量从最高的每天 240 条降到了每天不到 30 条而且值班响应质量明显提升——告警变少了每一条反而更受重视。7. 经验沉淀雷达上线半年后的建议清单最后整理几条我们在 PLFM_RADAR 演进过程中沉淀下来的实操建议算不上系统方法论但都是花时间换来的先建基线再定阈值。任何静态阈值的初始值都不要拍脑袋至少用两周的历史数据算分位数然后从分位数的 80% 开始调。基线要定期更新我们固定每周一凌晨自动重算并且保留了上一次的榜单方便对比变化。信号宁可少而精不要多而滥。每接入一种新的信号源之前先问自己三个问题它能判别什么故障形态故障发生时它是不是真的会变它变了之后团队能不能据此行动三个问题有一个答不上来这个信号就不该进雷达。告警文案直接给出下一步动作。我们曾经把告警写得特别详细但值班同学反而不知道怎么处理。后来改成每一条告警都以问题现象 疑似根因 建议动作 相关负责人四段式展示。排版变了之后平均处理时间下降了差不多三成。告警不是读给同行听的分析报告是给值班同学的行动指令。规则可配置性比规则本身更重要。业务是不断变化的今天合适的规则下个月可能就变成噪音。PLFM_RADAR 的规则全部用 YAML 文件管理修改后热加载不需要重启引擎。这个灵活性让我们敢持续调优规则而不是因为改起来麻烦就不动它。人要定期复盘雷达的盲区。雷达再强也不可能覆盖所有故障形态。我们在每季度做一次故障演练人为制造一些典型故障模拟下游超时、模拟数据库连接池耗尽等看雷达能不能正确识别和定位。上一轮演练就发现我们对节点网络分区这种场景完全没有信号覆盖后来专门补了心跳检测和网络探活信号。PLFM_RADAR 到今天已经稳定运行大半年团队对它从最初的怀疑态度变成现在的依赖。它不是什么高深的东西所有技术组件都是成熟开源方案核心价值在于我们把哪些信号值得看、什么时候该响、响了之后怎么办这套判断逻辑沉淀成了系统。如果你也在被监控碎片化、告警轰炸、根因定位慢这些问题困扰照着这个思路搭一套自己的雷达并不难。记住最关键的一点雷达的命中率不是靠算法多炫而是靠你对自家平台的理解有多深。