
半夜两点半手机疯狂震动。接起来对面是商务同事的声音“客户反馈我们平台下单页打不开了现在线上在推活动流量很大你赶紧看一眼。”我爬起来打开电脑跳板机连上去查了一圈服务器负载、进程状态、数据库连接数一切正常。最后才发现是 Nginx 上游请求大量超时而我们的监控系统只看机器层面的指标压根没覆盖到“业务入口是否可用”这一层。也就是说——我的平台可能已经“看不见”了但雷达上还是一片平静。那次之后我花了大概两周时间把手里零散的脚本、定时任务、第三方检测工具全扔到一边从零搭了一套自己命名的平台监控系统名字就叫PLFM_RADAR。PLFM 取自 platformRADAR 就是雷达。它做的事很简单把分散的平台状态信息汇总起来实时探测、周期性巡检、异常自动告警最终形成一张“平台健康雷达屏”。这篇文章就聊聊 PLFM_RADAR 的设计思路、核心实现、踩过的坑以及一路走下来的真实体会。如果你也在维护一个业务平台或者正在为“监控覆盖不全面、告警滞后、误报满天飞”头疼那这篇文章应该能给你一些可以抄作业的参考。1. 为什么不能只靠现成监控工具1.1 市面监控工具的边界在哪说到平台监控很多人第一反应是 Prometheus Grafana或者云厂商自带的基础监控告警。这些工具确实解决了很多问题CPU、内存、磁盘、网络这类基础设施指标它们做得非常成熟数据采集、存储、展示、告警一条龙。但问题在于基础设施监控覆盖不了“业务结果指标”。服务器负载正常不代表订单能正常提交数据库连接数正常不代表核心接口的 P99 延迟还扛得住磁盘剩余 40%不代表定时任务没有因为数据倾斜而卡死。而真正的线上事故恰恰往往先反映在业务指标上而不是物理指标上。我在前面提到的那个凌晨的事故就是典型例子——服务器层面一切健康但用户已经进不了下单页了。PLFM_RADAR 想做的是补足这个“最后一公里”把平台对外表现的真实状态探测出来而不是仅仅告诉运维“机器活着”还是“机器死了”。1.2 现成方案为什么还不够用当时我也调研过一些开源的探针、监控框架比如基于 Java 的 APM 系统、基于云厂商的拨测服务等等。它们各有优势但也各有各的问题配置重一套 APM 系统真正跑起来动辄要上探针、改代码、搭链路追踪对整个研发团队的侵入性太大。成本高云拨测按次数计费覆盖的节点和频率上去之后费用并不低而且默认探活逻辑比较浅拿不到业务维度的数据。不灵活平台方给的检测周期、检测协议都是固定的想自定义检测脚本、自定义判定逻辑往往得等版本迭代。说到底我需要的是一个“我可以完全掌控每一层逻辑”的系统探测目标我自己定义判定算法我自己写告警通道我自己接采集频率和重试策略全部自己说了算。这就是 PLFM_RADAR 立项的第一原则——可定制性优先。2. 整体架构设计搭积木式组装一套雷达2.1 四层结构各干各的PLFM_RADAR 从顶层设计上分成四块采集层、缓冲层、检测与存储层、告警与展示层。每一层相互独立通过标准接口对接这样既方便局部替换也方便故障隔离。采集层负责所有外呼动作。定时探测健康检查接口、抓取业务页面关键字段、查询核心数据库业务指标。用 Python asyncio 或者 Go goroutine 做并发避免串行等待瓶颈。缓冲层采集结果统一写入 Redis 队列后端检测模块异步消费。这样采集任务的返回时长波动不会直接堵住数据库写入也方便做削峰填谷。检测与存储层从队列中取数据判定“正常 / 异常 / 未知”写时序数据库做历史留存。这个层是雷达的“大脑”。告警与展示层检测层给出异常信号之后通过回调把事件推给告警通道企业微信、邮件、钉钉均可同时把实时指标推给 Grafana 展示。这个结构最直接的好处是哪天你觉得检测算法不够好了只改检测模块就行采集探针、告警通道完全不用动。反过来如果探测目标变了也不影响历史数据存储和告警逻辑。2.2 采集层的选型协程比线程好用太多采集层一开始我用的是 Python 的 threading requests每个探测目标一个线程。跑了一阵发现两个问题一是线程数量一大GIL 和上下文切换开销非常明显调度出现了延迟二是一个目标网络超时它会占着一个线程池的 worker 不放后面请求全部排队。后来我把整个采集层重写成了asyncio httpx的异步模型。单进程内用协程数代替线程数同时挂起的连接数可以做到几千个而 CPU 占用反而比之前低。用 httpx 的AsyncClient做 HTTP 探测把timeout统一设置为 3 秒超时的请求就直接记“不可用”不再傻等默认的 30 秒。这个改动带来的直接变化是原先跑一轮全量探测大概 300 个目标需要 40 多秒现在压缩到了 5 秒以内。对于雷达系统来说这个差距很关键——你只能在“感知异常”的速度比“故障扩散”的速度更快时预警才有意义。2.3 存储选型时序数据库才是正确姿势第一版我图省事把采集结果直接写 MySQL 一张大表字段就是 target、metric、value、timestamp、status。数据量上去以后马上难受了每天几百万条指标记录查询最近一周的趋势要扫上千万行索引建了也不好使因为查询条件总是跨时间和维度组合。后来我把存储切成了VictoriaMetrics这是一个兼容 Prometheus 协议的时序数据库。选择它而不是 InfluxDB原因有三VictoriaMetrics 单节点版部署极简单一个二进制文件跑起来就行不用 Java 环境。存储压缩率比 InfluxDB 好同样的数据量磁盘占用大概少一半。自带 PromQL 查询语法后续接 Grafana 不需要额外的适配层。实际用下来最直观的感受是以前 MySQL 里那条“查最近一小时每分钟可用率”要 800 毫秒的 SQL在 VictoriaMetrics 里用avg_over_time一行查询就出来了响应在几百毫秒到几十毫秒之间。监控系统本身对数据查询的实时性要求很高这一步选型非常关键。3. 核心功能落地判定逻辑与告警策略3.1 三种检测模式对应不同的真实场景雷达的核心不是“采集数据”而是“判断异常”。PLFM_RADAR 里我内置了三种判定模式覆盖绝大多数平台监测场景连通性检测。最简单也最基础对目标 URL 发起 HTTP 请求只要返回的状态码在 200 和 399 之间就算存活否则就判定异常。这个模式适合探测外网入口、网关地址、静态资源服务。内容关键字检测。有些平台的核心页面是软渲染出来的后端服务挂了页面还是能返回 200但页面里已经只剩下报错文案。这时候连通性检测完全失效。内容检测的做法是拿返回的 HTML 或 JSON 实体匹配预先设置的“业务存活关键字”——比如下单页必须包含某个表单元素或者登录接口必须返回某个固定字段。匹配不到就直接判异常。业务指标检测。这是最能发现问题、也最难做的模式。它需要定时从数据库或接口里拉取业务指标比如订单量、支付成功率、PV/UV、队列积压数。PLFM_RADAR 支持写一段自定义 SQL 或脚本周期性执行后把结果上报到检测模块。检测模块拿到数字之后再走阈值规则判断是否异常。三种模式没有高低之分实际使用中我通常是叠加布置的同一个核心平台既做每 30 秒一次的连通性探测也做每 5 分钟一次的内容关键字巡检再加每 10 分钟一次的业务指标拉取。3.2 告警阈值怎么写才不让人“狼来了”告警最怕的是两件事该报的不报不该报的乱报。后者引起的“告警疲劳”甚至比前者危害更大——一个群里的告警一天响五十次到第六十次真正出事的时候就没人看了。我踩过这个坑所以 PLFM_RADAR 里的阈值判定设计了两个很关键的参数连续失败次数N和确认窗口T。连续失败次数目标必须连续 N 次探测失败才真的进入待告警状态。比如单次超时可能是网络抖动造成的连续 3 次每隔 30 秒一次都失败那大概率不是抖动了是确实有问题。确认窗口进入待告警状态之后在 T 分钟内有任何一次成功状态自动恢复不算故障。设置成 2 分钟的话可以抵消掉大部分瞬时故障。这两个参数配合使用实测可以把误报率从最初的 40% 降到 5% 以下。代价是告警延迟会增加一两分钟但对于绝大多数平台故障场景来说这个延迟完全可以接受。真正需要秒级感知的场景更合适的做法是直接去拍基础设施指标而不是靠业务雷达。3.3 基线漂移用历史数据兜底固定阈值最大的问题是不同平台不同时间段的正常值范围可能天差地远。比如支付成功率平时 99.9%某天突然掉到 95%看起来还是“正常”但已经属于重大异常了。反过来一个日活不过千的小平台订单量从 100 涨到 300翻了三倍但离“阈值上限”还很远可能根本没人在意它正在被流量冲垮。为了解决这种场景PLFM_RADAR 里加入了动态基线检测对每个业务指标自动计算前 7 天相同时段比如每天 14:00-14:10的均值和标准差σ然后把当前值和均值做比较。偏差超过 3σ 就触发预警低于 2σ 就自动解除。这个算法的好处是零干预、自适应不需要我为每个指标去人工定阈值。坏处是它要求历史数据质量不能太差——如果前 7 天本身就频繁抖动标准差会很大异常被淹没在里面。所以在实际落地时我是“固定阈值 动态基线”双轨并行任一触发都会生成告警事件再由事件置信度决定是否推送负责人。4. 实操记录从零到一部署 PLFM_RADAR4.1 环境准备与目录规划这套系统我当时部署在一台 4C8G 的云服务器上操作系统 Ubuntu 22.04。虽然是小规格但跑完整套服务绰绰有余。目录结构我保持得很简单基本就是“一个主程序 两个子模块 配置和日志分开”/opt/plfm_radar/ ├── collector/ # 采集器负责探测和数据采集 ├── detector/ # 检测器消费队列做异常判定 ├── notifier/ # 告警通道封装 ├── config/ │ ├── targets.yaml # 探测目标配置 │ └── settings.yaml # 系统级参数 ├── logs/ └── main.py # 协同控制入口依赖方面Python 环境用 3.10 poetry 管理依赖包主要是httpx、apscheduler、redis、prometheus-client、sqlalchemy、pydantic。VictoriaMetrics 用官方提供的单节点二进制跑 systemd 服务Redis 直接用 apt 源装的最新 7.x。4.2 采集器核心代码实现采集器的主体逻辑不复杂关键点在于异步并发控制。我先贴一下健康检查探针的简化版本import asyncio import httpx from typing import Dict, List async def probe_http(target: dict, client: httpx.AsyncClient) - dict: 连通性探测 url target[url] expected_status target.get(expected_status, [200, 301, 302, 399]) result { target: target[name], type: http, timestamp: None, ok: False, latency_ms: 0, status_code: None, msg: , } try: start asyncio.get_event_loop().time() resp await client.get(url, follow_redirectsTrue) result[latency_ms] round((asyncio.get_event_loop().time() - start) * 1000, 2) result[status_code] resp.status_code result[ok] resp.status_code in list(range(200, 400)) result[msg] resp.text[:80] if not result[ok] else except httpx.TimeoutException: result[msg] timeout except httpx.RequestError as exc: result[msg] frequest_error: {exc.__class__.__name__} result[timestamp] int(asyncio.get_event_loop().time()) return result async def run_probe_batch(targets: List[dict]): 并发跑一批探测任务限制信号量为 50 semaphore asyncio.Semaphore(50) async with httpx.AsyncClient(timeout3.0) as client: async def guarded(target): async with semaphore: return await probe_http(target, client) results await asyncio.gather(*[guarded(t) for t in targets]) return results这里有两个细节值得多说一句。一是用Semaphore(50)限制并发避免目标服务那边看到请求洪峰也避免本机连接数被撑爆二是超时时间设 3 秒而不是默认的更长值因为对于监控系统来说响应超过 3 秒本身就已经是“可用性劣化”的信号了再等下去没有意义。4.3 检测器的滑动窗口判定检测器从 Redis 队列拉取采集结果后要做的第一件事不是直接判定而是按照目标维度维护一个滑动窗口队列。窗口里保留最近 N 次探测结果只有当 N 次里失败次数达到阈值 M 时才进入告警状态。下面是我实现这个逻辑的核心片段from collections import deque, defaultdict class FailureCounter: def __init__(self, maxlen6): self.maxlen maxlen self.window defaultdict(lambda: deque(maxlenmaxlen)) def add(self, target: str, ok: bool): self.window[target].append(1 if ok else 0) def fail_times(self, target: str) - int: return self.maxlen - sum(self.window[target]) def should_alert(self, target: str, threshold: int 3) - bool: # 只有窗口中样本填满了才启动判定 if len(self.window[target]) self.maxlen: return False return self.fail_times(target) threshold注意这个should_alert里加了一行“样本不满不判定”的逻辑。这个非常关键系统刚启动时窗口里都是空的如果立刻开始算失败率那前几次探测只要有一次超时就会误报。加了样本量保护之后系统启动阶段会静默观察很快窗口填满后续判定就完全基于真实滑动窗口了。4.4 告警通道封装一个接口覆盖所有通知方告警通道我封装成了一个简单的策略类核心接口就一个send(event)。企业微信、邮件、钉钉分别实现这个接口后续想接 Webhook 或者电话告警也只用在 notifier 包里加一个类。企业微信机器人是当时用得最多的通道因为响应快、手机端弹窗明显。发消息格式大概长这样import requests class WecomNotifier: def __init__(self, webhook_url: str): self.webhook_url webhook_url def send(self, event: dict) - bool: msg { msgtype: text, text: { content: ( f[PLFM_RADAR] 平台异常\n f目标: {event[target]}\n f类型: {event[type]}\n f连续失败: {event[fail_count]} 次\n f探测时间: {event[ts]} ) } } resp requests.post(self.webhook_url, jsonmsg, timeout5) return resp.status_code 200每次告警尽可能带上“目标、类型、失败次数、时间”这几个要素。不能只写一句“平台异常请检查”不然收到消息的人根本不知道从哪查起。好的告警文案本身也是一种排障效率。4.5 部署后的大屏Grafana 联动最后展示层我直接用了现成的 Grafana没有自己写前端页面。VictoriaMetrics 天然支持 Prometheus 兼容的查询接口所以 Grafana 数据源填一下 VictoriaMetrics 地址就行。我在 Grafana 里主要配了几张面板整体存活率热力图按探测目标分组显示最近一小时每个目标的可用状态绿红两色一眼看清哪里有问题。响应延迟分位图采集器上报的latency_ms数据按分钟做 p50/p95/p99 曲线发现延迟劣化趋势。告警事件时间线把每次告警事件画到时间轴上能看到故障的持续时间间隔和频次。这套大屏日常就投在团队办公室的横屏显示器上屏幕变红说明线上有情况不需要有人盯着日志看信息就会自己浮现出来。5. 踩坑记录与问题排查实录5.1 采集任务堆积单线程定时器不够用第一版采集器我用apscheduler的 BackgroundScheduler 直接跑定时任务每个任务单独执行一轮全量探测。实测下来发现一个问题由于单轮探测已经很快了大多数时候不会出问题但只要某个目标响应慢阻塞了协程调度后续任务的执行时间就开始漂移。累积到高峰时段本来每 30 秒一次的探测实际变成了 1 分钟甚至 90 秒一次雷达的实时性大打折扣。排查思路是给定时任务加了独立的执行锁和超时保护from apscheduler.schedulers.asyncio import AsyncIOScheduler import asyncio scheduler AsyncIOScheduler() running_task None async def safe_scan_wrapper(): global running_task if running_task and not running_task.done(): # 上一轮还没跑完这一轮跳过 return running_task asyncio.create_task(perform_scan()) try: await running_task except Exception: # 采集异常不能影响调度器本身 logger.exception(scan task failed) scheduler.add_job(safe_scan_wrapper, interval, seconds30)加了“上一轮还没跑完就跳过这一轮”的防堆积逻辑之后任务漂移问题彻底消失。宁可丢掉一轮采集也不允许定时器越跑越偏。5.2 误报率太高就差一个抑制逻辑系统刚上线那阵子企业微信群里一天能响四十多次。我复盘了一下告警记录发现大量误报集中在两类情况某台执行业务回执的机器每天凌晨有一小段 GC 停顿接口响应偶尔超过了 3 秒超时时间。某个下游服务提供商每周会有固定几分钟的入口防火墙策略更新导致探测请求被短暂丢弃。这两类都属于“知道会发生、但不可容忍长期存在”的波动——真实的业务没有断只是偶尔抖了一下。对策就是在告警层面加抑制窗口和冷却时间同一目标同一类型告警进入待告警状态后冷却 15 分钟冷却期内即使持续异常也不会重复推送消息。如果故障时间超过 30 分钟自动升级一条“持续异常”提醒换一种颜色或标题重新推送一次。加了冷却之后告警从一天四十次降到一天个位数每条消息的打开率、重视程度都明显上来了。这是我从“能发生告警”到“有效告警”这一步踩的最值得的一个坑。5.3 时间序列库选型折腾了一圈前面说存储用了 VictoriaMetrics其实在那之前我先试过 InfluxDB 1.8 的单机版。理由很简单网上推荐多、上手快自带 Web 控制台。但跑了大概一个月我遇到了两个问题一是 InfluxDB 的索引在高基数场景下膨胀非常快。平台探测目标有几百个如果每个目标都带若干标签维度单机版内存占用直线上升动不动 OOM。二是 InfluxDB 重启恢复耗时太长有时要等十来分钟才能重新接受写入。监控系统本身需要高可用存储层一个宕机恢复要这么慢实际是很难接受的。切到 VictoriaMetrics 之后这两个问题都消失了。它的写入路径和索引设计对“大量时间序列、低写入频率”这种监控场景更友好而且单机版就能撑起我们日增几百万数据点的规模成本几乎可以忽略。5.4 目标地址变化要主动感知还有一个容易被忽略的坑探测目标的地址不是一成不变的。比如走网关的服务后端换了端口或者 CDN 回源地址调整配置如果不跟着改雷达就一直对着一堆无效地址空转。有次平台切了新的负载均衡旧地址还能响应但已经是残血状态探测结果“正常”实际流量早就走了新链路。我的做法是给配置中心加了一个“资产自动同步”任务每天凌晨从 CMDB 拉一次最新目标列表和新旧配置对比自动增加新上线的探测目标自动标记不再存在的目标为废弃状态但保留一段观察期。这样即使有人忘了手动改配置雷达系统也会在第二天自动感知变化不会一直对着历史地址自嗨。6. 个人体会与后续扩展PLFM_RADAR 从立项到今天给我最大的收获其实是监控系统的价值并不能用“功能多不多”来衡量而是体现在发现问题的速度和准度上。功能再多产生一堆没人看的告警也只是数字垃圾功能再简只要能在故障造成更大影响前让你接到一条清晰可执行的消息就有意义。根据我自己的使用体验后续想继续做几个方向一是把检测算法从“固定阈值 简单基线”升级成更细粒度的时序预测比如用 Prophet 做短周期波动预测二是增加更多告警手段比如接入电话告警和值班排班表让深夜故障也能有可靠兜底三是把采集器打包成容器配合 Kubernetes 的 CronJob 弹性调度彻底解决单点采集器挂掉的风险。最后再分享一个小建议如果你也要自研一套类似的雷达系统先别急着写代码花两天时间把“到底哪些指标真的能代表平台健康”这个问题想清楚比选什么数据库、用什么框架都重要。指标选错了系统做出来也只是一个漂亮的报警器跟平台的真实状态没什么关系。