一条告警的 10 秒:从 -5 dB 到"已处理"

12:31:46,一条 snr = -5 的遥测记录落库。

几秒之内,屏幕右下角亮起红色弹窗:严重告警 · 下行信噪比过低 · Starlink-1002 · snr=-5.00 below threshold 0.00,顶栏铃铛的未读角标跳到 1。12:31:51,值班的人在告警中心点开详情、按下"确认处置"——确认人 admin、确认时间落库。又过一个评估周期,一条 snr = 25 的正常数据写入,这条记录自动翻成绿色:已恢复

告警弹窗.png

不是演示动画。上面的时间戳全部来自交付验收时真实跑出来的链路。这篇工程化笔记(系列第四篇)就按这"十秒"的先后顺序,把系统主动喊人的整条链路拆开:规则怎么定义、评估引擎在轮询什么、通知怎么送到浏览器、人点了确认之后状态机怎么走。最后两站,是这次一起交付的两个扩展——遥测健康评估升级(SU4) 与多数据源的数据管理中心

第 1 站 · 规则:判决书要提前写好

评估引擎里没有一行写死的判定逻辑。什么算"越限",全部来自规则表——一条规则回答四个问题:

  • 盯谁:指定卫星,或全体卫星(satellite_id 为空);
  • 盯什么:8 个遥测指标之一(电量、舱温、信噪比、存储占用……);
  • 什么算越限:四类规则任选;
  • 多严重:INFO / WARNING / CRITICAL 三档。

四类规则各管一种"越限"语义:

类型语义一句话例子
THRESHOLD单阈值比较(GT / LT)电量 低于 20%
RANGE区间判定(OUTSIDE / INSIDE)舱温跑出 [0, 60]℃
COMPOSITE多参数联合判读(AND / OR,2~5 条条件)温度高 且 功耗高才算数
TREND趋势斜率比较(带符号)数值没越线,但掉得比 0.5/分钟还快

后两类是本次新扩展的判读能力,放在第 5 站展开。这里先说两个规则引擎的设计决策。

第一条:规则的"法条"钉在数据库里。 应用层的校验总有可能被绕过(迁移脚本、手工改库、未来新写的接口),所以我们把语义同时写成了库层 CHECK 约束——RANGE 必须满足下界 < 上界、COMPOSITE 的条件数组必须是 25 个元素、TREND 的回看窗口必须在 30 秒24 小时之间。任何一条不合法的规则,无论从哪个入口进来,都会被数据库当场拒绝:

sql
复制代码
-- V6__alert_su4.sql(节选示意,省略部分互斥判据) ALTER TABLE alert_rule ADD CONSTRAINT alert_rule_threshold_check CHECK ( (rule_type = 'THRESHOLD' AND operator IN ('GT', 'LT') AND threshold IS NOT NULL) OR (rule_type = 'RANGE' AND threshold_min IS NOT NULL AND threshold_max IS NOT NULL AND threshold_min < threshold_max) OR (rule_type = 'COMPOSITE' AND condition_json IS NOT NULL AND CASE WHEN jsonb_typeof(condition_json) = 'array' THEN jsonb_array_length(condition_json) BETWEEN 2 AND 5 ELSE FALSE END) OR (rule_type = 'TREND' AND slope_threshold IS NOT NULL AND window_seconds IS NOT NULL AND window_seconds BETWEEN 30 AND 86400) );

一个值得注意的细节:PostgreSQL 不保证 OR 链的求值顺序,直接对非数组的 JSON 调 jsonb_array_length 可能当场报错——所以长度校验用 CASE WHEN jsonb_typeof(...) = 'array' 先验类型、再量长度。约束里的每一行,都是被生产环境教育出来的。

第二条:指标白名单只有一份来源。 规则校验要用指标集合、评估引擎要按指标从遥测快照里取值、判读策略还要复用同一套比较语义——这三处如果各写一份,早晚对不上。我们建了一个 AlertMetrics 注册表:指标名 → 遥测列取值函数的映射,校验、取值、比较全部共用它。以后加新指标,只在这个注册表里登记一行。

另外两个细节:服务层会做字段归一化——比如把 THRESHOLD 规则里残留的 min/max 字段清空,保证"库里每一行都能被 CHECK 约束解释";告警记录会快照规则名、卫星名、指标、级别——所以规则改名、删除、重建,历史告警永远自解释。(截图里的演示规则在出图后已删除,记录仍然完整可读,就是这条设计的现实版演示。)

第 2 站 · 评估:每 10 秒一次的"全量体检"

这条链路的核心不是"事件驱动"——没有人在遥测写入时同步判断。评估引擎是一个每 10 秒出发一次的巡官(fixedDelay,启动延迟 15 秒,先让数据库迁移和分区维护跑完),每一轮把全平台的"规则 × 卫星"组合过一遍。

一轮的内部流程:

  1. 加载全部启用规则(固定 id 升序,日志顺序稳定);
  2. 汇总规则实际涉及的卫星,只对它们取最新遥测快照——每颗卫星一次 LIMIT 1 索引点查,查询代价不随遥测表分区增长;
  3. 逐组合判定,三态分流:
  • BREACHED(越限):进入触发流程;
  • WITHIN(正常):如果有未恢复的记录,自动转"已恢复";
  • SKIP(无法判定):指标列缺失时不触发、也不恢复——保持现状。

三态里的 SKIP 值得多解释一句。"指标缺失"最容易写错的地方是把它当成"恢复正常"——那样一条 FIRING 记录会因为一次数据缺列被误报"已恢复"。我们的选择是:无法判定就什么都不做,宁可等下一轮。

持续时长规则("存储占用高于 90% 持续 5 分钟"这类)需要跨轮次记住"第一次越限是什么时候"。我们用一个 Redis 键(leo:alert:breach:{规则ID}:{卫星ID})存首次越限时间戳,TTL 设为"持续时间 + 10 分钟缓冲"、期间每轮续期;满时长才触发,恢复正常立刻删键、重新计时。

Redis 挂了呢?这里有一个反直觉的取舍:降级为"本轮不触发",而不是"立即触发"。把"计时失败"当成"时间已到"会制造大量误报;而漏掉一轮、Redis 恢复后重新计时,只损失几分钟的延迟。这类降级还做了日志限流——故障时记一条 WARN、恢复时记一条 INFO,不刷屏。

去重是老话题但有硬细节:命中 (rule_id, satellite_id, status) 上的专用索引,同一"规则 × 卫星"只要存在未恢复记录(触发中或已确认),就原地刷新指标值和消息,不新建、不重复推送。指标持续恶化时,列表里永远只有一条记录,值在不断更新。(算一笔账:没有这条去重,10 秒一轮、故障持续一小时,就是 360 条重复记录。)

自动恢复:最新值回到阈值内,FIRING 和 ACKNOWLEDGED 记录一律转"已恢复",恢复时间落库;但不会抹掉确认人和确认时间——"这条告警当初是谁处置的",永远查得到。

再补一张安全的网——单点隔离贯穿全轮:某颗卫星的快照加载失败,只跳过这颗卫星;某个组合判定抛异常,记录后继续下一个组合;推送失败,不影响落库。10 秒一班的巡官不会因为一个坏样本停摆。

告警列表.png

这一站顺带讲一个"只有真实库才能暴露的 Bug"

V5 迁移给告警记录加了"已读"列,NOT NULL + 数据库默认值 false。逻辑听起来万无一失,但真实链路上线后,告警一条都落不进库

根因:MyBatis-Flex 的 insert 会把实体里没赋值的字段显式写成 NULL——而"显式 NULL"会覆盖掉数据库的 DEFAULT false,直接撞上 NOT NULL 约束。数据没问题、SQL 没问题,是"默认值"这个承诺被 ORM 悄悄撕掉了。

为什么之前没抓到?两层巧合叠加:单测全程 mock,验证不到库层约束;而 D27 之前,真实数据库里从没有过一条被触发的告警(演示都是 mock 数据)。直到这次用"真后端 + 真 WS + 真落库"的端到端用例才把它钓出来。修复只有一行(新建记录时显式置"未读"),但补了回归断言。

两句话总结这个坑:数据库默认值在 ORM 的显式 NULL 面前是纸糊的;以及——有些缺陷只活在真实链路里,mock 测试永远绿得心安理得,却什么都证明不了

第 3 站 · 送达:从 STOMP 广播到铃铛亮起

评估引擎发现越限后,把事件交给"通知通道"。这里用了一个可插拔的接口 AlertNotifieralertFired / alertResolved 两个事件,当前唯一实现是把消息广播到 STOMP 主题 /topic/alerts 的 WebSocket 通道;将来要加邮件通道,实现接口、注册 Bean 即可,评估引擎零改动。单个通道推送失败只记日志,不影响落库和其他通道。

推送载荷是一个 12 字段的触发快照(事件类型、记录 ID、规则与卫星名、指标与值、级别、消息、时间戳)——前端拿到就能直接渲染,不需要再回查详情。这条通道的语义是"尽力而为":没有在线订阅者就直接丢弃,因为记录永远在库里、列表永远查得到。实时是体验,落库才是事实。

前端这一侧,是这次通知中心(D27)的全部工作:

  • 一条长连接:SockJS + STOMP 客户端,CONNECT 帧带 JWT(复用此前已完成鉴权的 STOMP 通道),断线 5 秒自动重连、双向 10 秒心跳;连接状态三态(连接中/已连接/已断开)暴露给 UI——面板底部的绿点"实时推送已连接"就是它(见图)。
  • 未读计数双轨:收到 ALERT_FIRING 事件本地 +1(实时),打开面板和进入页面时从后端拉取校正(兜底)。实时链路允许丢消息,所以任何时刻都要有一条"从库里拉"的退路。
  • 弹窗策略:只有 CRITICAL 级别弹窗(红色通知、10 秒自动消退、点击跳转告警中心),WARNING / INFO 只进列表——弹窗是稀缺资源,滥弹一次,以后就没人看了。并且加了同源去重:同一"规则 × 卫星"10 分钟内不重复弹窗,防的是"告警风暴瞬间糊满屏幕"。
  • 未读角标:铃铛上的红点计数来自 store,超过 99 显示 99+,归零自动隐藏。

通知面板.png

面板本身很简单:未读 / 全部两个视图、"全部已读"、点条目自动标注已读并跳转告警中心。真正花心思的是上面这些"行为边界"——什么时候弹、弹多久、丢消息怎么办、风暴时怎么压。

第 4 站 · 闭环:"已确认"不等于"已解决"

这是链路第 5 秒发生的事:人打开详情、按下"确认处置"。我们把状态机设计成三个状态,各自有清晰的语义:

  • FIRING(触发中):系统在喊,没有人回应;
  • ACKNOWLEDGED(已确认):有人接单了——不代表问题已解决,只代表"人看到了,正在处理";
  • RESOLVED(已恢复):指标自己回到了正常范围(由下一轮评估自动判定,不需要人工点)。

确认处置的三条防线,全部围绕"状态机的尊严":

  1. 已恢复的不许再确认(返回 400)——问题都没了,别补盖章;
  2. 重复确认是幂等的——不覆盖首次确认人和确认时间,接单只接一次;
  3. 确认时顺带清未读——处置过的事不该继续顶着未读角标。

还有个容易被忽略的一致性细节:确认之后,如果指标还在持续越限,评估引擎只刷新数值,不把状态打回 FIRING——人工处理中的告警,机器不能抢方向盘。等指标真正恢复正常,统一转"已恢复",而"当初谁确认的"依然留在记录里。

告警详情.png

这套闭环不是靠手点验证的。我们专门写了一条"真实后端"端到端用例:浏览器真实登录 → 通知中心建立真实 STOMP 连接(断言"实时推送已连接")→ 经 REST 灌入越限数据 → 评估引擎真实广播 → 断言真实弹窗内容(规则名 + snr=-5.00 below threshold 0.00)→ 未读角标 +1 → 详情页自动已读 → 确认处置闭环。全部断言跑在真实后端、真实数据库、真实 WebSocket 上——第 2 站那个 Bug,正是这类用例存在的理由。

验收当天还做了一次"全科考试":五类规则(阈值 / 区间 / 持续时长 / 联合 / 趋势)同轮全部触发(趋势规则的斜率 -32.74/分钟 被逮个正着),随后逐条确认、灌入恢复样本,五条记录在同一轮评估里全部自动转"已恢复"。

告警闭环.png

第 5 站 · SU4:遥测健康评估升级——联合判读与趋势预警

前四站把"一次告警的完整生命周期"讲完了。这一站回答两个新问题:只有单指标比较够用吗?没有。

问题一:有些异常,单指标永远看不出来。 "舱温偏高"和"电池放电电流偏高"单独出现都可能只是正常波动;两个同时出现,才是真正值得叫醒人的信号。于是有了 COMPOSITE 规则:条件数组(JSONB 存储、2~5 条、每条支持 GT/LT/OUTSIDE/INSIDE)按 AND / OR 汇总。语义上抠了两个细节:

  • AND:全部条件命中才算越限;任一指标缺失 → SKIP(无法判定)。不能因为"半个条件没数据"就触发,同样不能因为"半个条件没数据"就误报恢复。
  • OR:任一条件命中即越限;缺失指标的条件不参与判定;所有条件都拿不到值时才算 SKIP。

触发消息会列出参与判定的条件快照(AND 列全部、OR 列命中的),超过 500 字符自动截断——500 刚好是消息列宽,库和消息两端不会打架。

问题二:等越线再喊,有时候已经晚了。 电池从 90% 掉到 80% 用了一小时,和用十分钟掉下去,性质完全不同。TREND 规则盯的就是变化率:取回看窗口(30 秒~24 小时,可配)内的历史样本,做最小二乘线性回归,求出斜率(换算成"指标单位/分钟",带符号),再跟斜率阈值比较。LT -0.5 的含义是"下降速度快于 0.5/分钟"——数值还没跌破任何静态阈值,预警就已经发出。

两次"不硬算"的克制:有效样本少于 3 条直接跳过(样本太少,斜率全是噪声);窗口内样本上限 120 条、取最近的一批(遥测高频上报时,单轮评估不能被一条趋势规则拖慢)。

遥测监控.png

架构上做的最重要的一件事是留扩展点。判定逻辑被抽成策略族:RuleBreachEvaluator 接口 + 按 rule_type 分派的注册表,原有两个类型(阈值、区间)从评估引擎里抽出来成为独立策略类(行为不变、先补测试再搬运)。以后新增一类判读规则 = 新增一个策略类 + 注册表登记一行,评估主流程一行不改。 这次从 2 类扩到 4 类,验证的就是这个扩展点:评估引擎本身没有改动,新增逻辑全部落在新文件里。

第 6 站 · 数据管理中心:从外部编目"进货"

链路讲完了,最后看一条完全不同的数据流。平台的卫星档案原来靠自己录;这次追加了一个"数据管理中心"(仅管理员可见):从外部编目数据源(CelesTrak、Space-Track、KeepTrack)把星座与卫星档案批量"进货"进来。

数据管理中心.png

三种数据源的"脾气"完全不同,这恰恰是架构上最费心思的地方:

  • CelesTrak:公开编目,免凭证;按分组(空间站、星链、GPS……)查询,解析的是"卫星名 + 两行 TLE"组成的三行组;还带着第三方站点的 2 小时节流——正常使用时如果踩到 403,会被映射成"数据源忙碌,请稍后再试"的人话提示,而不是把上游故障原样砸给用户;
  • Space-Track:要账号密码;走 /ajaxauth/login 会话登录拿 Cookie,并且做了请求级 Cookie 隔离(会话不污染全局连接池);
  • KeepTrack:返回的 JSON 有"薛定谔的形态"——{data: []}、单个对象、裸数组都可能出现,字段名还有别名。解析层先全部容错,再统一收敛。

无论上游多参差,出口只有一个模型:CatalogEntry 归一化条目(NORAD 编号、名称、国际编号、倾角、偏心率、轨道分类……)。各数据源实现 CatalogProvider 接口(声明键、展示名、参数表单元数据、拉取逻辑),注册表自动发现——新增第四个数据源 = 写一个实现类,前端的参数表单会自动按元数据渲染,预览、导入、历史全部零改动复用。

前端流程(见图):选数据源 → 填参数(CelesTrak 免凭证直接拉)→ 拉取预览 → 勾选 → 导入设置 → 执行 → 历史留痕。预览里每个条目标注"新条目 / 已存在"——跟本地库按 NORAD 编号批量比对得出,不是逐条查库。

数据管理中心预览.png

上面这张图是真实拉取的现场:ISS(ZARYA)、天和核心舱、问天实验舱……20 条真实编目条目一目了然,轨道类型已经自动分好类(按高度/倾角判 LEO/MEO/GEO)。

导入环节三个防止"帮倒忙"的开关:

  • 已存在的卫星跳过(仅新增)更新轨道要素——更新模式只刷轨道类信息,你在平台上人工维护的字段不会被外部数据整行覆盖;
  • 归入星座:可选把新卫星归入指定星座;星座不存在时自动建档(用成员卫星的均值轨道要素建一个,并刷新成员计数);
  • 留痕:每次导入都写一条 append-only 记录(数据源、参数、模式、新增/更新/跳过/失败计数、操作人、耗时),接口层再叠一层操作审计。

最后是安全底线:入口做了三层管控(菜单隐藏、路由重定向、接口上类级 @PreAuthorize 锁 ADMIN);外部数据源的账号密码不进仓库——本地开发放 leo-local.yml(已被 gitignore 排除),部署走环境变量,仓库里只有示例模板。

收尾:本次交付的数字

  • 后端单测:172/172(系列上篇时还是 46 条:判读策略新增 22 条、数据管理中心新增 34 条,其余为评估引擎与规则校验扩展);
  • 前端门禁:eslint + vue-tsc 0 error
  • 端到端:13 passed + 4 skipped;其中"真实后端"告警链路用例 1 passed(真登录、真 WebSocket、真广播、真落库);
  • 闭环演示:五类规则同轮触发,一次通过(含 -32.74/分钟 的斜率预警);
  • 真实链路揪出的缺陷:1 个(is_read 落库,已修复并补回归断言)。

这套链路里没有一件东西是"卫星专属":规则的库层校验、评估的 SKIP 三态与去重、通知的"推送 + 拉取兜底"双轨、确认处置的状态机防线——任何"让系统主动找人"的场景都能整段搬走。

文中的截图全部来自真实系统运行(演示规则已在出图后删除,记录保留)。如果这篇帮你绕开哪怕一个坑,欢迎转发给正在设计告警的同事。留言区想聊聊:你们系统里"已确认未恢复"这种中间态,是保留独立的确认人信息,还是用完即弃?

本文关键词:告警规则引擎 | 评估引擎 | STOMP/WebSocket 推送 | 通知中心 | 状态机闭环 | 最小二乘趋势预警 | JSONB 联合判读 | 数据编目导入 | SPI 扩展点 | 卫星平台 2.0

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP