4. 告警去重:基于指纹的去重算法、窗口内重复告警合并策略

告警去重,说白了就是解决“同一个问题反复报,运维被刷屏”的痛点。

我在项目里见过最夸张的一次——某个磁盘 IO 抖动,5 分钟弹了 3000 多条一模一样的告警。值班同学直接放弃了看告警,等到真正挂了才发现。所以,去重不是锦上添花,是保命技能。

4.1 为什么要做告警去重?

先理清楚重复告警的来源:

  • 瞬时抖动:比如网络闪断、CPU 毛刺,几秒后又恢复了
  • 探针批量上报:同一台机器上的多个 agent 同时检测到同一个问题
  • 逐级聚合延迟:底层告警来了,策略还没收敛完又触发了第二次评估
  • 手动重试/模拟:操作失误或者调试时反复触发

去重能帮你省下三样东西:存储、网络带宽、人的注意力。尤其是注意力,比服务器贵多了。

核心目标:对同一类事件的 N 次告警,只保留一条(或合并信息),其余丢弃/静默。

4.2 基于指纹的去重算法

指纹去重,就是给每条告警算一个唯一 ID。如果两条告警的指纹相同,我们就认为它们是同一个事件的重复。

4.2.1 指纹怎么生成的?

我个人习惯的方案是这样的:

  1. 选取关键字段:主机名、告警名、告警级别、策略 ID、标签(labels)…… 不是你看到的所有字段都要加进去。
  2. 标准化处理:把字段值统一排序、去掉空格、转小写。否则“host=web01”和“host= web01”会生成两个指纹。
  3. 拼接+哈希:用分隔符拼接后,计算 SHA256 或 MD5。我建议用 SHA256,碰撞概率更低。

当年我刚接手告警平台时,遇到过因为标签顺序不同而导致的去重失效——一个标签是env=prod, region=sh,另一个是region=sh, env=prod,指纹结果完全不同。后来我加了一行sort(labels.items()),问题就解决了。

简化版指纹生成示例(Python)

import hashlib, json

def build_fingerprint(alert):
    # 只取决定“是否相同”的字段
    keys = sorted([
        alert.get('hostname', ''),
        alert.get('alertname', ''),
        alert.get('severity', ''),
        json.dumps(alert.get('labels', {}), sort_keys=True),
    ])
    raw = '||'.join(keys).lower().strip()
    return hashlib.sha256(raw.encode()).hexdigest()

4.2.2 指纹匹配与去重逻辑

有了指纹,接下来的流程很简单:

  • 每条告警到达时,先算指纹
  • 查一下这张指纹在当前窗口内是否已经出现过
  • 如果出现过→重复告警,做合并/丢弃
  • 如果没出现过→新告警,入库并启动窗口计时

这里有一个容易被忽略的细节:指纹要不要区分“已恢复”的状态?我个人建议是:恢复告警单独算指纹(比如在字段里加一个status=firing/resolved),否则恢复告警会被误当成重复。

4.3 窗口内重复告警合并策略

有了指纹,还得配合窗口策略才能起作用。窗口决定“多长时间内的重复算作同一次事件”。

4.3.1 常见的三种窗口模式

策略类型 工作原理 适用场景
滑动时间窗口 以当前告警时间为准,往前看 N 秒/分钟,如果有相同指纹的告警,则合并 一般性抖动告警,比如 CPU 超 95% 持续 30 秒后又降下去
固定静默窗口 一旦收到某个指纹的告警,接下来的 M 分钟内相同指纹全部静默 已知问题正在重启/变更,不需要反复报
计数窗口 收集 N 条相同指纹告警后,生成一条聚合后的告警(附带计数和最新时间) 业务错误码频发,每条错误码都一样

我自己在实际中用的最多的是滑动时间窗口 + 阈值。比如设定窗口 5 分钟,窗口内最多产生 1 条告警。如果 5 分钟内又来一次相同指纹,就更新这条告警的 last_seen_at 和 count,但不再创建新告警。

4.3.2 窗口状态的存储

窗口数据放在哪?快速读写是关键。

  • Redis:用SETEX加过期时间,指纹作为 key,数据结构用 hash 存计数和时间。读取 O(1),自动过期。
  • 本地内存 LRU Cache:适合单机部署的告警引擎,注意加锁。
  • Lua + 预计算:有些极端场景需要毫秒级去重,可以把窗口判重逻辑写在 Redis Lua 脚本里。

我曾经踩过的坑:窗口太小,抖动告警照样刷屏;窗口太大,同一个问题恢复后再次触发却被静默。我建议窗口长度取告警评估间隔的 2~3 倍,比如每 60 秒评估一次,窗口可以设为 180 秒。然后根据实际调优。

4.3.3 合并时保留有用信息

完全丢弃重复告警会丢失上下文。我更喜欢“合并”而不是“静默”。比如:

  • 保留首次触发时间
  • 保留最近一次触发时间
  • 累计重复次数
  • 如果告警内容有差异(比如错误码不同),可以聚合并附加到备注

这样运维人员看到的是:

主机 cpu_high (已重复 23 次)
首次: 14:01:23
最近: 14:08:45
错误码: 503 (21 次), 504 (2 次)

直观,而且不会淹没细节。

4.4 一张流程图说清全局

下面这张 SVG 图把指纹去重 + 窗口合并的完整流程串了起来。你可以拿去画架构图时参考。

告警去重 & 窗口合并流程图 原始告警到达 计算指纹 (SHA256) 查询窗口:指纹在窗口内吗? 是 → 合并/静默 否 → 写入窗口,创建新告警 下一步:通知/路由 窗口自动过期 (Redis TTL)

4.5 实践中的调优参数

讲完理论,我们落地时需要关注几个可配置参数。我一般把它们放在告警规则的 YAML 里:

fingerprint_fields:
  - hostname
  - alertname
  - severity
  - labels.error_code

window_seconds: 300       # 滑动窗口 5 分钟
merge_max_count: 100      # 最多合并 100 次,超过则强制再次告警
merge_update_interval: 60 # 每 60 秒更新一次 last_seen,减少 Redis 写入

一个小技巧:如果告警量极大(比如每秒上千条),可以先用布隆过滤器做一层快速去重,再用精确指纹做二次确认。布隆过滤器允许极小的误判率(0.1%),但能大幅减少 Redis 查询压力。

4.5.1 别忘了“去重失效”的场景

我曾经遇到一个线上事故:某台机器内存泄漏,告警策略每 60 秒评估一次,每次产生一条新告警。因为泄漏是持续升高的,每条告警的 CPU 使用率值不同,指纹字段中包含了value,导致指纹总在变,去重完全没发挥作用。后来我改策略:数值型字段不参与指纹计算,而是作为合并时的补充信息。去重就正常了。

经验法则:指纹只包含标识性字段(主机、告警名、标签 key),不要包含数值型瞬时值。数值保留在告警详情里,每次合并时更新。

4.6 小结

告警去重其实就三件事:

  • 算指纹——标准化 + 哈希,确保相同事件指纹一致
  • 开窗口——滑动/静默/计数,控制重复间隔
  • 合并输出——保留计数和最近时间,不丢信息

做到这三点,告警量至少能压下去七八成。剩下的精细化调优,就是根据业务场景微调指纹字段和窗口长度了。嗯,这块没有银弹,但有了这套框架,至少你不会被告警淹没。