做平台稳定性的人私下里都爱讲同一句话事故从来不会凭空出现它一定留足了前戏。延迟升高、错误率抖动、连接数慢慢爬坡、某个接口的耗时曲线突然变得毛糙……这些信号早就在那里问题是你看得见吗我做了六七年线上系统的监控和告警坦白说绝大多数团队并不缺监控工具缺的是把监控当成一个完整系统来设计的思路。今天想分享的 PLFM_RADAR就是我为解决这个问题而持续打磨的一个自研平台监控系统。PLFM 是 Platform 的缩写RADAR 很容易理解雷达——它做的事情和一个不断转动的雷达高度相似不停扫描平台各个维度的运行状态发现偏离正常范围的信号就把它们标出来、报出来让团队有机会在故障真正造成伤害之前动手干预。这个项目适合谁参考我觉得主要有三类人一是负责线上稳定性的 SRE 或运维工程师二是想给后端服务补全可观测性能力的中高级开发三是正在搭建内部监控体系、需要一套可落地方案的团队负责人。它不解决所有问题但能帮你把出了事才看日志的被动姿态改写成异常刚露头就能感知的主动姿态。下面我把这个系统从设计动机到实际踩坑的过程完整拆开讲尽量讲透。1. 为什么需要 PLFM_RADAR1.1 从一个让人后怕的线上事故说起先讲一个促使我下决心做这套系统的真实场景。有一个晚上业务方的同事在群里丢了一句今天的订单曲线不太对。当时所有人下意识去看基础监控CPU 正常、内存正常、磁盘 IO 正常数据库主从延迟也正常。但业务方说不对那就是大概率真有问题。后来花了大概四十分钟才在一个边缘服务的错误日志里发现它调用下游一个内部接口时超时重试逻辑出了 bug导致请求批量失败而失败的流量又被网关静默吞掉没有体现在任何一张基础监控图表上。这件事让我很警醒。传统监控的思路是先定机器再定指标比如盯住 CPU、内存、带宽觉得这些不爆就没事。可现代业务系统的故障形态早就不是资源耗尽这么简单了接口间依赖错乱、数据不一致、缓存击穿、慢查询堆积、限流误触发……这些故障可能完全不影响资源水位却会直接体现在业务结果上。所以我们必须把监控对象从机器资源升级为业务状态和关联关系这也是 PLFM_RADAR 最早的出发点。1.2 PLFM_RADAR 到底是个什么系统一句话概括PLFM_RADAR 是一套面向平台全局的周期性感知与异常探测系统。它的核心动作不是被动等待指标采集而是主动按预设频率扫描平台的各个层面包括服务节点健康度、接口错误率、调用链耗时分布、数据对账差异、流量形态突变等。扫描结果会被汇总到一个统一的状态模型里系统基于历史基线来判断当前的数字算不算异常异常信号再经过规则引擎研判、分级、收敛最终通过告警通道触达到对应负责人。为什么叫雷达而不叫监控平台因为它的行为模式确实更像雷达雷达不停发射波束扫描周围空域把回波和背景噪声做对比发现目标后持续跟踪。PLFM_RADAR 也是周期性地发射探测波束也就是一系列检测任务然后拿回波也就是采集到的指标与历史背景做对比。它不信任任何单点指标而是综合多个维度交叉验证降低误报率。你可以把它理解成一个带着怀疑心的观察者这份怀疑心正是普通监控工具最缺的东西。2. 架构设计与核心技术拆解2.1 整体链路从探测到触达需要经过几层PLFM_RADAR 的总体链路可以分成六层这里我用文字把每层的职责讲清楚不画图也能在脑子里拼出全景。最底下是采集层它的职责是产生和收集数据包括两类来源一类是被管服务的指标暴露接口比如 Prometheus 格式的 /metrics 端点另一类是业务侧主动上报的日志型事件比如一次调用的耗时、状态码、对账结果。采集器并不直接写入存储而是统一上报给传输层。传输层本质是一个高吞吐的消息通道我用了 Kafka原因很简单峰值时业务事件量会瞬间翻数倍只有消息队列能扛住这种毛刺而且下游消费出现问题时队列能天然做缓冲不至于直接丢数据。再往上走是处理层它负责对原始事件做清洗、聚合、标记比如把一次请求的耗时数据按接口维度聚合成 P50、P95、P99把错误日志归类成错误码集合也负责把不同来源的指标做维度对齐。清洗过后的数据有两个去向高频的时序指标进时序库完整的原始事件进明细库。这两类数据存储就是存储层。存储层之上是分析层它运行动态基线和异常检测逻辑产出某指标偏离了多少这样的结论。再往上就是触达层包括告警引擎、分级策略和通知通道最终把结论变成一条可执行的消息出现在钉钉、企微或者 IM 群里。这个分层结构并不花哨每一个互联网公司里都能见到类似的设计。但真正影响系统效果的不是分层本身而是层与层之间的数据契约和决策逻辑这恰恰是大多数实现没做好的地方。2.2 为什么时序库和明细库必须分开存在设计存储层时我踩过一个很深的坑差点把所有数据都塞进同一个库里。当时想的是数据就那些存哪里不是存结果业务量一上来就发现完全不是一回事。这里的核心冲突是访问模式不同。看趋势图、算基线、判断指标是否偏离需要的是一条 SQL 能在几十毫秒内完成对某段时间序列的聚合查询而排查一次具体事故需要的却是精确到某一秒、某一个请求参数的原始记录。前者是典型的时序场景后者是典型的日志检索场景。把两种需求强行放在一个存储系统里结果就是两败俱伤聚合查询被大量写操作拖慢原始检索又被聚合场景的预计算压缩搞得丢细节。后来我采用了双写方案时序指标进 Prometheus原始事件进 ClickHouse。Prometheus 擅长按标签聚合和高基数的时序计算ClickHouse 则擅长在超大数据集上做条件检索。两者互补各自的负担都不大。存储类型保存内容查询特征典型时效Prometheus聚合后的时序指标按维度做范围聚合、基线检索秒级出图ClickHouse完整的请求事件明细按请求ID、时间、服务名过滤秒级返回明细当然代价是要维护两套存储机器成本和管理成本都上来了。但从收益来看这个坑值得填。你要想清楚一个原则监控系统是用来救命的不是用来省磁盘的关键时刻查询效率远比存储成本重要。2.3 雷达式扫描机制是怎么实现的雷达的核心行为是转动天线、持续扫描PLFM_RADAR 把这种物理行为抽象成了调度器加探测任务。调度器像一个时钟周期性触发一批探测任务每个任务针对一个具体的检测对象。比如有一个任务是每 30 秒扫描一遍订单服务所有实例的错误率另一个任务是每 5 分钟比对一次核心库的主从数据差异。任务的粒度可以很细也可以很粗关键是要把所有任务放进一张统一的任务表里管理。这个设计的巧妙之处在于扫描任务本身也是被监控的对象。如果某个任务连续几轮都没有按时执行调度器会主动产生一条扫描器失聪的告警。这是很多监控系统特别容易忽略的地方——监控自身也可能哑火。我在这里做了双重保险调度器进程会实时上报心跳如果心跳中断超过 90 秒备用的调度节点会自动接管任务列表。这个机制后来真的救过我一次主节点因为内存溢出挂掉备用节点无缝接上团队全程没有感知。扫描任务要标准化我给每个任务定义了统一的协议探测对象、采样周期、采集协议、解析方式、结果输出格式。这样做最大的好处是新接入一个被管服务时不需要重新写采集器只需要在任务表里加一行配置。一个平台的监控覆盖度能快速扩大靠的绝不全是代码而是这套标准化能力。3. 实操记录把核心链路从零搭起来3.1 采集器实现与上报协议先看采集器。PLFM_RADAR 的采集器不是单一组件而是一组可插拔的 Python 进程按部署位置分成两种每台机器上的本地采集器以及独立运行的集中采集器。本地采集器负责收集系统资源和本地进程状态集中采集器负责按任务表主动拉取各个服务的 HTTP 指标接口。我把本地采集器上报逻辑简化成下面这个核心片段import gzip import json import socket import threading import time import requests collector_id f{socket.gethostname()}-{threading.get_ident()} def send_batch(metrics: list, endpoint: str): body gzip.compress(json.dumps(metrics).encode(utf-8)) for retry in range(3): try: resp requests.post( endpoint, databody, headers{ Content-Type: application/json, Content-Encoding: gzip, X-Collector-Id: collector_id, }, timeout5, ) if resp.status_code 200: return True except Exception: time.sleep(2 ** retry) return False def collect_and_send(endpoint: str): metrics [] metrics.append({ timestamp: int(time.time()), metric: node.cpu.usage, labels: {host: socket.gethostname()}, value: read_cpu_usage(), }) metrics.append({ timestamp: int(time.time()), metric: node.memory.usage, labels: {host: socket.gethostname()}, value: read_memory_usage(), }) # 这里继续采集磁盘、TCP 连接数等核心是保证同一批数据时间戳尽量一致 send_batch(metrics, endpoint)这个片段里有几个细节值得展开讲。第一我刻意用批量上报而不是单条上报单条上报在节点数量多时会产生巨大的连接开销批量把几十条指标打成一个 gzip 包压缩后体积大约是原来的 15% 到 20%对带宽和接收端压力都小很多。第二失败重试用了指数退避1 秒、2 秒、4 秒最多三次。这里不能无限重试因为本地采集器在故障期间会积压大量数据全量重发会导致接收端雪崩。第三点可能很少有人提同一批指标要尽量保证时间戳一致我采集的时候先把所有需要采的数据在代码里准备好再统一打时间戳。如果一条一条采一批数据的时间戳可能差了上百毫秒做基线对比时会出现微小的锯齿影响后面异常检测的判断精度。3.2 告警规则与动态基准线的计算告警是整个系统里最容易失灵的环节我的经验是固定阈值告警只适合两种情况一种是有硬性资源上限的指标比如磁盘利用率超过 90%一种是法律法规或运维规范里明确有数值要求的指标。除此之外业务指标我全部采用动态基准线来实现告警。为什么举个直观的例子一个日常 QPS 在 2 万左右的接口某个凌晨瞬时掉到 3000这类指标在流量低谷能不能算异常固定阈值根本无法回答这个问题但动态基线可以——它会拿当前值和过去同时段的正常区间做对比发现偏离了 85%再加持持续的时长判断就能准确标注为疑似异常。我实现动态基线的方式不复杂核心是存储每个指标在不同时间窗口的分位数值。简单说就是对过去 30 天的同星期、同时段、同维度数据做分位统计。为了不把存储撑爆不需要保存全量原始数据只需要每小时算出 P5、P25、P50、P75、P95 五个分位点落在 ClickHouse 的聚合表里。运行期判断时把当前值跟对应时段 P25 和 P75 组成的正常带比较。正常带内绿灯超出 P95 或低于 P5亮黄灯超出 P99 或低于 P1亮红灯。这种方式既保留了周期性的升降特征又足够简单可靠。这里我附一个小片段是告警规则表里的核心逻辑用 Python 伪代码表示def evaluate_baseline(metric_name, labels, current_value, window): p5, p25, p50, p75, p95 query_historical_percentiles( metric_name, labels, window ) if current_value p95 or current_value p5: return warning if current_value p99 or current_value p1: return critical return normal实际工程里我还给每个规则加了一个持续周期参数比如连续 5 分钟超过 P95 才触发告警。这个参数极其重要它能过滤掉大量瞬时抖动你会发现系统整体的告警精准度提升一个台阶。3.3 可视化面板的关键配置思路可视化部分我用 Grafana 改的但重点不在用什么工具而在于面板怎么排才真正有用。我把 PLFM_RADAR 的看板分成三层第一层是业务总览屏幕上能看到整条核心链路的流量、成功率、平均耗时这一层的核心是趋势不是精确数值第二层是服务健康度按服务分组展示实例数、错误率、延迟分位线第三层是实时追踪在排查事故时使用把所有相关指标按请求维度并列展示。这里有个实战心得配置监控面板时同一张图上一定要叠加所有实例的曲线而不是只画平均线。平均线是故障现场最大的迷惑源因为一台机器故障时整体平均往往只是从 80 毫秒涨到 120 毫秒眼睛根本看不出来但你把 10 台机器的曲线全部叠上去一眼就能注意到有 1 台机器的曲线高高飞起。我曾经就因为只看平均线把定位问题的时间从五分钟拖到了半小时这种感觉相信排查过线上问题的人都能体会。4. 上线之后踩过的坑与排查实录4.1 数据毛刺引发的误报警第一轮告警体验上线那天我被系统折腾了一整晚。原因很典型某个服务的指标里混进了大量瞬时毛刺一次完整请求会被拆成多个子请求记录计数器实际是重复计数的。结果就是某个接口的错误率在凌晨两点忽高忽低基线完全无法适应这种波动告警像机关枪一样响个不停。那晚我从睡梦中被叫醒第一反应是业务出大事了结果查了一圈数据源自己有问题。这个坑给我留下的教训是异常检测系统最怕的不是异常数据太少而是脏数据太多。如果数据源本身有重复计数的逻辑死角任何聪明的算法都会被带偏。解决办法分两步第一步从业务侧修复计数逻辑让数据契约干净起来第二步在采集器里加入一个毛刺过滤器在指标入库前用一分钟窗口内出现超过 3 次相同异常值的记录做标记允许查询时跳过这些点。很多团队遇到告警风暴的第一反应是调阈值这是典型的治标不治本真正的病根在数据的产生端。4.2 基线漂移和处理节假日特殊形态上线大概一个月后另一个问题冒了出来。动态基线按照过去 30 天同时段来计算一旦业务形态发生趋势性变化比如某条业务线在快速增长过去一天比前一个月每天的峰值都高系统就会疯狂误报流量异常暴涨。反过来如果业务在萎缩期真实恢复正常的上量也会被判定为异常暴跌。这就是基线漂移问题。我采取的方案是给基线增加了一个趋势校正系数而不是盲目等 90 天后再修正。具体做法是对每个指标额外保存最近 7 天的中位数值在做判断时把参考窗口分成长周期和短周期两个长周期保底短周期纠偏。我这里再补充一个很微妙的话题节假日和下雪天等特殊日期段业务形态和常规工作日完全不同。常规基线会完全失效所以我在算法里专门维护了一个特殊日期字典感知到日期属于特殊类型时自动切换到对应类型的参考基线。这个功能上线之后节假日期间的误报率降了至少 60%远超我的预期。4.3 告警风暴与收敛策略告警风暴是让所有运维人头皮发麻的问题PLFM_RADAR 也经历过。引发风暴的原因通常是一条下游链路故障导致关联的上游几十个服务同时出现错误率升高每一个告警规则都被触发于是一晚上产出上千条通知。这种场景负责的人真正需要的只有一个结论下游某个核心依赖挂了。剩下的大量信息不仅没有帮助还会把真正要看的信号淹没。解决思路是收敛链路。我在告警引擎里加入了一个收敛模块按服务调用关系建立了一张依赖图谱。一条断言的触发如果它能被更高层级的父级断言解释就会被自动抑制只保留最顶层的那个告警作为根源通知。同时我把告警消息改成优先级队列P0 通知直接进 IM 并电话P1 保留为群消息P2 只进告警列表。调度规则里有一套告警路由树有点像电路里的熔断器当某个服务实例进入断连状态就自动把这个维度下的其他告警静默掉三十分钟。这里最大的收益不是告警变少本身而是团队对告警的信任度恢复了。告警太多会导致人开始划水看到任何 P0 都默认又是误报这才是最危险的。我宁可告警少漏掉一些也要保住团队每次接到告警都会紧张这个状态。4.4 常见问题速查表上面的经验我整理成一张速查表方便像我当时一样被各种怪问题折磨的同行直接对照排查。典型现象可能原因排查思路与解决手段告警高频触发但排查无果数据源计数重复、毛刺未过滤检查业务侧埋点逻辑、在采集层增加毛刺过滤基线持续误报流量异常业务处在上行或下行趋势给基线增加短周期趋势校正系数节假日告警大面积失灵静态参考窗口无法覆盖特殊形态建立特殊日期字典切换参考基线类型告警风暴导致关键信息被淹没链路依赖没归一化建立依赖图谱启动父级告警抑制机制聚合查询越来越慢指标时序库角色被混用拆分时序库和明细库各自承担专门职责长时间无告警但事故照出监控指标维度选错了重新梳理业务核心路径增加业务状态指标扫描5. 一些延伸想法做监控系统几年我最大的感受是监控的终极目标不是让告警变少而是让团队对每个告警都认真。如果系统天天吵大家麻木了那它还不如不响如果系统静悄悄结果让故障闷声爆发那也是失职。PLFM_RADAR 想要达到的状态是一种健康的平衡——它平时像雷达一样安静地转遵守纪律但在信号真正偏离正常范围的那一刻它能够立刻让正确的人知晓并给出足够判断现场的信息。最后分享一个经验在给新项目接入监控时先想做监控的目的是什么再谈用什么工具。指标热热闹闹接了一大堆却没有一个能回答业务是否正常的问题那这份监控其实还是在裸奔。