资讯详情

资讯详情

PLFM_RADAR:轻量级平台资产监控与规则告警雷达系统实践

最近把手里这套PLFM_RADAR整理出来了顺手写一篇完整复盘。先说清楚它是干什么的——这是一个面向平台资产与信号监控的轻量级雷达系统名字取PLFMPlatformRADAR雷达的组合用来对业务平台、公开接口、页面资源、证书状态这些散落的目标做周期性扫描、变更感知和异常告警。过去我们团队盯线上资产靠的是人工巡检加微信群接龙效率低不说漏检率还高。有一次某平台页面悄悄改了核心接口的返回结构前端灰度一开全站白屏等我们发现已经是三个小时之后的事。PLFM_RADAR 解决的就是这类“没人盯着但绝对不能出事”的盲区问题把分散的目标集中管理用规则引擎自动判读信号变化变化了立刻通知人全过程不需要写一行业务代码配置完 YAML 就能跑。这个项目适合谁参考一是负责平台运维、站点可靠性SRE的工程师二是做数据采集或公开信息追踪的研发同学三是团队里承担“平台监控”职责但不想引入整套重量级 APM 体系的人。不需要你有很深的前端或大数据背景核心依赖就 Python 3.10、Redis、PostgreSQL 三件套整套部署下来三十分钟以内。下面我把设计思路、核心模块、搭建过程、踩坑记录全部分享出来代码和配置都是可直接抄作业的版本。1. 项目整体设计与思路拆解1.1 为什么叫“雷达”需求拆解与核心边界“雷达”这个意象我在起名时琢磨了很久。雷达的工作方式不是把整个天空都拍一张照片而是持续发射脉冲、接收回波、分辨哪些是目标、哪些是杂波。平台监控也是一样目标本身在变页面结构会改、接口参数会调、证书会过期真正的难点不是“扫一次”而是在持续扫描中捕捉变化并判断变化是否危险。所以 PLFM_RADAR 从第一天起就定了四条设计边界后续所有模块都是围绕这四条做的以信号为单位而不是以系统为单位。传统监控看的是主机存活、CPU 占用、内存水位这类基础设施指标PLFM_RADAR 看的是业务信号比如某个接口的响应体字段、某个页面的核心文案、某份文件的哈希值。基础设施关心“系统还在不在”雷达关心“业务的约定还成立吗”。变化不等于告警规则引擎负责判断。目标变了是常态但如果每次变化都喊人运维会被活活累死。雷达把“变化”和“异常”分开变化的原始记录全部入库只有触发了高置信度规则的变化才会升级成告警。可解释性优先。每一跳告警都必须能回溯到原始证据响应报文、匹配到的规则、评分过程一个都不能少。否则告警出来了却说不清楚为什么跟没告警一样。低接入成本。新增一个监控目标只改一行 YAML不需要开发介入。整体架构从逻辑上分成五层采集层、规则引擎层、存储层、可视化层、告警层。采集层负责发请求、收响应规则引擎层负责对响应内容做匹配和评分存储层记录每一次扫描的原始快照可视化层提供时间线回放和状态总览告警层负责把高风险信号推送到即时通讯工具。1.2 技术选型为什么是 Python Redis PostgreSQL Grafana技术栈上我没有选择重型的大数据组件理由很简单PLFM_RADAR 的核心负载是“周期性的小流量 HTTP 请求 文本结构比对”不是“海量日志流处理”Kafka 和 Flink 在这里属于杀鸡用牛刀还白白增加运维复杂度。组件选型用途选型理由语言Python 3.10采集与规则判定生态成熟asyncio 足以支撑上千并发目标扫描异步框架aiohttpHTTP 请求发送连接复用比 requests 高一个量级适合周期性批量请求缓存与队列Redis 7任务队列、结果缓存天然支持 TTL 和列表结构做分布式锁也很顺手数据库PostgreSQL 14快照与告警持久化JSONB 字段对规则匹配结果非常友好直接存 RoaringBitmap 也行可视化Grafana Prometheus指标面板与告警趋势免费、成熟、社区面板多团队里基本不用额外培训配置YAML 自研解析器监控目标与规则定义比 DB 配置更直观方便走 Git 评审和变更审计这里有一个当时花了比较多时间权衡的点为什么不直接用现成的开源探针工具坦白说现有工具在“基础设施监控”维度非常强但一旦要表达“页面 A 的促销价不能低于 199 元”“接口 B 必须返回 code0”这类业务语义就得写一堆胶水脚本。PLFM_RADAR 的规则引擎本质上是一个面向业务变化的领域语言这就是它区别于通用监控系统最大的地方。2. 核心模块拆解与实操要点2.1 采集层异步并发、限速与失败重试采集层是整个雷达的“天线”。天线设计得不好后面再强的信号处理都是白搭。我的首选方案是asyncio aiohttp semaphore用信号量控制全局并发数。这里有个常见误区很多人一上来就开几百个协程结果对方服务器直接开始甩 429 甚至封 IP。正确做法是给每个目标域单独配置rate_limit同时做全局并发兜底两层限制缺一不可。# 核心采集代码片段 import asyncio import aiohttp from dataclasses import dataclass dataclass class ScanTarget: name: str url: str method: str GET headers: dict None body: str None timeout: float 10.0 rate_limit: float 1.0 # 每域名每秒最大请求数 class RadarScanner: def __init__(self, targets: list[ScanTarget], global_limit: int 50): self.targets targets self.semaphore asyncio.Semaphore(global_limit) self.rate_limiters: dict[str, asyncio.Semaphore] {} for t in targets: domain self._extract_domain(t.url) self.rate_limiters.setdefault(domain, asyncio.Semaphore(int(t.rate_limit))) async def scan_one(self, session: aiohttp.ClientSession, target: ScanTarget): async with self.semaphore: async with self.rate_limiters[self._extract_domain(target.url)]: try: async with session.request( target.method, target.url, headerstarget.headers, datatarget.body, timeoutaiohttp.ClientTimeout(totaltarget.timeout) ) as resp: return target.name, resp.status, await resp.text() except asyncio.TimeoutError: return target.name, 0, TIMEOUT except aiohttp.ClientError as e: return target.name, 0, fNETWORK_ERROR: {e}失败重试我建议只对网络层错误重试不对 HTTP 非 2xx 状态码重试。超时、连接重置这类是“没拿到结果”重试有意义服务器返回 500、502 说明目标本身就有异常这个异常本身就是监控信号不能因为重试后拿到了一张 200 就把问题掩盖掉。实践中重试次数设为 2退避策略用固定 1.5 秒等待实测下来比指数退避在短周期扫描场景更适合因为指数退避的重试经常跨越到下一个扫描周期造成排队堆积。请求头至少要设置真实的User-Agent和Accept-Language很多平台会拦截无 UA 的请求但也不能伪装得太“浏览器化”容易被风控判定为脚本行为。我的经验是保留 Python 标识写成PLFM-Radar/1.0 (internal monitor)合规性和可联系性都强。2.2 规则引擎YAML 定义信号评分决定告警等级规则引擎是整套系统的“信号处理器”。我把它设计成三层模型规则Rule、命中Hit、评分Score。一条规则由四部分组成target目标名、type规则类型、condition匹配条件、weight权重。核心规则类型有以下几类regex正则匹配响应文本比如匹配code:0或页面 titlejsonpath从 JSON 响应中提取指定路径再对取值做比较hash对响应主体的哈希值做对比用于监控静态文件是否被篡改keyword关键词出现/消失判断比如监控页面上“断货”两个字是否出现ttl审批证书校验检查 SSL 证书剩余有效天数每条规则都有一个权重值命中一个规则就累积对应分值最终由判定函数映射成四个等级正常、观察、告警、严重。用加权的意义在于单条弱规则命中可能只是波动多条弱规则同时命中往往就是事故前兆。# rules/example_platform.yaml target: example_platform interval: 300 rules: - name: core_api_code_field type: jsonpath path: $.code condition: equals 0 weight: 30 - name: promotion_price_not_below type: jsonpath path: $.data.price condition: gte 199 weight: 50 - name: critical_keywords type: keyword keywords: [系统异常, 正在维护, 紧急通知] condition: absent weight: 40 - name: ssl_expire_days type: ttl condition: lte 15 weight: 25 judge: normal: { lt: 30 } watch: { ge: 30, lt: 60 } alert: { ge: 60, lt: 90 } critical: { ge: 90 }这个评分模型的妙处在于它的“叠加效应”单个 80 分的高权重规则命中会直接告警但更常见的情况是多个 20、30 分的规则同时打中比如页面出现维护关键词40 分 核心字段返回异常30 分 证书只剩 10 天25 分 95 分直接判定为严重。这种“多条弱信号聚合”的预警方式是雷达整体设计里最有价值的部分。规则编写有一个需要注意的细节尽量让规则匹配“业务约束”而不是匹配“当前值”。比如监控一个价格接口不要写“价格 equals 199”要写“价格 gte 199”前者在运营正常调价后会立刻误报后者只有在价格异常下探的时候才触发。监控的本质是守住底线不是复读现状。2.3 存储层快照历史与可追溯性设计存储设计上我坚持“所有原始快照全部落库”不做采样、不做聚合。为什么因为告警发生后大家最想知道的是“这个接口从哪天开始变的”如果没有历史快照这个答案永远查不出来。存储成本方面每条扫描记录平均 2~8KB一天 5000 个目标、每 5 分钟扫一轮压缩后大概 1.5GBPostgreSQL 完全顶得住。表结构核心是三张表scan_records保存每一次扫描的原始响应和响应状态rule_hits保存这次扫描命中了哪些规则alerts保存最终的告警记录。CREATE TABLE scan_records ( id BIGSERIAL PRIMARY KEY, target_name VARCHAR(128) NOT NULL, scanned_at TIMESTAMPTZ NOT NULL DEFAULT now(), http_status INTEGER NOT NULL, latency_ms INTEGER NOT NULL, response_body TEXT, response_headers JSONB, request_id UUID NOT NULL ); CREATE TABLE rule_hits ( id BIGSERIAL PRIMARY KEY, scan_record_id BIGINT REFERENCES scan_records(id) ON DELETE CASCADE, rule_name VARCHAR(128) NOT NULL, rule_type VARCHAR(32) NOT NULL, hit BOOLEAN NOT NULL, score_contribution INTEGER NOT NULL, matched_detail JSONB ); CREATE TABLE alerts ( id BIGSERIAL PRIMARY KEY, target_name VARCHAR(128) NOT NULL, level VARCHAR(16) NOT NULL, total_score INTEGER NOT NULL, alert_message TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );这里一定要给scan_records(target_name, scanned_at)建联合索引否则查询时间范围的时候全表扫描慢得离谱。另外response_headers用 JSONB 比用独立的列灵活得多因为不同平台的响应头千差万别用列存会天天因为缺字段改表结构。数据保留策略我建议热数据保留 30 天冷数据归档到对象存储保留 180 天。把过期数据定期转储成一个按日期命名的 JSONL 文件传走本地库始终保持在可控体积。3. 实操过程从零搭建 PLFM_RADAR 完整步骤3.1 环境准备与项目结构先把环境准备好。我推荐用 Docker Compose 一把梭把基础设施拉起来省去本机装 PostgreSQL 和 Redis 的麻烦。下面这段配置直接保存成docker-compose.yml即可。version: 3.8 services: postgres: image: postgres:14-alpine container_name: radar-pg environment: POSTGRES_USER: radar POSTGRES_PASSWORD: radar_pass POSTGRES_DB: radar volumes: - pg_data:/var/lib/postgresql/data ports: - 5432:5432 redis: image: redis:7-alpine container_name: radar-redis command: redis-server --appendonly yes volumes: - redis_data:/data ports: - 6379:6379 volumes: pg_data: redis_data:跑docker compose up -d之后Python 侧只需要创建一个虚拟环境安装依赖python -m venv .venv source .venv/bin/activate pip install aiohttp asyncpg redis pyyaml prometheus-client项目目录我建议按下面的结构组织目标配置、规则配置、核心代码、模块代码严格分离后续接 CI 评审也比较顺畅plfm_radar/ ├── config/ │ ├── targets/ # 监控目标 YAML │ └── rules/ # 规则配置 YAML ├── radar/ │ ├── __init__.py │ ├── scanner.py # 采集层 │ ├── rules_engine.py # 规则引擎 │ ├── storage.py # 数据持久化 │ ├── alerter.py # 告警推送 │ └── scheduler.py # 任务调度 ├── docker-compose.yml ├── requirements.txt └── main.py3.2 核心流程实现调度、扫描、判定、告警闭环PLFM_RADAR 的调度策略用最简单的固定周期轮询每个目标在 YAML 里配置自己的interval调度器用asyncio.create_task周期性启动扫描协程。这里没有用 Celery以雷达的吞吐量直接用 asyncio 就已经达到消费端极限没必要引入额外的任务队列系统。主循环的伪代码逻辑是这样的import asyncio from radar.scanner import RadarScanner from radar.rules_engine import RulesEngine from radar.storage import Storage from radar.alerter import Alerter async def scan_cycle(): scanner RadarScanner(targets) rules_engine RulesEngine() storage Storage() alerter Alerter() async with aiohttp.ClientSession() as session: results await scanner.scan_all(session) for result in results: hits rules_engine.evaluate(result) await storage.save(result, hits) alert rules_engine.judge(hits) if alert.level in (alert, critical): await alerter.send(alert) async def main(): while True: await scan_cycle() await asyncio.sleep(10) # 10 秒调度心跳 if __name__ __main__: asyncio.run(main())注意aiohttp.ClientSession必须全局复用不能每个请求都创建新的连接池否则会产生大量 TIME_WAIT 连接遇到高并发目标时直接把自己打垮。实测同一个ClientSession扫 2000 个目标和每目标新建 Session 相比系统负载能差 10 倍以上。告警推送我这里做了两级普通告警只发 Webhook 到群机器人严重级别的额外增加电话和短信兜底。群机器人用通用 Webhook 格式对齐钉钉、飞书、企业微信任何一种都行避免绑死在某个平台的 API 上。告警内容一定要带上目标名、等级、总评分、命中规则列表和响应摘要不然告警就是一条没有上下文的碎片。3.3 可视化Grafana 面板与 Prometheus 指标可视化层我用 Prometheus 提供指标Grafana 渲染面板。不需要搞一套自定义前端那样维护成本太高而且监控场景下 Grafana 图表的成熟度远超自研。在采集器里需要暴露两个核心指标radar_scan_duration_seconds单目标扫描耗时和radar_hit_total按目标名、规则名、命中状态统计的规则命中计数。from prometheus_client import Histogram, Counter, start_http_server SCAN_DURATION Histogram( radar_scan_duration_seconds, Scan duration per target, [target] ) RULE_HITS Counter( radar_hit_total, Rule hit counter, [target, rule, status] )起一个独立的start_http_server(9100)Prometheus 配置里加一条抓取任务即可。Grafana 面板我推荐至少放三块雷达总览各目标扫描状态、成功率、平均延迟、规则命中趋势哪个规则最近 24 小时命中次数变多了、告警时间线叠加上线和恢复时间。命中趋势那张图最有用——很多故障不是突然发生的而是某个规则的命中频率提前一两天就开始缓慢爬升。4. 常见问题与排查技巧实录4.1 高频问题速查表跑了一段时间后整理一份出现频次最高的坑。如果你准备照抄这套方案建议先把这张表存下来。问题现象根因解决办法扫描的响应体和浏览器看到的不一致目标平台做了 User-Agent 或 Cookie 风控补充必要的请求头和认证信息优先用平台开放接口数据库磁盘快速增长到报警response_body无索引且保留了超大响应对单条响应大小做上限截断20KB超出部分只存前 N 字节和末尾 N 字节一次目标抖动触发十几条告警没有做聚合通知按目标维度做告警聚合同一目标在 30 分钟内只发送一条合并告警规则引擎正则效率低扫一个目标要 3 秒正则回溯导致 CPU 打满用regex模块或者对响应先做长度门槛短响应直接跳过复杂规则Redis 连接数涨到上限每次创建新连接而非复用连接池全局初始化一个redis.Redis实例禁止在循环体内重复创建4.2 排障思路从告警反推问题链路PLFM_RADAR 的告警消息里我强制带上了一个request_id字段这个字段是排障的关键抓手。每次扫描开始前生成一个 UUID整个请求、响应、规则命中、告警判定全部挂在同一个request_id下数据库里按这个字段一查整条链路一目了然。实际排障最常用的一条 SQLSELECT rr.scanned_at, r.hit, r.rule_name, r.score_contribution, rr.http_status, rr.latency_ms FROM scan_records rr JOIN rule_hits r ON r.scan_record_id rr.id WHERE rr.target_name example_platform AND rr.scanned_at now() - interval 24 hours ORDER BY rr.scanned_at DESC;这能非常直观地看到一张雷达回波图哪条规则从哪个时间点开始持续命中、响应延迟曲线是不是同步变化、HTTP 状态码有没有异常波动。我遇到过几次“页面语义被改但 HTTP 状态全部正常”的隐形事故如果不看规则命中历史很难发现平台悄悄改了核心字段的取值逻辑。排查时还有一个心得告警消息里的“命中规则列表”永远比“告警文案”重要。告警文案是写给值班人看的命中规则列表是写给工程师看的后者能直接把排查范围收敛到具体字段和具体条件上省去大量盲目对比前后版本的浪费时间。4.3 参数调优并发数、扫描周期与告警阈值最后分享三个调参的实测经验这些值不是拍脑袋定的是跑了几个星期逐步试出来的。并发数从本地机器发起扫描全局信号量建议控制在 30~50。低于 30 浪费带宽高于 50 容易引发目标端限流尤其是动辄全量返回的平台接口响应体普遍在几十 KB 到几百 KB并发上去会瞬间拉爆出口带宽和本地 CPU。扫描周期核心交易链路建议 30~60 秒一次边缘页面建议 5 分钟一次完全静态的资源可以放到 30 分钟。周期设置的原则是“故障容忍时间”而不是“数据新鲜感”比如业务上允许 5 分钟内发现问题扫描周期就没必要设为 30 秒。告警阈值如果刚开始跑一周全是误报绝大多数情况是权重分设置太高把阈值本身抬高并不能解决问题反而让真正的严重故障被淹没。正确做法是保留阈值不动去调整单条规则的权重和匹配条件让规则更贴近业务约束。另外强烈建议部署时把日志打到独立的日志文件里这和业务日志分开存放。PLFM_RADAR 扫描量大日志写入频繁如果混在一起业务日志的查询会变得非常难受。我用的是logging.handlers.RotatingFileHandler按 200MB 一个文件滚动保留最近 10 个文件既方便按时间查问题又不怕磁盘被写满。5. 一些实际使用的补充体会写到这里最后补充几点我在实际使用中踩过坑之后的切身体会。雷达这类监控工具最大的价值不是它激活了多少条告警而是它在不打扰人的前提下把风险暴露时间从“小时级”压缩到了“分钟级”。部署 PLFM_RADAR 的头一个月我们一共收到 16 条告警其中 2 条是误报剩下的 14 条有 11 条在业务方自己发现之前就把问题报了出来。还有一个小技巧是给规则配置加上详细的description字段。当初觉得写说明是浪费时间后来运营同学会拿着告警截图来问“这条告警是什么意思”的时候才意识到规则命中的原始信息如果不写清楚业务语义别人根本没法介入处理。现在我把每条规则的描述都写成“如果这个规则命中意味着什么、应该找哪个团队、历史发生过什么问题”相当于把值班知识库固化到配置里。如果你也想搭一套类似的轻量雷达系统我建议不要一上来就贪多先把一两个核心接口的字段校验和证书监控跑通再逐步扩展。PLFM_RADAR 的整个实现其实不复杂但把“变化感知、规则判定、告警通知、历史回溯”这条链路做好做到位收益会远超预期。后续如果时间允许我还会把规则可视化调试界面补上让运营同学也能自己配置监控规则。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →