4. 告警去重:基于指纹的去重算法、窗口内重复告警合并策略
告警去重,说白了就是解决“同一个问题反复报,运维被刷屏”的痛点。
我在项目里见过最夸张的一次——某个磁盘 IO 抖动,5 分钟弹了 3000 多条一模一样的告警。值班同学直接放弃了看告警,等到真正挂了才发现。所以,去重不是锦上添花,是保命技能。
4.1 为什么要做告警去重?
先理清楚重复告警的来源:
- 瞬时抖动:比如网络闪断、CPU 毛刺,几秒后又恢复了
- 探针批量上报:同一台机器上的多个 agent 同时检测到同一个问题
- 逐级聚合延迟:底层告警来了,策略还没收敛完又触发了第二次评估
- 手动重试/模拟:操作失误或者调试时反复触发
去重能帮你省下三样东西:存储、网络带宽、人的注意力。尤其是注意力,比服务器贵多了。
核心目标:对同一类事件的 N 次告警,只保留一条(或合并信息),其余丢弃/静默。
4.2 基于指纹的去重算法
指纹去重,就是给每条告警算一个唯一 ID。如果两条告警的指纹相同,我们就认为它们是同一个事件的重复。
4.2.1 指纹怎么生成的?
我个人习惯的方案是这样的:
- 选取关键字段:主机名、告警名、告警级别、策略 ID、标签(labels)…… 不是你看到的所有字段都要加进去。
- 标准化处理:把字段值统一排序、去掉空格、转小写。否则“host=web01”和“host= web01”会生成两个指纹。
- 拼接+哈希:用分隔符拼接后,计算 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 图把指纹去重 + 窗口合并的完整流程串了起来。你可以拿去画架构图时参考。
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 小结
告警去重其实就三件事:
- 算指纹——标准化 + 哈希,确保相同事件指纹一致
- 开窗口——滑动/静默/计数,控制重复间隔
- 合并输出——保留计数和最近时间,不丢信息
做到这三点,告警量至少能压下去七八成。剩下的精细化调优,就是根据业务场景微调指纹字段和窗口长度了。嗯,这块没有银弹,但有了这套框架,至少你不会被告警淹没。