欢迎光临
我们一直在努力

从发现到定位的最后一公里:机器学习时序异常检测与根因分析结合实践

从发现到定位的最后一公里:机器学习时序异常检测与根因分析结合实践

信息图

一、为什么要结合?

1.1 孤立的痛点

先看两个常见的失败场景:

场景一:异常洪水

03:15 Prometheus告警:CPU > 90%
03:16 Prometheus告警:内存 > 85%
03:17 Prometheus告警:磁盘IO > 95%
03:18 Prometheus告警:延迟P99 > 2s
… 接下来10分钟,又触发了20条告警

值班工程师面对一堆告警,无从下手。这些指标到底哪个是因、哪个是果?

场景二:有异常无告警

服务响应变慢,用户投诉
但所有指标都在阈值范围内
——因为没有达到告警阈值

这就是传统阈值告警的盲区。而机器学习时序检测可以捕捉到"指标虽然没超阈值,但趋势已经明显异常"的情况。

二、系统设计:从检测到关联再到定位

2.1 总体架构

flowchart TD
A["[时序指标]"] –> B["异常检测(多算法)"] –> C["异常事件"]
D["[错误日志]"] –> E["错误聚合(聚类)"] –> F["错误模式"]
C –> G["异常关联引擎"]
F –> G
G –> H["LLM根因分析器"] –> I["根因分析报告"]

2.2 多算法时序异常检测

单一算法容易误判,我们采用多算法投票机制:

# multi_detector.py — 多算法异常检测
import numpy as np
from scipy import stats
from sklearn.ensemble import IsolationForest
import pandas as pd

class MultiAlgorithmDetector:
"""多算法异常检测引擎"""

def __init__(self):
self.detectors = {
'3sigma': self._detect_3sigma,
'mad': self._detect_mad,
'iqr': self._detect_iqr,
'isolation_forest': self._detect_iforest
}

def _detect_3sigma(self, series: pd.Series):
"""3-sigma检测"""
mean = series.mean()
std = series.std()
anomalies = (series > mean + 3*std) | (series < mean – 3*std)
return anomalies, mean, std

def _detect_mad(self, series: pd.Series):
"""MAD(中位数绝对偏差)检测,对异常值更鲁棒"""
median = series.median()
mad = np.median(np.abs(series – median))
# 阈值:median ± 3 * 1.4826 * MAD
threshold = 3 * 1.4826 * mad
anomalies = (series > median + threshold) | (series < median – threshold)
return anomalies, median, threshold

def _detect_iqr(self, series: pd.Series):
"""IQR四分位距检测"""
q1 = series.quantile(0.25)
q3 = series.quantile(0.75)
iqr = q3 – q1
lower = q1 – 1.5 * iqr
upper = q3 + 1.5 * iqr
anomalies = (series < lower) | (series > upper)
return anomalies, lower, upper

def _detect_iforest(self, series: pd.Series):
"""Isolation Forest检测"""
values = series.values.reshape(-1, 1)
model = IsolationForest(contamination=0.05, random_state=42)
preds = model.fit_predict(values)
anomalies = pd.Series(preds == -1, index=series.index)
return anomalies, None, None

def ensemble_detect(self, series: pd.Series,
min_votes: int = 2):
"""集成投票:至少2个算法判定为异常才算异常"""
votes = np.zeros(len(series))

for name, detector in self.detectors.items():
anomalies, _, _ = detector(series)
votes += anomalies.astype(int)

# 投票决定
final_anomaly = votes >= min_votes
anomaly_score = votes / len(self.detectors)

return {
'is_anomaly': final_anomaly,
'confidence': anomaly_score,
'votes': votes,
'details': {name: detector(series)[0].tolist()
for name, detector in self.detectors.items()}
}

2.3 日志错误模式聚类

# log_cluster.py — 日志错误模式聚类
import re
from collections import Counter
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import DBSCAN

class ErrorLogCluster:
"""错误日志模式聚类"""

def __init__(self):
self.patterns = {}
self.vectorizer = TfidfVectorizer(
analyzer='char_wb',
ngram_range=(3, 5),
max_features=1000
)

def normalize_log(self, message: str) -> str:
"""将变量部分替换为占位符"""
# 替换IP地址
message = re.sub(r'\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}', '{ip}', message)
# 替换数字
message = re.sub(r'\\d+', '{n}', message)
# 替换UUID
message = re.sub(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
'{uuid}', message)
return message

def cluster(self, log_entries: list) -> dict:
"""聚类错误日志"""
messages = [entry['message'] for entry in log_entries]
normalized = [self.normalize_log(m) for m in messages]

# TF-IDF向量化 + DBSCAN聚类
X = self.vectorizer.fit_transform(normalized)
clustering = DBSCAN(eps=0.3, min_samples=2, metric='cosine')
labels = clustering.fit_predict(X)

# 统计每个聚类的数量和样本
clusters = {}
for i, label in enumerate(labels):
if label == -1:
continue # 噪点忽略
if label not in clusters:
clusters[label] = {
'count': 0,
'pattern': normalized[i],
'sample_message': messages[i]
}
clusters[label]['count'] += 1

# 按数量排序
sorted_clusters = sorted(
clusters.values(),
key=lambda x: x['count'],
reverse=True
)

return {
'total_errors': len(log_entries),
'clusters': sorted_clusters[:10],
'unique_patterns': len(clusters)
}

2.4 异常关联引擎

这是最关键的一步——把指标异常和日志错误关联起来:

# anomaly_correlator.py — 异常关联引擎
from datetime import datetime, timedelta
import numpy as np

class AnomalyCorrelator:
"""异常关联引擎:找到指标异常和日志错误的因果关系"""

def __init__(self, time_window: int = 5):
self.time_window = time_window # 关联时间窗口(分钟)

def correlate(self, metric_anomalies: dict,
log_clusters: dict,
time_range: tuple) -> dict:
"""核心关联逻辑"""
correlations = []

# 对每个异常指标,检查同一时间窗口内的日志模式
for metric_name, anomaly_info in metric_anomalies.items():
anomaly_times = anomaly_info['anomaly_times']

for atime in anomaly_times:
window_start = atime – timedelta(minutes=self.time_window)
window_end = atime + timedelta(minutes=self.time_window)

# 找这个时间窗口内的错误日志
matching_errors = [
c for c in log_clusters['clusters']
if any(window_start <= e['timestamp'] <= window_end
for e in c.get('entries', []))
]

if matching_errors:
correlations.append({
'metric': metric_name,
'anomaly_time': atime.isoformat(),
'anomaly_value': anomaly_info['value'],
'baseline': anomaly_info['baseline'],
'deviation': f"{((anomaly_info['value'] / anomaly_info['baseline']) – 1) * 100:.1f}%",
'related_errors': [
{
'pattern': c['pattern'],
'count': c['count'],
'sample': c['sample_message']
}
for c in matching_errors
],
'correlation_strength': len(matching_errors) / len(log_clusters['clusters'])
})

# 按关联强度排序
correlations.sort(key=lambda x: x['correlation_strength'], reverse=True)

return {
'total_correlations': len(correlations),
'top_correlations': correlations[:5],
'uncorrelated_anomalies': self._find_uncorrelated(
metric_anomalies, correlations
)
}

def _find_uncorrelated(self, anomalies, correlations):
"""找出来关联不上的异常(可能是未知问题)"""
correlated_metrics = {c['metric'] for c in correlations}
return [
name for name in anomalies
if name not in correlated_metrics
]

2.5 LLM根因分析

# llm_analyzer.py — LLM根因分析器
class LLMRootCauseAnalyzer:
"""基于关联结果,用LLM做根因分析"""

def analyze(self, correlation_result: dict,
service_name: str) -> str:
"""生成根因分析"""
correlations = correlation_result['top_correlations']

# 按时间线排序
timeline = sorted(correlations,
key=lambda x: x['anomaly_time'])

# 找到最早出现的异常(极有可能是根因)
earliest = timeline[0] if timeline else None

prompt = f"""根据以下关联分析结果,判断{service_name}的故障根因:

## 执行时间线(按时间排序)
{self._format_timeline(timeline)}

## 最早出现的异常
{json.dumps(earliest, indent=2, ensure_ascii=False) if earliest else '无'}

## 未关联的异常指标
{json.dumps(correlation_result.get('uncorrelated_anomalies', []), ensure_ascii=False)}

请分析:
1. 最早异常的指标是什么?它是否可能是根因?
2. 其他异常是指标级联效应还是独立事件?
3. 给出最可能的根因(最多3个,按概率排序)
4. 下一步排查建议
"""
# 调用LLM…
return self._call_llm(prompt)

三、生产案例

在一次生产故障中,我们的系统检测到了以下关联:

flowchart TD
Start["检测到5个指标异常,2个错误日志模式"] –> Analysis["关联分析结果"]

subgraph AnalysisDetails [关联分析详情]
direction TB
A1["1. [最早] order-db 活跃连接数 50→200 (16:30:05)"] –> A1_Err["关联错误:conn pool exhausted (count: 847)"]
A1 –> A1_Str["关联强度:0.83"]

A2["2. order-service CPU 40%→85% (16:30:30)"] –> A2_Err["关联错误:connection timeout (count: 423)"]
A2 –> A2_Str["关联强度:0.67"]

A3["3. api-gateway 延迟P99 80ms→3.2s (16:31:00)"] –> A3_Err["关联错误:upstream timeout (count: 215)"]
A3 –> A3_Str["关联强度:0.54"]
end

Analysis –> RootCause["根因概率"]

subgraph RootCauseDetails [根因分析]
direction TB
RC1["1. 数据库连接池耗尽 — 80%<br/>(最早异常,关联最强)"]
RC2["2. 数据库慢查询 — 15%"]
RC3["3. 网络问题 — 5%"]
end

RootCause –> RC1
RootCause –> RC2
RootCause –> RC3

RC1 –> Action["排查建议"]

subgraph ActionDetails [排查步骤]
direction TB
S1["1. 检查MySQL max_connections和连接池配置"]
S2["2. 查看慢查询日志"]
S3["3. 检查是否有未关闭的事务"]
end

Action –> S1
Action –> S2
Action –> S3

实际根因:数据库连接池max_connections配置为200,但应用实例从3个扩容到10个时,每个实例的连接池扩大,总和超过了200。

四、总结

从"多个维度独立检测"到"跨维度关联分析",这是运维可观测性从L2向L3演进的关键一步。机器学习做时序检测发现异常,日志聚类聚合错误模式,关联引擎连接指标和日志,LLM给出最终分析——这四层组合起来,让"发现到定位"的平均时间从40分钟降到了8分钟。

赞(0)
未经允许不得转载:171主机测评 » 从发现到定位的最后一公里:机器学习时序异常检测与根因分析结合实践
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址