
PLFM_RADAR 这个项目名乍看有点抽象拆开就清楚了PLFM 基本就是 Platform 的缩写RADAR 是雷达。合起来就是一个“平台雷达”系统——把多个数据源持续扫一圈把散落的信号、动态、异常波动统一收进来做成一个实时更新的态势感知面板像雷达一样周期性扫描地平线。这几年我做过的数据监控类项目不少但这个项目比较特殊。它不是简单的爬虫入库也不是传统 BI 报表而是把“周期性扫描”和“信号判定”结合起来——扫描频率、信号阈值、多维度权重打分、雷达图可视化每一环都需要仔细设计。如果你正在做平台数据监控、竞品动态跟踪或者舆情预警类的东西这篇复盘应该能给你几条能直接抄作业的思路。1. 项目定位与方案选型1.1 为什么叫“雷达”而不是“监控”监控这个词太被动了——通常是出了问题才知道盯着日志和指标看。雷达不一样雷达是主动扫描周期性地向四周发射探测波然后根据回波判断目标。PLFM_RADAR 的核心思路就是这个轮询式扫描 信号判定 可视化呈现。系统按固定的时间间隔去各个目标平台拉取数据拉回来之后不是简单存起来而是立刻做一次信号分析——有没有新目标出现、有没有指标异常、有没有趋势突变。如果有就把这批信号标记出来推送到面板前端。换句话说传统监控回答的是“系统挂没挂”PLFM_RADAR 回答的是“平台上发生了什么值得注意的事”。这两个问题看着接近但实现逻辑差别很大。1.2 自研 vs 直接用开源监控工具最开始有人提议直接用 Prometheus Grafana 那一套毕竟现成、稳定画图也漂亮。但仔细评估之后放弃了原因有三一是数据形态不匹配。Prometheus 是为服务器指标设计的时序模型很固定。但 PLFM_RADAR 要做的不只是数值指标还包括文本信号、分类标签、业务事件。强塞进 metric 模型里会很别扭。二是信号判定逻辑太重。Prometheus 的告警规则适合做阈值判断但我们的信号判定涉及多个维度加权打分还要结合历史基线对比用 PromQL 写起来极其痛苦后期维护更是灾难。三是可视化需求特殊。雷达图是这套系统的标志性界面Grafana 虽然插件多但要做一个动态更新的多维度雷达面板还是自己写前端更可控。所以最终方案是Python 做数据处理和信号判定Go 写 API 服务前端用 ECharts 做雷达图可视化。数据存储用了 ClickHouse后面细说为什么用它。1.3 核心功能拆解PLFM_RADAR 最终交付的核心功能有这么几块多源数据采集支持同时接入多个平台数据源每个源独立配置扫描周期和接口参数信号信号解析对采集到的原始数据做清洗、结构化、特征提取转成统一格式多维度信号判定从活跃度、增长率、异常波动、文本情绪、交互质量等维度给每个目标打分雷达图可视化把打分结果映射到雷达图的多条轴上一眼看出哪个方向有信号、哪个方向沉寂信号事件追踪对标记出来的异常信号自动创建事件记录持续跟踪后续变化告警通知信号强度超过阈值时通过 Webhook 推送到即时通讯工具这里用企业微信机器人历史回溯所有原始数据和信号记录都落库支持按时间范围回放当时的雷达状态这套组合下来整个系统至少能覆盖从“数据接入”到“决策辅助”的完整链路。2. 系统架构与数据链路设计2.1 四层结构从采集信号到决策辅助PLFM_RADAR 的架构分四层每一层各司其职。第一层是数据接入层。这一层负责对接各平台的数据接口。每个平台写一个独立的采集器Collector实现相同的接口但内部逻辑各自独立。这样新增平台只需要新写一个 Collector不用动其他代码。采集器支持两种模式主动轮询和 Webhook 被动接收。主动轮询就是定时去拉接口Webhook 模式是平台有事件时主动推过来。我把两种都做了轮询为主Webhook 作为补充因为很多平台的 Webhook 并不是实时可靠。第二层是信号处理层这是整套系统的大脑。原始数据进来之后先清洗去掉明显无效的字段然后做特征提取——把文本数据转成情绪评分、把计数数据转成变化率、把分类数据转成独热向量。接着进入信号判定器这一层会结合历史基线判断当前这批数据有没有值得关注的信号点。第三层是存储层。时序数据用了 ClickHouse原因下面专门讲。业务元数据用 MySQLRedis 用来做缓存和临时状态存储。第四层是展示与交互层。后端 API 用 Go 写的提供雷达图数据接口、信号事件接口、趋势查询接口。前端是 Vue ECharts雷达图是核心页面旁边配信号事件流列表。2.2 存储选型为什么是 ClickHouse这个决策是我在项目推进到一半的时候重新做的。一开始我用了 PostgreSQL 存所有数据但跑了一周之后发现两个问题第一数据量增长太快。多个平台、多个目标、每隔几分钟一轮扫描一周就积累了上千万条信号记录。PostgreSQL 在这种规模下做聚合查询已经有点吃力了。第二雷达图后端需要按多种维度做聚合统计。比如“按小时统计某个平台所有目标的活跃度分布”“按天统计信号强度的均值与峰值”。这种多维聚合正是 ClickHouse 的强项。于是把时序数据迁移到了 ClickHouse。表结构大致长这样CREATE TABLE signal_records ( platform LowCardinality(String), target_id String, ts DateTime64(3), dim_active Float32, dim_growth Float32, dim_anomaly Float32, dim_sentiment Float32, dim_interaction Float32, signal_score Float32, event_type LowCardinality(String), raw_data String ) ENGINE MergeTree() PARTITION BY toYYYYMMDD(ts) ORDER BY (platform, target_id, ts);几个关键设计LowCardinality类型用在 platform 和 event_type 上因为值种类很少查询很快存储也更省分区按天每天一个分区清理旧数据直接 drop partition不用 DELETE排序键是 (platform, target_id, ts)这样按平台查目标的时间序列几乎就是顺序读性能极好这个表结构稳定运行到现在千万级数据量下雷达图接口的响应时间基本都在 200ms 以内很稳。2.3 采集器的统一接口设计每个数据源的采集器实现统一接口这是我踩过坑之后总结出来的。最初第一个平台我写得很随意第二个平台接入的时候发现没法复用被迫重构浪费了三天。统一接口长这样class BaseCollector(ABC): abstractmethod def fetch(self, last_since: datetime) - list[RawRecord]: 拉取自上次扫描以来的新数据 abstractmethod def parse(self, raw: dict) - NormalizedRecord: 把平台原始格式转成统一数据模型 abstractmethod def health_check(self) - bool: 检查数据源连接是否正常 property abstractmethod def platform_name(self) - str: 平台标识, 全局唯一fetch的入参是last_since这很关键。每个平台返回的数据只要增量按时间过滤避免重复拉全量parse是数据源适配的核心把五花八门的平台字段映射到统一的 NormalizedRecord 结构health_check用来感知数据源本身是否挂了挂了就跳过不阻塞主链路只要新平台能实现这四个方法就可以无缝接入雷达系统主流程完全不用改。3. 信号处理机制与实现细节3.1 信号强度的五维评分模型雷达图可视化的核心是维度评分。我用了五个维度每个维度 0 到 100 分五条轴构成雷达形状。这五个维度是反复试出来的组合活跃度dim_active反映目标最近一段时间内的总体动作频率比如发帖数、更新数、登录次数等。这是一个基础量代表“目标有没有在动”。增长率dim_growth跟上一周期相比的增长幅度。这个维度专门捕捉“突然加速”活跃度可能只是平均水平但增长率一旦异动就有信号。异常波动dim_anomaly基于历史基线的偏离程度用 Z-score 类方法计算。这个维度捕捉的是“不正常的状态”区别于前两个维度的“正常变化”。文本情绪dim_sentiment对采集到的文本内容做情绪分析从负面到正面映射到 0-100 分。这个维度对舆情类场景特别重要。交互质量dim_interaction统计转发、评论、点赞这类交互行为并按互动深度加权。它跟活跃度的区别在于活跃度只看“发了多少”交互质量看的是“内容有多少人响应”。每个维度的计算都独立实现最后汇总时按权重加权平均。默认权重是活跃度 0.2、增长率 0.2、异常波动 0.3、文本情绪 0.1、交互质量 0.2。异常波动权重最高因为雷达的核心使命是捕捉异常这一点从项目定位上就决定了。3.2 异常检测结合滑动窗口基线与 Z-score异常波动的计算是整个系统技术含量最高的地方也是最容易出 bug 的地方。基本方案是维护一个滑动窗口的历史基线。每个目标保留最近 7 天的历史数据按小时分桶共 168 个桶实时计算当前值与历史同期值的偏差。def compute_anomaly(history, current): # history: 最近N天的历史值列表 # current: 当前周期的观测值 mean np.mean(history) std np.std(history) if std 1e-6: # 历史方差接近0, 任何明显偏离都算异常 return 100.0 if abs(current - mean) epsilon else 50.0 z_score abs(current - mean) / std anomaly_score min(z_score / 3.0 * 100, 100) return round(anomaly_score, 2)这里有个细节Z-score 理论上是标准正态分布超过 3 的概率约 0.3%所以 3 个标准差打 100 分比较合理。但实际数据往往不是正态分布偏态和长尾很常见所以直接把 Z-score 线性映射到 100并没有用 p-value 做非线性变换——工程上够用而且解释起来更直观。这个函数被调用之前还有一道预处理历史数据先做一次离群值截断把前 5% 的超高值剔除掉不然一个极端历史值会把标准差拉大导致当前异常被掩盖。3.3 转录与汇聚信号事件生成逻辑单个异常点不构成信号信号要“连续、显著、可追踪”才有意义。所以我单独做了一层事件生成逻辑。事件生成的条件有三个需要同时满足信号强度阈值当前综合评分超过 70 分持续时间连续两轮扫描超过一个扫描周期都满足阈值防止瞬时抖动误报幅度条件相对上一个基线周期的提升幅度不低于 30%满足这三个条件之后系统会自动创建一条 SignalEvent 记录包含信号类型、涉及目标、初始强度和当前强度、事件时间线。事件一旦创建后续每轮扫描都会更新它的“最新状态”直到信号强度跌破 40 分并持续 3 轮自动标记为“已平息”。这套设计解决了一个很实际的问题如果只对单点异常做告警运营同事一天能收到几百条通知最后大家都把通知免打扰了。事件聚合机制把几百条原始信号折叠成了几条真正值得看的结论。3.4 雷达图后端数据的生成过程雷达图的前端画起来不复杂真正的复杂度在于后端要算出“此刻每个维度该显示什么”。每轮扫描完成后后端会为每个目标生成一条雷达数据记录五个维度各一个 0-100 的值外加综合评分。但这五个值不是同一时刻算出来的因为五个维度依赖的数据源不同、时间窗口也可能不同。比如活跃度看的是最近 1 小时增长率看的是环比上一扫描周期异常波动看的是与历史基线的偏差。所以我做了一个对齐操作所有维度的值都对齐到“当前扫描轮次”这个时间坐标即以当前时刻为基准往前取对应的窗口期数据。这样雷达图上的每个点都代表“此刻这个目标的状态快照”语义一致、可对比。前端拿到的接口返回值格式大致是这样的{ target_id: tk_1024, ts: 2024-03-18T14:30:0008:00, platform: platform_beta, dims: { active: 76.5, growth: 82.1, anomaly: 93.4, sentiment: 45.2, interaction: 61.8 }, signal_score: 78.3, event_id: evt_88991 }前端拿到这份数据直接填进 ECharts 的雷达图配置项即可。单目标展示时画一个雷达多目标对比时画叠加雷达颜色区分目标一眼就能看出谁在哪个维度上突出了。4. 实操过程中的坑与排查记录4.1 ClickHouse 对高基数表的查询延迟问题跑了一阵子之后雷达接口偶尔会从 200ms 抖到 1.5 秒以上。排查发现是target_id的基数太高某个平台的目标数量有几十万个按target_id过滤时索引选择率差扫描数据块多。处理方法给高频查询的场景单独建了一张物化视图按平台 小时粒度预先聚合好五维度的平均值和最大值。查询接口优先读物化视图明细表只保留 7 天用于下钻分析。4.2 文本情绪分析在特定语料上翻车文本情绪维度最初用的是开源的情感分析模型中文语料下整体还行但遇到网络热词、反讽语料就频频翻车。比如“太强了直接开摆”这句话模型判成了中性偏负面但实际语境是正面“开摆”在这里是自我调侃。这个问题的解决思路比较务实在小样本上人工标了一批领域特有词汇做成一个覆盖层override layer先查词表命中就覆盖模型结果未命中才走模型。这样既控制成本又能保证准确率在特定领域内达标。词表大概维护了 120 个词条效果提升立竿见影。4.3 扫描任务堆积导致延迟连锁放大最开始用 Python 的 APScheduler 做定时扫描平台多了之后就出现任务堆积——上一轮没跑完下一轮又触发导致采集延迟越来越大数据新鲜度持续恶化。改造方案是把调度逻辑改成“串行消费 超时熔断”所有扫描任务投递到队列里由一组 worker 消费每个扫描任务设置三档超时时间软超时报警、硬超时终止、熔断暂停该平台后续任务。这个改动之后哪怕个别平台接口变得很慢也不会拖垮整体链路。4.4 采集器被对端限流导致数据缺失这个坑很经典。某个平台短时间请求太频繁直接开始返回 429而且后续一段时间内所有请求都被降级处理。结果就是当天该平台的数据大面积缺失雷达图上直接凹进去一块。对策有两层第一层是退避重试遇到 429 先停止该平台任务 60 秒指数退避第二层是数据补采检测到缺失时段后等流量平峰时启动补采任务把缺的数据追回来。补采逻辑单独跑不在正常链路里执行不影响正在进行的扫描。4.5 奇偶轮次波动导致的误报某个目标平台的数据晴雨表的波动本身有周期性。用“上一个完整周期”做环比时如果恰好碰到周期边缘增长率计算会异常高触发误报。后处理策略是增长率维度采用“同周期环比”比如与上周同一天的同一时段对比而不是简单跟上一个 24 小时比较。同时增加了最小样本量约束——如果当前周期有效样本量不足 5 条直接把增长率和平共处到中性位置不参与雷达加权。4.6 雷达图前端加载卡顿的真凶雷达图页面加载慢一开始怀疑是后端接口慢查了半天性能瓶颈最后发现居然是 ECharts 实例没销毁。前端做了路由切换之后旧的 chart 实例还挂在内存里每个雷达图几百个点累积起来内存就爆了。解决办法是在组件beforeDestroy或 Vue 3 的onBeforeUnmount里调用chart.dispose()同时在路由切换前清空所有实例的引用。改完之后页面切换流畅了很多内存也稳定下来了。5. 关于后续拓展和业务接入整套系统从设计到落地我个人有一些感受。PLFM_RADAR 这种“雷达”思路很适合做成通用能力。我后续已经在规划把它抽象成一套 SDK让不同业务方接入自己的数据源和目标集哪怕不懂后端也能配置出属于自己的雷达面板。有一个比较成功的应用是把雷达的评分结果接入到了业务侧的“重点目标识别”每天凌晨跑一次全量目标扫描按信号强度排序把 top 10 的目标自动推送给运营同事作为当日重点关注清单。这个比人工刷后台高效太多反馈非常好。如果你想在自己团队里落地类似系统我的建议是不要一开始就追求大而全——先选一个平台、一个目标集、三个维度跑通一轮完整的“采集-分析-展示-告警”闭环再逐步扩展。雷达图的魅力在于多维度交叉但前提是每个维度都足够稳定不然只会得到一个剧烈抖动的多边形让你什么信号也看不清。另外信号阈值这种东西不要拍脑袋定一定要让业务方参与进来。我最初定的 70 分阈值被运营挑战了好几次最后是基于两周的线上数据和多次人工复核校准到 70 分的。后续每季度要重新复盘一次阈值毕竟平台行为特征会随着时间缓慢漂移一成不变的阈值迟早会失效。最后分享一个小经验这类系统的可视化重要性绝对不能低估。雷达图不是锦上添花它是信号判定结果的最终出口——一套算法再精准如果展示得让人看不懂那也起不到辅助决策的作用。我在设计雷达图时反复调整了轴向标签、配色对比度、数据刷新动画和异常高亮效果这些细节直接决定了用户是每天打开看还是打开一次就再也不碰。如果你的项目也需要类似的可视化表达多用真实数据去测试可读性别在抽象图例上花太多时间。