首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >工业 IoT 告警治理实战:去重、抖动抑制与群发告警的收敛

工业 IoT 告警治理实战:去重、抖动抑制与群发告警的收敛

原创
作者头像
Xxtaoaooo
发布2026-08-28 09:35:24
发布2026-08-28 09:35:24
1230
举报
文章被收录于专栏:应用实践应用实践

摘要

做工业 IoT 平台的人,很多经历过这样一条曲线:告警系统刚上线时,每天几十条,每条都值得看;三个月后设备全量接入,每天几千条;半年后,运维开始无视告警群——不是他们不负责任,是告警里十有八九不需要处理。等到真故障发生,那条关键的告警躺在几百条"离线告警""越限告警"中间,没人点开。

这就是告警风暴,以及它带来的告警疲劳。告警系统的价值不在于"能报多少",而在于"每一条都值得看"。一个每天几千条的告警系统,实际效果不如一个每天二十条、条条有效的系统。

告警风暴有三个典型来源:重复轰炸(同一个问题反复报)、边界抖动(指标在阈值附近来回穿越,每次穿越报一条)、共因群发(一条产线停电,两百台设备同时报离线)。这三个来源对应三道治理闸门:单条告警的卫生(去重、持续确认、滞回抑制)、群发告警的共因收敛、以及分级静默路由。

本文用 DolphinDB 的流计算引擎(响应式状态引擎 + @state 状态函数)把这三道闸门逐个实现出来,最后给出一组度量告警治理效果的指标。流计算引擎本身的性能调优(吞吐、延迟)不是本文重点,本文聚焦的是跑在引擎之上的告警治理逻辑


一、告警风暴从哪来:三个爆炸源

先诊断,再开药。告警量失控,来源无非三类:

来源一:重复轰炸

同一个故障没恢复,告警每分钟报一次。设备轴承磨损,振动持续超标,于是从超标那一刻起每小时 60 条告警,直到有人处理。一条故障,几百条噪音。

来源二:边界抖动

阈值设在 5.0,而设备的振动值恰好在 4.9~5.1 之间自然震荡。于是告警状态反复"触发→恢复→触发→恢复",一小时能产生上百次状态翻转。每次翻转都通知,就是每次都在喊狼来了。

来源三:共因群发

一条产线停电,线下 200 台设备全部离线。设备级的离线告警逐台触发,200 条告警瞬间涌入——但根因只有一个:停电。运维要看的不是 200 条"设备离线",而是一条"三号产线供电异常"。

三个来源,占了告警噪音的绝大多数。它们分别对应下面三节的三道闸门。


二、第一道闸:单条告警的卫生

这一节处理来源一和来源二,核心是三个机制:同源去重、持续时长确认、滞回抑制

2.1 同源去重:未恢复,不重报

去重的原则:同一个设备 + 同一条规则,告警未恢复前只报一次。后续数据继续越限,只更新"持续时长",不再产生新告警。

实现上,这意味着告警判断必须是有状态的——要记住"这台设备这条规则当前是否已处于告警态"。这正是 @state 状态函数的用武之地:用它写的函数在流计算引擎里,每个设备(keyColumn)维护一份独立的状态,跨消息持续存在。

2.2 持续时长确认:越限且持续,才算数

瞬时毛刺不该报。振动值因为一次物料冲击窜到 5.2、一秒后回落,这不是设备故障,是正常工况波动。规则改成:越限并持续 N 秒才触发

@state 维护一个持续计数器:

代码语言:javascript
复制
// 持续时长确认:越限且连续满 confirmSecs 次采样才告警
@state
def persistentAlarm(value, threshold, confirmSecs) {
    mutable dur = 0
    if (value > threshold) {
        dur += 1
    } else {
        dur = 0
    }
    return iif(dur >= confirmSecs, 1, 0)
}

这段逻辑的关键在 mutable dur:它是跨消息持续的。同一台设备每来一条数据,函数执行一次,dur 在上一次的基础上累加或清零。毛刺窜一下立刻回落,dur 清零,不会误报;持续超标,dur 涨到阈值才触发。

2.3 滞回抑制:触发阈值 ≠ 恢复阈值

对付边界抖动的经典手段是滞回(hysteresis):触发和恢复用两个阈值,中间留一段缓冲带。

代码语言:javascript
复制
振动值
  5.5 ┤              ●━━━━━ 告警持续中
  5.0 ┤ ─ ─ ─ ─ 触发阈值(向上穿越才触发)─ ─ ─
  4.5 ┤ ─ ─ ─ ─ 恢复阈值(向下穿越才恢复)─ ─ ─
      ┤    ●──●──●  值在 4.6~5.1 震荡 → 不再反复翻转

触发阈值 5.0、恢复阈值 4.5:值窜过 5.0 进入告警态后,必须跌回 4.5 以下才恢复。在 4.5~5.0 之间的震荡不会引起任何状态翻转。

代码语言:javascript
复制
// 滞回抑制:双阈值状态机
@state
def hysteresisAlarm(value, onThreshold, offThreshold) {
    mutable alarmed = 0
    if (alarmed == 0 and value > onThreshold) {
        alarmed = 1
    } else if (alarmed == 1 and value < offThreshold) {
        alarmed = 0
    }
    return alarmed
}

两个阈值之间的缓冲带宽度是调参重点:太窄抑制不住抖动,太宽会让告警恢复迟钝。经验起点是缓冲带取正常波动幅度的 2~3 倍。

2.4 装上引擎

把两个函数挂到响应式状态引擎,按设备维护独立状态:

代码语言:javascript
复制
// 输入:实时测点流(每台设备每秒一条)
// 输出:带状态翻转标记的告警流
alertOut = table(1:0, `deviceId`ts`value`alarmState, [SYMBOL, DATETIME, DOUBLE, INT])

alertEngine = createReactiveStateEngine(
    name        = "alertHygiene",
    metrics     = <[
        persistentAlarm(value, 5.0, 10) as confirmed,
        hysteresisAlarm(value, 5.0, 4.5) as hysteresis
    ]>,
    dummyTable  = sensorStream,
    outputTable = alertOut,
    keyColumn   = "deviceId"
)

subscribeTable(tableName=`sensorStream, actionName="hygiene",
               handler=tableInsert{alertEngine}, msgAsTable=true)

输出流里,真正要通知的只有状态翻转的边沿(0→1 触发、1→0 恢复),持续期间的 1 不再重复报——这就是去重的落地:下游订阅时只消费边沿。


三、第二道闸:群发告警的共因收敛

单条告警干净了,群发还没解决。200 台设备同时离线,即使每台都做了去重,还是 200 条。

3.1 思路:从设备级上卷到组级

共因故障的特征是时间和空间上的聚集:同一产线(或同车间、同配电回路)的设备,在一个很短的时间窗内集中越限。治理思路是把告警判断从设备级上卷到组级

  • 组内越限设备比例超过阈值(比如 60%),报一条组级告警("三号产线疑似供电异常")
  • 组内个别设备越限,保持设备级告警(单点故障照常报)

这样停电场景收敛成一条告警,单点故障不受影响。

3.2 两级引擎编排

实现是两级流水线:第一级做设备级卫生判断(上一节的引擎),第二级用时间序列聚合引擎按组统计越限比例:

代码语言:javascript
复制
// 第一级输出(alertOut)带 deviceId,先映射到所属产线
// 假设设备档案(PKEY 表)提供 deviceId → lineId 映射
enriched = select a.deviceId, a.ts, a.confirmed,
                  p.lineId
          from aj(alertOut as a, loadTable("dfs://iot_meta", "deviceProfile") as p, `deviceId)

share enriched as enrichedStream

// 第二级:每 10 秒一个窗口,按产线统计越限比例
groupOut = table(1:0, `lineId`windowEnd`alarmRatio`alarmCount, [SYMBOL, DATETIME, DOUBLE, INT])

convEngine = createTimeSeriesAggregator(
    name        = "groupConverge",
    windowSize  = 10s,
    step        = 10s,
    metrics     = <[sum(confirmed) \ count(confirmed) as alarmRatio,
                   sum(confirmed) as alarmCount]>,
    dummyTable  = enrichedStream,
    outputTable = groupOut,
    timeColumn  = `ts,
    keyColumn   = `lineId
)

subscribeTable(tableName=`enrichedStream, actionName="converge",
               handler=tableInsert{convEngine}, msgAsTable=true)

然后在 groupOut 上判断:alarmRatio 超过 0.6 的窗口,产出一条组级告警,同时抑制该组窗口内的设备级告警(组级优先);比例不高但有零星越限的,放行设备级。

3.3 共因的根因线索

组级告警之外,收敛过程中留下的"哪些设备、多短时间内、集中越限"的记录本身就是根因分析的线索。把每个窗口的越限设备清单存下来,事后回看:如果 200 台设备在 8 秒内集中离线,基本可以排除 200 个独立故障同时发生的可能,指向供电/网络这类共享基础设施——收敛不是丢信息,是把信息换了一种更集中的形态


四、第三道闸:分级、静默与升级

前两道闸解决"报什么",这一节解决"怎么送"。

4.1 分级路由

不是所有告警都值得打断人。按影响程度分级,不同级别走不同通道:

级别

定义

通知方式

提示

偏离正常但无风险

只进看板,不通知

警告

需要关注,可等班次交接

班次汇总推送

严重

影响生产,需当日处理

即时消息 + 工单

紧急

安全风险,需立即响应

电话/短信 + 工单

分级的依据通常是"规则类型 × 设备关键度"——关键设备的警告可能比边缘设备的严重更高优先级。设备关键度放在设备档案表(PKEY 引擎)里,告警产生时 join 上。

4.2 维护窗口静默

计划检修期间,设备轮流停启,会产生大量"离线""参数越限"的伪告警。做法是维护一张静默配置表,检修计划登记进去,告警发送前 join 这张表,落在静默窗口内的降级为"仅记录,不通知":

代码语言:javascript
复制
// 静默配置表:哪台设备、哪个时间窗、静默原因
share table(1:0, `deviceId`startTime`endTime`reason,
            [SYMBOL, DATETIME, DATETIME, SYMBOL]) as silenceCfg

// 发送前过滤:命中静默窗的只记录不通知
toNotify = select a.deviceId, a.ts, a.alarmState,
                  iif(s.deviceId != NULL, 0, 1) as shouldNotify
           from aj(alertOut as a, silenceCfg as a2, `deviceId) ...
           // 简化示意:实际按 deviceId + 当前时间是否落在 [startTime, endTime] 过滤

// 静默配置由排程系统写入,也可用 scheduleJob 定时同步 CMMS 的检修计划

一个容易忽略的细节:静默解除后要做一次状态对账——检修期间被静默的告警里,可能有已经真实恶化的,解除静默时把仍未恢复的重新评估一遍,不能一静了之。

4.3 升级机制

严重告警发出后 N 分钟无人确认,自动升级(通知下一级负责人)。确认与升级的状态流转放在工单表里做(用 IMOLTP 事务引擎存告警工单,保证流转一致性),流引擎只负责把"超时未确认"这个事件检测出来:

代码语言:javascript
复制
// 每 30 秒扫一次未确认的严重告警,超 5 分钟未确认 → 推升级流
def escalateCheck() {
    pending = select ticketId, deviceId, createdAt
              from loadTable("dfs://iot_txn", "alertTicket")
              where status = `SEVERE_UNCONFIRMED
                and createdAt < now() - 5m
    if (pending.size() > 0) {
        loadTable("dfs://iot", "escalationStream").tableInsert(pending)
    }
}
scheduleJob("escalate_scan", "严重告警升级扫描", escalateCheck,
            now() + 30s, now() + 1d, 'S', "30s")

五、度量告警治理:指标与调优循环

三道闸装完,怎么知道有没有效?告警治理不是一次性工程,需要指标 + 循环

5.1 四个核心指标

代码语言:javascript
复制
// 每日告警治理指标
dailyMetrics = select
    date(createdAt)                                   as day,
    count(*)                                          as total_alerts,
    // 噪音比:自动恢复(从未被处理)的告警占比 —— 越低越好
    sum(iif(status = `AUTO_RECOVERED, 1, 0)) \ count(*)   as noise_ratio,
    // 可行动率:引发工单的告警占比 —— 越高越好
    sum(iif(hasTicket, 1, 0)) \ count(*)                  as actionable_rate,
    // 平均确认时长(分钟):告警到被确认的耗时
    avg((confirmedAt - createdAt) \ 60000)                as mtta_min
from loadTable("dfs://iot_txn", "alertTicket")
group by date(createdAt)
  • 告警总量:治理前后对比最直观的数字
  • 噪音比:自动恢复占比高,说明阈值/滞回带宽没调好,告警在报"自然波动"
  • 可行动率:引发处理动作的告警占比。健康的目标是大部分告警都值得看
  • MTTA(平均确认时长):告警疲劳的直接信号——MTTA 涨了,说明人已经不看告警了

5.2 调优循环

指标每周看一次,按这个循环调:

代码语言:javascript
复制
告警量上涨 → 看是哪个来源(重复/抖动/群发)
    → 重复多:检查去重边沿逻辑有没有漏
    → 抖动多:加宽滞回缓冲带,或提高持续确认时长
    → 群发多:检查组收敛的分组维度(按产线?按配电回路?)
    → 都不是:规则本身过敏感,调阈值或降级

治理的本质是持续把"不值得看的告警"挡在通知之外,让每条送达的告警都配得上一次人工关注。


六、写在最后

告警这件事有三个层次:第一层是把告警做出来——从海量数据到实时预警,这是大多数平台起步时攻坚的部分;第二层再往前一步,预测故障——在告警发生之前预判风险;而容易被忽略的第三层是:告警做出来之后,怎么让它值得被看。本文写的就是这一段——它没有前两层技术含量高,却直接决定告警系统有没有人信。

三道闸门,一句话总结:

  • 第一道闸管单条:去重(只报边沿)、持续确认(越限且持续才算数)、滞回(触发与恢复双阈值)
  • 第二道闸管一群:共因收敛,设备级上卷到组级,200 条变 1 条
  • 第三道闸管送达:分级路由、维护窗口静默、超时升级

技术上的共同点是:告警治理的核心是有状态判断。去重要记住"是否已告警",持续确认要数"连续越限几次",滞回要维护"当前状态",收敛要统计"组内比例"——这些都是跨消息的状态,@state 状态函数 + 响应式状态引擎的组合正是为此设计的:每个设备一份独立状态,增量更新,毫秒级完成判断。

最后记住那个反直觉的结论:告警系统的价值不在能报多少,而在每一条都值得看。每天几千条的告警系统不是监控完善,是监控失效——它已经训练所有人学会了忽略它。治理的目标,是把告警量压回到"每条都配得上一次人工关注"的水平。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 摘要
  • 一、告警风暴从哪来:三个爆炸源
  • 二、第一道闸:单条告警的卫生
    • 2.1 同源去重:未恢复,不重报
    • 2.2 持续时长确认:越限且持续,才算数
    • 2.3 滞回抑制:触发阈值 ≠ 恢复阈值
    • 2.4 装上引擎
  • 三、第二道闸:群发告警的共因收敛
    • 3.1 思路:从设备级上卷到组级
    • 3.2 两级引擎编排
    • 3.3 共因的根因线索
  • 四、第三道闸:分级、静默与升级
    • 4.1 分级路由
    • 4.2 维护窗口静默
    • 4.3 升级机制
  • 五、度量告警治理:指标与调优循环
    • 5.1 四个核心指标
    • 5.2 调优循环
  • 六、写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档