PLFM_RADAR这个代号,是我给自己最近做的一套平台数据监测系统起的名字。PLFM取Platform,RADAR则跟雷达的语义完全一致——持续发射扫描信号、接收回波、识别异常。这套系统的应用场景很简单:我需要同时盯住几个平台的公开信息,包括商品价格、库存状态、榜单排名、规则公告,还有我们自己的运营数据,靠人肉刷新根本盯不过来,于是就有了这个"平台雷达"。如果你也在维护数据类服务、做竞品监测,或者纯粹想摆脱"定时刷网页"这种原始操作,这篇文章应该能给你一份可以照着抄的作业。
整套系统从立项到跑通,前后用了三周左右,中间推翻过一次重来。现在回头复盘,踩过的坑基本都集中在采集频率控制、信号去重、告警风暴这三件事上。我会把架构设计、关键代码、参数取舍、排障方法都拆开讲,尽量说清楚每一步背后的为什么,而不只是给结论。
1. 需求拆解:平台雷达到底要"看"什么
1.1 一句话说清楚这个项目
PLFM_RADAR本质上是一个把"周期性外部数据采集+指标异常识别+多渠道消息推送"串起来的监控系统。它解决的问题非常具体:过去运营同学每天上班第一件事是打开十几个网页,逐个看竞品价格有没有变、榜单有没有波动、平台有没有出新规则。这种纯手工操作效率低、容易漏,而且人眼很难发现缓慢的趋势变化——比如某个关键词排名连续七天每天跌两名,到第八天才发现已经跌出首页了。
雷达系统把这件事自动化:每个数据源按照预设频率扫描一次,采集到的数据进入存储层,分析层计算各种派生指标并与历史基线比对,一旦触发告警规则,就通过飞书、钉钉或企业微信的webhook把消息推给对应的人。重点不是抓取所有数据,而是抓"有用的、会变化的、能触发决策的"数据。
这个项目比较适合三类人参考:一类是运营或产品负责人,想做自己的轻量级数据监控;一类是后端开发,想看看一个完整的采集告警链路怎么落地;还有一类是独立开发者,想给自己的多个服务加一个统一观测入口。技术难度其实不高,难的是对需求的收敛和参数的调优。
1.2 为什么最终选了Python技术栈
选技术栈这件事我纠结了两天,对比过Node.js和Go。Node.js在异步IO和高并发上有优势,Go在性能和部署上更省心,但最后我还是选了Python,理由有三条。
第一,生态匹配度最高。采集层有requests、httpx、BeautifulSoup、playwright;分析层有pandas、numpy;调度层有APScheduler;展示层有Streamlit和Grafana客户端。全部是Python生态内已经验证过的方案,不用跨语言拼装。第二,迭代速度快。这个项目最核心的价值在分析规则的调整上,Python这种动态语言改完就能跑,不需要编译重启的负担。第三,团队里其他同学也会看代码,Python对他们来说是最低门槛的语言。Go和Node.js在性能上确实更好,但一个低频率的监测系统根本到不了性能瓶颈,大多数采集任务每天也就执行几十次到几百次,用Python绰绰有余。
顺便说一句,如果哪天真到了需要高并发的场景,Python也留了后路:把采集器单独拆成服务,用Celery做分布式任务队列,或者用FastAPI暴露接口交给其他语言调用。前期不需要把架构搞得太重,够用就好。
1.3 数据源的边界设定
做监测系统第一件事不是写代码,而是把"哪些数据可以采、哪些不能采"界定清楚。我的原则是三条:只采集公开可见的信息,不碰任何需要登录凭证才能访问的数据;严格遵守平台的访问频率限制,不给对方服务器增加压力;优先使用官方提供的API,其次再考虑页面解析。
这个原则很重要,因为合规性是整套系统的地基。数据源的范围直接决定了采集层怎么写,也影响了后续规则引擎的复杂度。我在PLFM_RADAR里把数据源分成了四类:价格与库存类、内容与榜单类、公告与规则类、自有业务数据类。每一类有不同的采集频率和解析方式,比如价格类需要小时级的监控,公告类反而半天扫一次就够了。先把边界定清楚,后面才不会写出一堆用不上的采集器。
2. 架构设计:从采集到告警的完整链路
2.1 四层架构与数据流向
PLFM_RADAR的整体架构我按数据流向分成了四层:采集层、存储层、分析层、展现与告警层。每层只和相邻层通信,接口定义清楚之后,各层都可以独立替换。
采集层是雷达的"发射端",负责按照调度策略访问各数据源,拿到原始数据后做初步清洗,统一成JSON结构写入存储层。每个数据源对应一个采集器插件,插件内部只负责"获取+解析",不关心数据处理逻辑。存储层我选了PostgreSQL,主要存两类内容:原始快照数据和派生指标数据。原始数据保留30天,派生指标保留90天,超期数据由定时任务清理,避免表无限膨胀。
分析层是雷达的"信号处理器",它周期性从存储层读取数据,完成三类任务:第一是计算派生指标,比如价格变化率、排名走势、榜单词频;第二是和基线做对比,判断当前值是否落入了正常波动范围;第三是触发规则引擎,输出告警事件。展现与告警层在架构上是分开的,展现部分用的是Grafana看板,告警部分通过一个统一网关把消息路由到飞书、钉钉、企业微信或邮件。分两套子系统是为了避免"看板把告警接口拖挂了"这种低级事故。
2.2 关键设计决策的取舍理由
先说存储选型。好友推荐过SQLite和MySQL,我都认真想过,最后仍然坚持用PostgreSQL。SQLite确实零运维,但它的并发写性能比较差,采集器每隔几秒就要写入数据,后期一定会卡。MySQL虽然成熟,但PostgreSQL在JSON字段支持、窗口函数、时序处理上对数据分析类任务更友好。比如我需要算percent_rank()、lag()这类分析函数,PostgreSQL原生支持得很舒服,MySQL 8.0以后虽然也有了,但部分语法细节还是有差异。监测系统最怕分析SQL写起来别扭,所以存储层投资值得。
再说消息队列。很多同类项目一上来就引入RabbitMQ或Kafka,我最后没加。原因是当前规模根本不需要异步削峰——采集任务低频率运行,分析任务以分钟级触发,直接同步调用就够了。引入消息队列反而增加运维负担,还要处理消息积压、消息丢失一堆破事。我的原则是:架构复杂度永远晚于业务需求出现,等采集源超过50个、单轮采集超过5分钟,再考虑加队列也不迟。
最后是配置管理。所有采集源的URL、频率、解析规则、告警阈值,我都放在一个YAML配置文件里,程序启动时加载到内存。之所以不用数据库存配置,是因为配置文件可以用Git管理,改版有记录、出问题可以回滚。这点在多次调整参数后体现出了巨大价值。
3. 核心模块:采集、规则与告警的核心细节
3.1 采集层实现要点与调度参数
采集层是雷达的地基,它的设计直接决定了数据质量和系统稳定性。我拆成三个子模块:调度器、采集器插件、数据清洗管道。
调度器用的是APScheduler的CronTrigger,配置示例大概是:
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.cron import CronTrigger scheduler = BackgroundScheduler(timezone="Asia/Shanghai") # 价格类数据源:每小时整点采集,错开前5秒 scheduler.add_job( collect_price_job, CronTrigger(hour="*", minute="0", second="5"), id="price_collector", max_instances=1, coalesce=True, ) # 公告类数据源:每30分钟采集一次 scheduler.add_job( collect_notice_job, CronTrigger(minute="*/30", second="15"), id="notice_collector", max_instances=1, coalesce=True, ) scheduler.start()这里有个细节:max_instances=1和coalesce=True必开。前者防止上一次任务还没跑完就把下一次任务拉起来,后者是把错过的任务合并成一次执行。对监测系统来说,丢一个周期不可怕,可怕的是任务堆积导致采集频率变得不可预期。
采集频率怎么定?我的经验公式是:单个数据源最低频率不要低于对方平台允许阈值的四分之一,给自己留出4倍余量。比如某个商品列表页的接口明显是5秒限制一次,那我们就至少间隔20秒以上。整套系统里最频繁的是价格类数据,每小时扫一次,其他类型数据源的频率都在30分钟以上。实际上这个频率对运营决策来说绰绰有余——价格变化很少在十分钟内发生两次,榜单数据的更新周期通常是按天算的。
数据清洗管道是容易被人忽略的地方。原始数据结构五花八门,有的平台返回JSON,有的平台返回的是嵌在HTML里的动态数据,还有的是经过混淆的编码字段。我在清洗管道里做了三件具体的事:统一字段命名格式为snake_case、统一时间字段为ISO 8601字符串并附上时区、统一价格单位到"分"以避免浮点数误差。最后这一点特别重要,所有金额在内部全部转成整数分存储,只在展示层转回元。这样做的原因是浮点数比较大小会有精度问题,做历史趋势分析时会出现"1.9999元"这种脏数据。
3.2 分析层的信号识别逻辑
分析层是这套雷达里最需要"调"的部分,刚开始我用的是最简单粗暴的固定阈值,比如"价格低于100元就告警"。跑了一周后发现这种规则误报率相当高——有些商品平时就是130元上下波动,偶尔促销打折到99元,这根本不是异常,但我们每次都报警,时间长了同事直接无视所有通知。
后来我把规则改成了两层:先计算历史基线,再用偏离度触发告警。历史基线的计算很简单,取过去7天同时段的均值作为基线,再计算标准差,当当前值偏离超过2个标准差时触发关注级告警,超过3个标准差才触发严重告警。用标准差而不是百分比的好处是它能自适应不同数据的波动特性:价格本来就波动大的品项,它的标准差也大,就不容易误报;而平时一直很稳定的指标,稍微动一下就会被捕获。
排名类数据的信号识别用了另一种思路,核心指标是"跨天位次变化":
def ranking_shift_analyzer(current_rank, last_rank, history_ranks): # 计算7天内的排名中位数作为基准 median_rank = sorted(history_ranks)[len(history_ranks) // 2] if current_rank == 0: return none # 数据缺失不分析 if last_rank == 0 or median_rank == 0: return "new_entry" shift = current_rank - last_rank if abs(shift) >= 5 and current_rank <= median_rank * 0.6: return "significant_up" if shift >= 10 and last_rank <= 5: return "rank_drop_danger" return "normal"原理是通过中位数做稳健基准,避免极值把均值拉偏。比如某个词的历史排名是第3、4、5、50、6、5,均值会被50这个异常值拉高,中位数就不会。这样设计后,排名监测识别"新上榜""显著上升""危险下滑"三类信号,命中率比固定阈值提高了一倍多。
公告类数据的分析逻辑最简单:用关键词和文本指纹去重,只推送新增的规则条目。实现方式是维护一个长度为64的simhash,比对两两之间的汉明距离,距离小于3就认为是重复内容。这个方法在处理"平台更新了公告但只是改了个别措辞"时很有效。
3.3 告警分级与通道路由
告警这件事做得不好,比不告警还糟糕。我在PLFM_RADAR里做了三个级别,对应不同的紧急程度和处理方式。P0是严重故障级,比如核心数据源连续3次采集失败、关键指标价格跌幅超过5%或出现页面结构重大变更,这类告警直接推送到飞书群的@所有人,同时发邮件给值班同学。P1是关注级,比如排名突降、库存告急,推送到工作群里但不@所有人。P2是信息级,只写入告警事件表,第二天早上同步进日报邮件。
通道路由方面,飞书、钉钉、企业微信都支持webhook机器人,我的统一网关就是一层简单的HTTP封装:
def send_alert(event): webhook_map = { "feishu": config["alert_channels"]["feishu_url"], "dingtalk": config["alert_channels"]["dingtalk_url"], "email": config["alert_channels"]["smtp_host"], } if event["level"] == "P0": payload = build_feishu_card(event, mention_all=True) post_webhook(webhook_map["feishu"], payload) if event["level"] in ("P0", "P1"): payload = build_dingtalk_markdown(event) post_webhook(webhook_map["dingtalk"], payload) if event["level"] == "P2": append_to_daily_digest(event)这里有个很容易踩的坑:不要把告警逻辑和业务逻辑耦合在一起。我最初偷懒直接在分析函数里调用发送函数,结果有一次发布代码时改了发送逻辑,导致所有告警静默了12个小时。后来把告警独立成一个事件模块,分析层只往告警表里写记录,再由另一个定时任务消费告警表做分发,彻底解耦。这多花了一天时间,但换来的是后续所有改动都变得安全了。
3.4 可视化看板的落地方式
可视化我用的是Grafana加PostgreSQL数据源。告警系统可以没有看板,但没有看板的雷达等于盲飞,特别是当你想复盘某个时间段到底发生了什么时,有图表一眼就能看出问题。
Grafana里我配置了两类看板:一类是"信号概览",展示各数据源最近24小时的采集成功数、失败数、平均耗时、告警事件数量;另一类是"指标趋势",按数据源维度展示价格、排名、词频的历史曲线和告警时间点标记。
配置Grafana不需要写代码,关键是在SQL里把时间字段格式处理好。我们的时间统一存成timestamptz类型,Grafana只需要指定timeField就能正确识别。还有一个小经验:在采集成功数的地方加一个阈值线,连续失败超过3次就显示红色,这样值班同学打开看板第一眼就能判断系统健康状态。
4. 实操实录:从零搭建PLFM_RADAR
4.1 环境准备与目录结构
我用的是最简单的部署方案:一台2核4G的云服务器,Ubuntu 22.04,上面跑Docker容器。整套系统分三个容器:plfm-db跑PostgreSQL 15,plfm-app跑采集与分析的Python应用,plfm-grafana跑可视化。docker-compose是核心编排文件,就不贴完整代码了,重点说目录结构:
plfm_radar/ ├── app/ │ ├── collectors/ # 采集器插件目录 │ │ ├── __init__.py │ │ ├── price_source.py │ │ ├── ranking_source.py │ │ └── notice_source.py │ ├── analyzers/ # 分析器目录 │ │ ├── baseline.py │ │ ├── ranking_shift.py │ │ └── rules_engine.py │ ├── notifiers/ # 告警分发目录 │ │ ├── feishu_webhook.py │ │ ├── dingtalk_webhook.py │ │ └── email_sender.py │ ├── storage/ # 存储层封装 │ │ ├── db.py │ │ └── models.py │ ├── config.yaml # 全局配置 │ └── main.py # 调度入口 ├── grafana/ │ ├── provisioning/ │ └── dashboards/ └── docker-compose.yml这个目录结构是我重构后的最终形态。第一版没有collectors插件目录,所有采集逻辑都堆在一个crawler.py里,一个文件一千多行,改一个新数据源要动其他模块,很容易改出问题。做了插件化之后,新增一个数据源只需要在collectors/下加一个文件,在config.yaml里配好参数,主程序无需改动。
4.2 采集器与主调度实现
核心采集代码不宜追求花哨,我的报价采集器核心逻辑大概30行左右:
import httpx import yaml from storage.db import save_snapshot class PriceCollector: def __init__(self, source_config): self.url = source_config["url"] self.selector = source_config["selector"] self.headers = source_config.get("headers", {}) self.timeout = source_config.get("timeout", 15) def collect_once(self): try: resp = httpx.get(self.url, headers=self.headers, timeout=self.timeout, follow_redirects=True) resp.raise_for_status() raw = parse_price_dom(resp.text, self.selector) normalized = normalize_price(raw) # 统一转“分” save_snapshot( source=self.url, metric_type="price", value=normalized, raw_json={"snippet": resp.text[:500]}, ) return True except Exception as e: log_collect_error(source=self.url, error=str(e)) return False注意我用了httpx而不是requests,原因是httpx支持HTTP/2和异步接口,后期如果要并发采集多个源,改造起来成本更低。第一次写的时候用的requests,后来换数据源的时候发现有些接口是HTTP/2 only,被迫整批改掉,这个教训记住就好。
主调度入口main.py做的事情很单一:加载所有数据源的配置,注册到APScheduler,然后启动一个常驻循环。不过有一个事情必须在主循环里做——周期性的元数据检查和异常自愈。比如检测到某个采集器连续失败次数超过阈值,就自动降低该采集频率,避免放大对源站的请求压力。这个"自动退避"机制是应对平台风控的有效手段,后面排障部分会细讲。
4.3 信号分析规则配置
分析规则的配置我放在同一个YAML文件里,格式一目了然:
analyze_rules: - name: "price_drop_p0" metric: "price" condition: "zscore < -3 or change_pct <= -5" window: "2h" level: "P0" - name: "stock_low_warn" metric: "stock_status" condition: "current_value == 'out_of_stock'" level: "P1" cooldown: "6h" - name: "rank_drop_danger" metric: "ranking" condition: "shift <= -10 and last_rank <= 5" level: "P1" cooldown: "1h"这里的zscore就是前面说的标准差偏离度,cooldown是告警冷却时间,用来做告警抑制。比如"缺货"这种事,如果平台半小时内补上了又缺,系统会重复推送同一条消息,冷却时间保证同一个指标在周期内只推一次。这个配置化思路让运营同学自己也能改规则,不必每次都找我写代码。
4.4 告警落地与看板验证
告警这块踩了一个比较典型的坑:飞书自定义机器人的签名问题。飞书webhook要求把请求时间戳和密钥拼接后做HMAC-SHA1签名,第一次对接时我漏了这个步骤,导致所有消息都被拒。排错方式也比较直接,先手动curl测试webhook地址,发现能通,再看业务代码的Header构造。最后问题出在我把签名值做成了大写十六进制,而飞书要求小写。这种跨平台的细节基本只能靠踩坑积累,文档里不会写这些。
Grafana看板验证就顺利很多,配置完数据源后,写一条测试SQL确保时间字段正确:
SELECT recorded_at AS time, source_url, value FROM metric_snapshots WHERE metric_type = 'price' AND recorded_at >= now() - interval '24 hours';Grafana会自动识别time列。另外一个提醒:不要在Grafana的SQL里用GROUP BY做太复杂的聚合,数据量大时查询会慢,可以在PostgreSQL里预先创建物化视图,Grafana只做简单查询。
5. 常见问题与排查速查表
5.1 运行两周后遇到的典型问题
系统上线两周,我记录了13个问题,其中最典型的有6个,整理成了一张速查表:
| 现象 | 原因 | 排查思路 | 解决办法 |
|---|---|---|---|
| 某个数据源偶发采集失败 | 目标站有简单的频率限制 | 查看采集日志的状态码分布 | 增加随机sleep,避免固定间隔触发封禁 |
| 告警重复推送 | 没有做冷却时间 | 查看告警事件表的insert时间 | 给每条规则加cooldown字段 |
| 价格数据出现0值 | 解析到了页面默认占位符 | 打印原始DOM片段 | 在清洗管道过滤0值和空值 |
| 图表上时间不连续 | 时区设置不一致 | 检查数据库时区和调度器时区 | 统一为Asia/Shanghai,存timestamptz |
| 分析任务越跑越慢 | 快照表没有索引 | 查看慢查询日志 | 对(metric_type, recorded_at)建复合索引 |
| 告警全部静默 | 告警模块异常但被try捕获吞掉 | 检查日志有没有error级别记录 | 告警发送失败要单独抛出并落盘 |
这个表就是项目的"体检报告",建议任何监测系统上线后都要做类似的问题复盘,很多坑在不同数据源之间会重复出现。
5.2 关于数据源反爬与请求频率的实践心得
说句实在话,与其花大量精力研究如何绕过各种反爬机制,不如一开始就把请求频率控制得足够礼貌。我在PLFM_RADAR里对所有数据源的请求都做了三重保险:全局每秒最大请求数限制为2,单个源的最小请求间隔不低于5秒,失败自动退避。这组参数牺牲了一些采集时效性,但换来了稳定的长期运行——我系统连续跑了两个多月,没有一次被封IP或被平台重点标注。
另外,代码里所有HTTP请求必须设置timeout,这是血泪教训。最开始有几个采集器没配timeout,结果某个源服务器假死时,连接一直挂在那里,线程越积越多,最后整个进程内存和文件描述符都被耗尽,系统无响应。加了统一超时后,这个问题彻底消失。
5.3 告警风暴抑制的三重手段
告警风暴是监测系统最常被诟病的问题,我的处理思路是三条线共同作用。第一,单源静默:同一数据源在指定时间段内只发一条告警,配置里的cooldown字段就是在干这个。第二,全局静默:当某个P0级事件触发后,系统自动进入10分钟的"观察期",期间其他P1/P2告警只记录不推送,避免连环爆炸消息。第三,维护窗口:每周日凌晨2点到4点设定为静默窗口,因为平台方通常在这个时间段做系统升级,会造成大量假性告警,没必要惊扰值班人员。
这三条线同时启用后,告警数量从最初每天40多条降到了平均每天5条以内,其中还包括了一半是P2信息级消息。值班同学终于愿意认真看告警了,而不是把群消息一键已读。
5.4 巡检与调优经验
系统正常跑起来后,我给自己定了一个巡检清单,频率是每周一次。核心看四个指标:采集成功率是否在98%以上、分析任务是否有积压、数据库磁盘使用率是否触顶、告警事件的平均处理时长变化趋势。这四个指标能覆盖大部分稳定性问题。
调优方面最重要的一条经验是:基线要滚动更新,但不能更新太快。价格类数据的基线窗口我设置为7天,每天滑动更新。窗口太短比如1天,就容易把突发波动当成基线吸收掉,后面真的出现异常时发现不了。窗口太长比如30天,又无法适应季节性变化。7天是我的经验值,不同场景可以自己调,但思路是一样的——基线更新速度要远慢于你关心的异常变化速度。
还有一个容易犯的错:分析规则越加越多,但从来不清理失效规则。我第一版写了十几条规则,后来发现三分之一永远触发不了。建议每次加规则都带上有效期,过期后自动停用并清理,保证规则集始终精简可用。
最后再说两句掏心窝的话
做PLFM_RADAR这个项目,最深的体会是:监控系统的核心不在于技术多先进,而在于"少打扰人"和"抓得准"。我一开始把规则做得很激进,恨不得所有波动都提醒一遍,结果大家麻木了,真正的严重问题反而被淹没。后来我反复调整基线算法和告警阈值,目标变成了"要么不响,响就一定有用"。
这套系统衍生出来的另一个小工具也很有用——我把数据采集层的清洗管道单独抽出来,做成了一个可以给任意数据源做"健康体检"的脚本,输入URL就能看这个接口的响应时间、数据结构稳定性、字段缺失率。采集源多起来之后,这个工具成了每次接入新数据源的第一道检查关卡。
如果你也想搭一套类似的平台雷达,建议从一个小数据源起步,先把链路跑通,再慢慢加规则加看板。不要一开始就追求大而全,这套系统的复杂度会随着数据源数量线性增长,控制规模才能保持它的可用性。希望这篇复盘能给你省掉几天的试错时间。