☰
自研平台监控雷达PLFM_RADAR:从业务探测到告警治理的工程实践
2026/11/2 14:52:12 网站建设 项目流程

半夜两点半,手机疯狂震动。接起来对面是商务同事的声音:“客户反馈我们平台下单页打不开了,现在线上在推活动,流量很大,你赶紧看一眼。”

我爬起来打开电脑,跳板机连上去查了一圈服务器负载、进程状态、数据库连接数,一切正常。最后才发现是 Nginx 上游请求大量超时,而我们的监控系统只看机器层面的指标,压根没覆盖到“业务入口是否可用”这一层。也就是说——我的平台可能已经“看不见”了,但雷达上还是一片平静。

那次之后我花了大概两周时间,把手里零散的脚本、定时任务、第三方检测工具全扔到一边,从零搭了一套自己命名的平台监控系统,名字就叫PLFM_RADAR。PLFM 取自 platform,RADAR 就是雷达。它做的事很简单:把分散的平台状态信息汇总起来,实时探测、周期性巡检、异常自动告警,最终形成一张“平台健康雷达屏”。

这篇文章就聊聊 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_RADAR

4.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_redirects=True) 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"] = f"request_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(timeout=3.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, maxlen=6): self.maxlen = maxlen self.window = defaultdict(lambda: deque(maxlen=maxlen)) 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, json=msg, timeout=5) 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", seconds=30)

加了“上一轮还没跑完就跳过这一轮”的防堆积逻辑之后,任务漂移问题彻底消失。宁可丢掉一轮采集,也不允许定时器越跑越偏。

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 弹性调度,彻底解决单点采集器挂掉的风险。

最后再分享一个小建议:如果你也要自研一套类似的雷达系统,先别急着写代码,花两天时间把“到底哪些指标真的能代表平台健康”这个问题想清楚,比选什么数据库、用什么框架都重要。指标选错了,系统做出来也只是一个漂亮的报警器,跟平台的真实状态没什么关系。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询