AIOps|日志告警去重与降噪:基于相似度的告警合并策略

举报
江南清风起 发表于 2026/11/03 22:59:45 2026/11/03
【摘要】 AIOps|日志告警去重与降噪:基于相似度的告警合并策略 告警洪流问题在大型运维系统中,一次网络抖动可能触发数百条告警——交换机端口Down告警、服务器不可达告警、应用健康检查失败告警、数据库连接超时告警,海量的告警邮件和短信瞬间涌入值班人员的通知渠道。这种现象被称为"告警洪流"或"告警风暴",它带来三个严重后果:告警疲劳:值班人员在短时间内收到大量告警,无法分辨哪些是紧急的、哪些是派生的...

AIOps|日志告警去重与降噪:基于相似度的告警合并策略


告警洪流问题

在大型运维系统中,一次网络抖动可能触发数百条告警——交换机端口Down告警、服务器不可达告警、应用健康检查失败告警、数据库连接超时告警,海量的告警邮件和短信瞬间涌入值班人员的通知渠道。这种现象被称为"告警洪流"或"告警风暴",它带来三个严重后果:

  1. 告警疲劳:值班人员在短时间内收到大量告警,无法分辨哪些是紧急的、哪些是派生的,导致关键告警被淹没在噪声中
  2. 响应延迟:大量低优先级告警占据了注意力和处理时间,真正需要紧急处置的问题反而被推迟
  3. 信任丧失:频繁的误报和重复告警让运维团队逐渐忽视告警系统,形成"狼来了"效应

告警去重与降噪的目标是:在告警到达通知渠道之前,自动识别并合并相似告警,将数百条原始告警压缩为几条有意义的摘要通知。核心技术手段是文本相似度计算和告警聚类合并。

文本相似度算法

余弦相似度

余弦相似度通过计算两个向量的夹角余弦值来衡量相似程度,适用于TF-IDF向量化后的文本比较。它的优势是对文本长度不敏感,能够捕捉关键词重叠程度。

import math
from collections import Counter

def tokenize(text):
    """简单分词:适用于英文和中英混合告警文本"""
    import re
    # 提取英文单词和数字
    tokens = re.findall(r'[a-zA-Z_]+|\d+', text.lower())
    # 过滤过短的token
    return [t for t in tokens if len(t) > 1]

def tfidf_vectorize(texts):
    """将文本列表转换为TF-IDF向量"""
    # 统计文档频率
    doc_freq = Counter()
    for text in texts:
        unique_tokens = set(tokenize(text))
        for token in unique_tokens:
            doc_freq[token] += 1

    total_docs = len(texts)
    idf = {token: math.log(total_docs / (1 + freq))
           for token, freq in doc_freq.items()}

    # 构建TF-IDF向量
    vectors = []
    for text in texts:
        tokens = tokenize(text)
        tf = Counter(tokens)
        vec = {token: (count / len(tokens)) * idf.get(token, 0)
               for token, count in tf.items()}
        vectors.append(vec)

    return vectors

def cosine_similarity(vec1, vec2):
    """计算两个稀疏向量的余弦相似度"""
    dot_product = sum(vec1.get(k, 0) * vec2.get(k, 0)
                      for k in set(vec1) & set(vec2))
    norm1 = math.sqrt(sum(v * v for v in vec1.values()))
    norm2 = math.sqrt(sum(v * v for v in vec2.values()))

    if norm1 == 0 or norm2 == 0:
        return 0.0
    return dot_product / (norm1 * norm2)

编辑距离

编辑距离(Levenshtein Distance)衡量两个字符串之间通过插入、删除、替换操作转换所需的最小步数。它适合短文本告警的精确比对,特别是当告警文本只有变量值不同时。

def levenshtein_distance(s1, s2):
    """计算两个字符串的编辑距离"""
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = 1 + min(dp[i-1][j],    # 删除
                                   dp[i][j-1],    # 插入
                                   dp[i-1][j-1])  # 替换

    return dp[m][n]

def normalized_similarity(s1, s2):
    """归一化编辑距离为0-1之间的相似度分数"""
    max_len = max(len(s1), len(s2))
    if max_len == 0:
        return 1.0
    return 1.0 - levenshtein_distance(s1, s2) / max_len

Jaccard相似度

Jaccard相似度计算两个集合的交集与并集之比,适合基于token集合的快速比对:

def jaccard_similarity(text1, text2):
    """基于token集合的Jaccard相似度"""
    set1 = set(tokenize(text1))
    set2 = set(tokenize(text2))

    intersection = set1 & set2
    union = set1 | set2

    if not union:
        return 0.0
    return len(intersection) / len(union)

算法选择策略

不同相似度算法各有适用场景:

算法 适用场景 优势 局限
余弦相似度 长文本告警(>10个词) 关注关键词重叠,忽略长度差异 需要预计算IDF
编辑距离 短文本告警(模板化文本) 精确字符级比对 计算复杂度O(m*n)
Jaccard 快速粗筛 计算简单快速 忽略词频和顺序

实践中常采用两级策略:先用Jaccard做粗筛快速排除明显不相似的告警对,再用余弦相似度或编辑距离做精细比较。

告警聚类

基于密度的聚类(DBSCAN)

告警聚类不需要预先知道类别数量,DBSCAN是自然选择——它能自动发现告警簇,并将噪声点(孤立告警)单独保留。对于告警去重场景,相似度矩阵替代欧氏距离即可。

import numpy as np
from collections import defaultdict

class AlertClusterer:
    """基于相似度矩阵的告警聚类"""

    def __init__(self, sim_threshold=0.65, min_samples=2):
        self.sim_threshold = sim_threshold
        self.min_samples = min_samples

    def cluster(self, alerts):
        """对告警列表进行聚类
        alerts: [{'id': str, 'text': str, 'source': str, 'severity': str, ...}]
        返回: list of clusters, 每个cluster是alert列表
        """
        n = len(alerts)
        if n == 0:
            return []

        # 预计算TF-IDF向量
        texts = [a.get('text', a.get('title', '')) for a in alerts]
        vectors = tfidf_vectorize(texts)

        # 构建相似度邻接关系
        neighbors = defaultdict(list)
        for i in range(n):
            for j in range(i + 1, n):
                sim = cosine_similarity(vectors[i], vectors[j])
                if sim >= self.sim_threshold:
                    neighbors[i].append(j)
                    neighbors[j].append(i)

        # DBSCAN聚类
        visited = set()
        clusters = []
        noise = []

        for i in range(n):
            if i in visited:
                continue

            # 检查是否为核心点
            if len(neighbors[i]) + 1 < self.min_samples:
                noise.append(i)
                visited.add(i)
                continue

            # 扩展簇
            cluster = [i]
            visited.add(i)

            queue = list(neighbors[i])
            while queue:
                j = queue.pop(0)
                if j in visited:
                    continue
                visited.add(j)
                cluster.append(j)

                if len(neighbors[j]) + 1 >= self.min_samples:
                    for k in neighbors[j]:
                        if k not in visited:
                            queue.append(k)

            clusters.append([alerts[idx] for idx in cluster])

        # 噪声点各自成簇
        for idx in noise:
            clusters.append([alerts[idx]])

        return clusters

    def get_cluster_summary(self, cluster):
        """生成告警簇的摘要信息"""
        if not cluster:
            return {}

        # 统计来源
        sources = Counter(a.get('source', 'unknown') for a in cluster)

        # 选取代表性告警(最长的文本通常信息最全)
        representative = max(cluster, key=lambda a: len(a.get('text', '')))

        # 严重程度取最高
        severity_rank = {'critical': 4, 'high': 3, 'warning': 2, 'info': 1}
        max_severity = max(cluster,
            key=lambda a: severity_rank.get(
                a.get('severity', 'info').lower(), 0))

        return {
            'count': len(cluster),
            'representative_alert': representative,
            'sources': dict(sources),
            'max_severity': max_severity.get('severity', 'info'),
            'first_seen': min(a.get('timestamp', '') for a in cluster),
            'last_seen': max(a.get('timestamp', '') for a in cluster),
        }

聚类效果评估

# 使用示例
alerts = [
    {'id': '1', 'text': 'Host web-01 CPU usage 95% exceeded threshold',
     'source': 'prometheus', 'severity': 'warning'},
    {'id': '2', 'text': 'Host web-02 CPU usage 92% exceeded threshold',
     'source': 'prometheus', 'severity': 'warning'},
    {'id': '3', 'text': 'Host db-01 CPU usage 98% exceeded threshold',
     'source': 'prometheus', 'severity': 'critical'},
    {'id': '4', 'text': 'Memory usage high on web-01 node',
     'source': 'zabbix', 'severity': 'warning'},
    {'id': '5', 'text': 'Disk space low on /var/lib/docker',
     'source': 'node_exporter', 'severity': 'info'},
    {'id': '6', 'text': 'Host web-03 CPU usage 91% exceeded threshold',
     'source': 'prometheus', 'severity': 'warning'},
    {'id': '7', 'text': 'Connection refused to redis-prod-01 port 6379',
     'source': 'healthcheck', 'severity': 'critical'},
    {'id': '8', 'text': 'Connection refused to redis-prod-02 port 6379',
     'source': 'healthcheck', 'severity': 'critical'},
]

clusterer = AlertClusterer(sim_threshold=0.55, min_samples=2)
clusters = clusterer.cluster(alerts)

for i, cluster in enumerate(clusters):
    summary = clusterer.get_cluster_summary(cluster)
    print(f"簇 {i}: {summary['count']}条告警")
    print(f"  代表: {summary['representative_alert']['text'][:60]}")
    print(f"  最高级别: {summary['max_severity']}")
    print(f"  来源: {summary['sources']}")

预期输出中,告警1/2/3/6会被聚为CPU告警簇,7/8被聚为Redis连接拒绝簇,4和5各自独立。

时间窗口合并

滑动窗口去重

告警聚类后,还需要在时间维度上做窗口合并——同一告警簇在短时间窗口内的重复实例只通知一次,同时记录重复次数。这既减少了通知数量,又保留了频率信息供异常判断。

from datetime import datetime, timedelta
from collections import defaultdict

class TimeWindowAlertMerger:
    """基于时间窗口的告警合并器"""

    def __init__(self, window_minutes=5):
        self.window = timedelta(minutes=window_minutes)
        self.pending = defaultdict(list)  # cluster_key -> [alerts]

    def _cluster_key(self, alert):
        """基于来源和严重程度生成粗粒度簇键"""
        return f"{alert.get('source', '')}:{alert.get('severity', '')}"

    def add_alert(self, alert):
        """添加告警到合并器,返回是否应该立即通知"""
        key = self._cluster_key(alert)
        now = datetime.fromisoformat(
            alert.get('timestamp', datetime.now().isoformat()))

        # 清理过期窗口
        self.pending[key] = [
            a for a in self.pending[key]
            if now - datetime.fromisoformat(a.get('timestamp',
                datetime.now().isoformat())) < self.window
        ]

        # 检查窗口内是否已有类似告警
        self.pending[key].append(alert)

        # 如果是窗口内第一条,返回True(需要通知)
        if len(self.pending[key]) == 1:
            return True
        return False

    def flush_window(self, key=None):
        """强制刷新窗口,返回合并后的摘要"""
        if key:
            keys = [key]
        else:
            keys = list(self.pending.keys())

        results = []
        for k in keys:
            alerts = self.pending.get(k, [])
            if not alerts:
                continue

            results.append({
                'cluster_key': k,
                'count': len(alerts),
                'first_time': alerts[0].get('timestamp', ''),
                'last_time': alerts[-1].get('timestamp', ''),
                'alerts': alerts
            })
            self.pending[k] = []

        return results

滚动窗口聚合

对于高频率告警,滚动窗口比滑动窗口更适合——每个固定时间区间输出一次聚合结果:

class RollingAlertWindow:
    """滚动窗口告警聚合"""

    def __init__(self, window_seconds=60):
        self.window_seconds = window_seconds
        self.buffer = []
        self.window_start = datetime.now()

    def add(self, alert):
        """添加告警,返回None(仍在窗口内)或合并结果(窗口结束)"""
        now = datetime.now()
        alert_time = datetime.fromisoformat(
            alert.get('timestamp', now.isoformat()))
        self.buffer.append(alert)

        if (now - self.window_start).total_seconds() >= self.window_seconds:
            result = self._merge()
            self.buffer = []
            self.window_start = now
            return result
        return None

    def _merge(self):
        """合并窗口内所有告警"""
        if not self.buffer:
            return None

        clusterer = AlertClusterer(sim_threshold=0.55)
        clusters = clusterer.cluster(self.buffer)

        merged = []
        for cluster in clusters:
            summary = clusterer.get_cluster_summary(cluster)
            merged.append({
                'count': summary['count'],
                'representative': summary['representative_alert'],
                'sources': summary['sources'],
                'severity': summary['max_severity'],
                'time_range': f"{summary['first_seen']} ~ {summary['last_seen']}"
            })

        return {
            'window_start': self.window_start.isoformat(),
            'total_alerts': len(self.buffer),
            'cluster_count': len(merged),
            'clusters': merged
        }

相似度阈值调优

阈值对效果的影响

相似度阈值是告警合并系统最关键的超参数。阈值过高会导致相似告警无法合并(去重不足),阈值过低会导致不相关的告警被错误合并(过度聚合)。

class ThresholdOptimizer:
    """通过历史告警数据调优相似度阈值"""

    def __init__(self, labeled_alerts):
        """labeled_alerts: [{'text': str, 'label': int}, ...]
        label相同的告警属于同一簇"""
        self.labeled = labeled_alerts

    def evaluate_threshold(self, threshold):
        """评估给定阈值下的合并效果"""
        texts = [a['text'] for a in self.labeled]
        vectors = tfidf_vectorize(texts)
        n = len(texts)

        # 构建预测的簇
        union_find = list(range(n))

        def find(x):
            while union_find[x] != x:
                union_find[x] = union_find[union_find[x]]
                x = union_find[x]
            return x

        def union(x, y):
            px, py = find(x), find(y)
            if px != py:
                union_find[px] = py

        for i in range(n):
            for j in range(i + 1, n):
                sim = cosine_similarity(vectors[i], vectors[j])
                if sim >= threshold:
                    union(i, j)

        # 统计预测簇和真实簇
        pred_labels = [find(i) for i in range(n)]
        true_labels = [a['label'] for a in self.labeled]

        # 计算合并率和误合率
        merged_pairs = 0
        should_merge_pairs = 0
        false_merge_pairs = 0
        total_pairs = 0

        for i in range(n):
            for j in range(i + 1, n):
                total_pairs += 1
                if pred_labels[i] == pred_labels[j]:
                    merged_pairs += 1
                    if true_labels[i] != true_labels[j]:
                        false_merge_pairs += 1
                if true_labels[i] == true_labels[j]:
                    should_merge_pairs += 1

        merge_rate = merged_pairs / should_merge_pairs if should_merge_pairs else 0
        false_rate = false_merge_pairs / merged_pairs if merged_pairs else 0

        return {
            'threshold': threshold,
            'merge_rate': merge_rate,      # 应合并对中实际合并的比例
            'false_merge_rate': false_rate  # 合并对中错误合并的比例
        }

    def find_optimal_threshold(self, thresholds=None):
        """在候选阈值中寻找最佳值"""
        if thresholds is None:
            thresholds = [0.3, 0.4, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8]

        results = [self.evaluate_threshold(t) for t in thresholds]

        # 目标:合并率 > 0.9 且误合率 < 0.05
        best = None
        for r in results:
            if r['merge_rate'] >= 0.9 and r['false_merge_rate'] <= 0.05:
                if best is None or r['merge_rate'] > best['merge_rate']:
                    best = r
            elif best is None:
                best = r

        return best, results

动态阈值调整

不同告警来源的文本特征差异较大,固定阈值难以全局最优。可以根据告警来源或类型动态调整阈值:

class DynamicThresholdMerger:
    """按告警来源使用不同阈值的合并器"""

    DEFAULT_THRESHOLD = 0.65

    # 按来源调优的阈值表
    SOURCE_THRESHOLDS = {
        'prometheus': 0.60,   # 监控指标告警模板化程度高
        'zabbix': 0.55,       # Zabbix告警文本更统一
        'log_alert': 0.70,    # 日志告警文本变化大,需更高阈值
        'healthcheck': 0.50,  # 健康检查告警高度模板化
        'security': 0.75,    # 安全告警更谨慎,减少误合
    }

    def get_threshold(self, alert):
        source = alert.get('source', '').lower()
        return self.SOURCE_THRESHOLDS.get(source, self.DEFAULT_THRESHOLD)

    def should_merge(self, alert1, alert2):
        """根据来源动态选择阈值判断是否合并"""
        threshold = min(
            self.get_threshold(alert1),
            self.get_threshold(alert2)
        )
        vec1 = tfidf_vectorize([alert1.get('text', '')])[0]
        vec2 = tfidf_vectorize([alert2.get('text', '')])[0]
        sim = cosine_similarity(vec1, vec2)
        return sim >= threshold

完整告警去重系统

将上述组件组合为完整的告警去重管道:

from collections import Counter

class AlertDeduplicationSystem:
    """端到端告警去重与降噪系统"""

    def __init__(self, window_minutes=5, sim_threshold=0.65,
                 min_cluster_size=2):
        self.window_merger = TimeWindowAlertMerger(window_minutes)
        self.clusterer = AlertClusterer(sim_threshold, min_cluster_size)
        self.merged_count = 0
        self.total_count = 0

    def process_alert(self, alert):
        """处理单条告警,返回通知内容或None(被去重)"""
        self.total_count += 1

        # 第一层:时间窗口去重
        should_notify = self.window_merger.add_alert(alert)

        if should_notify:
            return self._format_notification(alert)
        else:
            self.merged_count += 1
            return None

    def process_batch(self, alerts):
        """批量处理告警,返回合并后的通知列表"""
        self.total_count += len(alerts)
        clusters = self.clusterer.cluster(alerts)

        notifications = []
        for cluster in clusters:
            if len(cluster) >= 2:
                self.merged_count += len(cluster) - 1

            summary = self.clusterer.get_cluster_summary(cluster)
            notification = self._format_cluster_notification(summary)
            notifications.append(notification)

        return notifications

    def _format_notification(self, alert):
        """格式化单条告警通知"""
        return (
            f"[{alert.get('severity', 'INFO').upper()}] "
            f"{alert.get('source', 'unknown')}: "
            f"{alert.get('text', alert.get('title', ''))}"
        )

    def _format_cluster_notification(self, summary):
        """格式化告警簇摘要通知"""
        alert = summary['representative_alert']
        count = summary['count']
        sources = ', '.join(f"{k}({v})" for k, v
                           in summary['sources'].items())

        header = f"[{summary['max_severity'].upper()}] 告警合并 ({count}条)"
        body = (
            f"来源: {sources}\n"
            f"时间: {summary['first_seen']} ~ {summary['last_seen']}\n"
            f"代表告警: {alert.get('text', alert.get('title', ''))}\n"
        )
        return f"{header}\n{body}"

    def get_stats(self):
        """获取去重统计"""
        return {
            'total_alerts': self.total_count,
            'deduplicated': self.merged_count,
            'dedup_rate': (self.merged_count / self.total_count
                          if self.total_count > 0 else 0)
        }

运行示例

system = AlertDeduplicationSystem(
    window_minutes=5, sim_threshold=0.55)

# 模拟一次故障引发的告警风暴
alert_storm = [
    {'text': 'Host web-01 CPU 95% exceeded 80%', 'source': 'prom',
     'severity': 'warning', 'timestamp': '2026-09-07T14:32:01'},
    {'text': 'Host web-02 CPU 93% exceeded 80%', 'source': 'prom',
     'severity': 'warning', 'timestamp': '2026-09-07T14:32:02'},
    {'text': 'Host web-03 CPU 91% exceeded 80%', 'source': 'prom',
     'severity': 'warning', 'timestamp': '2026-09-07T14:32:03'},
    {'text': 'HTTP 500 rate on api-gateway 12%', 'source': 'log',
     'severity': 'critical', 'timestamp': '2026-09-07T14:32:10'},
    {'text': 'HTTP 500 rate on api-gateway 15%', 'source': 'log',
     'severity': 'critical', 'timestamp': '2026-09-07T14:32:15'},
    {'text': 'Database connections exhausted', 'source': 'db_mon',
     'severity': 'critical', 'timestamp': '2026-09-07T14:32:20'},
]

notifications = system.process_batch(alert_storm)

print("=== 合并后的通知 ===")
for n in notifications:
    print(n)
    print("---")

stats = system.get_stats()
print(f"\n去重率: {stats['dedup_rate']:.1%} "
      f"({stats['deduplicated']}/{stats['total_alerts']} 条被合并)")

小结

告警去重与降噪是AIOps体系中最直接见效的能力之一。通过文本相似度算法(余弦相似度、编辑距离、Jaccard)度量告警文本之间的语义距离,配合密度聚类算法自动发现告警簇,再叠加时间窗口合并策略,可以将一次故障引发的上百条告警压缩为几条精准的摘要通知。相似度阈值的调优是系统效果的关键,建议基于标注历史数据寻找合并率和误合率的平衡点,并按告警来源使用差异化阈值。整套系统可以部署为Alertmanager和通知系统之间的独立中间层,在不改动现有告警规则的前提下显著降低告警噪声。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。