第2457篇:AIOps工程实践——让运维系统具备自主分析和响应能力
第2457篇:AIOps工程实践——让运维系统具备自主分析和响应能力
适读人群:运维工程师、后端工程师、SRE | 阅读时长:约18分钟 | 核心价值:从零搭建一套能自主发现问题、分析根因、触发响应的AIOps系统
去年双十一前夕,我们的核心交易链路出了一个诡异的问题:订单创建成功率从99.8%降到了97.2%,但没有任何服务报出明显异常。
监控大盘上全是绿的。告警没有触发。日志里每秒几十万条,看起来都是正常的错误率。
最后是一个老运维同事用肉眼盯着Grafana,发现了一个不起眼的指标在缓慢上升——数据库连接池等待队列。但等他发现的时候,已经过去了23分钟,损失了大量订单。
事后复盘,我一直在想一件事:这个问题,规则告警为什么没发现?因为每一个单独的指标都没有超过阈值,问题藏在指标之间的关联关系里。
这就是传统运维和AIOps的本质差距。
为什么规则告警会失效
传统监控告警的逻辑是:定义一个阈值,某个指标超过阈值就告警。这个模型有几个根本性的缺陷:
缺陷一:需要人提前知道所有可能的故障模式。你需要为每一种可能的问题预先定义规则,但生产环境里的故障模式是无穷的,而且会不断演化。
缺陷二:无法处理多指标关联的问题。单指标没有异常,但三个指标的组合已经是一种危险信号,规则系统无法表达这种逻辑。
缺陷三:告警疲劳。为了不遗漏问题,阈值往往设得很低,导致告警风暴,值班工程师要处理几百条告警,真正的问题被淹没。
AIOps想解决的核心问题就是这三个。
AIOps系统的整体架构
在动手写代码之前,我想先把架构讲清楚。AIOps系统不是一个单一的服务,而是一个数据流水线加上智能分析层的组合。
这个架构有几个关键设计决策值得解释:
- 数据预处理是独立层:原始指标噪声太大,必须先做归一化和去噪,否则异常检测会产生大量误报
- LLM只在根因分析层:LLM的推理能力用在对异常事件的语义理解上,而不是用来做时序异常检测(这个用统计模型更准)
- 响应决策需要置信度阈值:不是所有LLM给出的响应建议都直接执行,高风险操作必须人工确认
核心组件实现
1. 异常检测引擎
先实现基于3-sigma的统计异常检测,这是最基础但在工程上很实用的方法:
@Component
public class StatisticalAnomalyDetector {
private static final int BASELINE_WINDOW = 60; // 60分钟基线窗口
private static final double SIGMA_THRESHOLD = 3.0;
private final MetricsRepository metricsRepository;
private final Cache<String, BaselineStats> baselineCache;
public StatisticalAnomalyDetector(MetricsRepository metricsRepository) {
this.metricsRepository = metricsRepository;
this.baselineCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
}
public AnomalyResult detect(MetricPoint current) {
BaselineStats baseline = getOrComputeBaseline(current.getMetricKey());
if (baseline.getSampleCount() < 30) {
// 样本不足,无法建立基线
return AnomalyResult.insufficient();
}
double zScore = Math.abs(current.getValue() - baseline.getMean()) / baseline.getStdDev();
if (zScore > SIGMA_THRESHOLD) {
double anomalyScore = Math.min(1.0, (zScore - SIGMA_THRESHOLD) / SIGMA_THRESHOLD);
return AnomalyResult.anomaly(
current.getMetricKey(),
current.getValue(),
baseline.getMean(),
zScore,
anomalyScore
);
}
return AnomalyResult.normal();
}
private BaselineStats getOrComputeBaseline(String metricKey) {
return baselineCache.get(metricKey, key -> {
List<Double> values = metricsRepository.getRecentValues(
key,
Instant.now().minus(BASELINE_WINDOW, ChronoUnit.MINUTES),
Instant.now().minus(5, ChronoUnit.MINUTES) // 排除最近5分钟避免污染基线
);
return computeStats(values);
});
}
private BaselineStats computeStats(List<Double> values) {
if (values.isEmpty()) {
return BaselineStats.empty();
}
double mean = values.stream().mapToDouble(Double::doubleValue).average().orElse(0);
double variance = values.stream()
.mapToDouble(v -> Math.pow(v - mean, 2))
.average()
.orElse(0);
double stdDev = Math.sqrt(variance);
// 防止stdDev为0导致除零
stdDev = Math.max(stdDev, mean * 0.01);
return BaselineStats.of(mean, stdDev, values.size());
}
}单指标异常检测只是第一步。更重要的是多指标关联分析,这才是发现复杂故障的关键:
@Component
public class CorrelationAnomalyDetector {
private final StatisticalAnomalyDetector baseDetector;
private final ServiceTopologyRepository topologyRepository;
/**
* 分析一个服务的所有相关指标,找出异常指标集群
*
* 核心思路:在同一时间窗口内,如果一个服务的多个相关指标同时出现异常,
* 说明不是随机噪声,而是真实的系统异常。
*/
public CorrelationAnomalyReport analyzeService(String serviceId, Instant timestamp) {
List<MetricGroup> relatedMetrics = topologyRepository.getServiceMetrics(serviceId);
Map<MetricGroup, List<AnomalyResult>> anomalyGroups = new HashMap<>();
for (MetricGroup group : relatedMetrics) {
List<MetricPoint> currentPoints = metricsRepository.getMetricsAtTime(
group.getMetricKeys(), timestamp
);
List<AnomalyResult> groupAnomalies = currentPoints.stream()
.map(baseDetector::detect)
.filter(AnomalyResult::isAnomaly)
.collect(toList());
if (!groupAnomalies.isEmpty()) {
anomalyGroups.put(group, groupAnomalies);
}
}
// 计算关联异常得分:同一资源组的多个异常指标给更高权重
double correlationScore = computeCorrelationScore(anomalyGroups);
return CorrelationAnomalyReport.builder()
.serviceId(serviceId)
.timestamp(timestamp)
.anomalyGroups(anomalyGroups)
.correlationScore(correlationScore)
.severity(classifySeverity(correlationScore))
.build();
}
private double computeCorrelationScore(Map<MetricGroup, List<AnomalyResult>> anomalyGroups) {
if (anomalyGroups.isEmpty()) return 0.0;
double score = 0.0;
for (Map.Entry<MetricGroup, List<AnomalyResult>> entry : anomalyGroups.entrySet()) {
MetricGroup group = entry.getKey();
List<AnomalyResult> anomalies = entry.getValue();
// 关键指标组权重更高
double groupWeight = group.isCritical() ? 2.0 : 1.0;
// 组内多个异常有协同效应
double groupScore = anomalies.stream()
.mapToDouble(AnomalyResult::getAnomalyScore)
.sum();
// 组内超过3个异常,关联性很强,额外加分
if (anomalies.size() >= 3) {
groupScore *= 1.5;
}
score += groupScore * groupWeight;
}
return Math.min(1.0, score / 10.0); // 归一化到0-1
}
}2. LLM根因分析引擎
这是AIOps系统里技术含量最高的部分。我们不是直接把原始指标扔给LLM,而是先做结构化的上下文组装:
@Service
public class LLMRootCauseAnalyzer {
private final OpenAiChatClient chatClient;
private final ServiceTopologyRepository topologyRepository;
private final IncidentHistoryRepository incidentHistory;
public RootCauseAnalysisResult analyze(CorrelationAnomalyReport report) {
// 组装分析上下文
String context = buildAnalysisContext(report);
ChatMessage systemMessage = new SystemMessage("""
你是一个资深的生产系统SRE工程师,专门分析系统异常和故障根因。
分析时请遵循以下原则:
1. 优先考虑最近的变更(发布、配置修改、流量变化)
2. 从异常指标的关联关系推断故障传播路径
3. 给出具体的根因假设,而不是模糊的描述
4. 推荐的处置步骤必须是可操作的
输出必须是JSON格式,包含以下字段:
- rootCauseHypotheses: 根因假设列表(最多3个,按置信度排序)
- propagationPath: 故障传播路径描述
- immediateActions: 立即处置步骤(可自动执行的)
- investigationSteps: 需要人工排查的步骤
- confidence: 整体分析置信度(0-1)
""");
ChatMessage userMessage = new UserMessage(context);
ChatResponse response = chatClient.call(
new Prompt(List.of(systemMessage, userMessage),
OpenAiChatOptions.builder()
.withModel("gpt-4o")
.withTemperature(0.1f) // 低温度,要求确定性输出
.withResponseFormat(new ResponseFormat(ResponseFormat.Type.JSON_OBJECT))
.build())
);
String jsonResult = response.getResult().getOutput().getContent();
return parseAndValidate(jsonResult, report);
}
private String buildAnalysisContext(CorrelationAnomalyReport report) {
StringBuilder sb = new StringBuilder();
// 服务拓扑信息
ServiceTopology topology = topologyRepository.getTopology(report.getServiceId());
sb.append("## 服务信息\n");
sb.append("服务: ").append(report.getServiceId()).append("\n");
sb.append("上游依赖: ").append(topology.getUpstreamServices()).append("\n");
sb.append("下游依赖: ").append(topology.getDownstreamServices()).append("\n\n");
// 异常指标详情
sb.append("## 异常指标 (发生时间: ").append(report.getTimestamp()).append(")\n");
for (Map.Entry<MetricGroup, List<AnomalyResult>> entry : report.getAnomalyGroups().entrySet()) {
sb.append("### ").append(entry.getKey().getName()).append("\n");
for (AnomalyResult anomaly : entry.getValue()) {
sb.append(String.format("- %s: 当前值=%.2f, 基线均值=%.2f, Z-score=%.1f\n",
anomaly.getMetricKey(),
anomaly.getCurrentValue(),
anomaly.getBaselineMean(),
anomaly.getZScore()
));
}
}
// 最近的变更记录(关键!)
sb.append("\n## 最近24小时变更记录\n");
List<ChangeRecord> recentChanges = changeRepository.getRecentChanges(
report.getServiceId(), Duration.ofHours(24)
);
if (recentChanges.isEmpty()) {
sb.append("无变更记录\n");
} else {
for (ChangeRecord change : recentChanges) {
sb.append(String.format("- [%s] %s: %s\n",
change.getTimestamp(),
change.getChangeType(),
change.getDescription()
));
}
}
// 历史相似事件
sb.append("\n## 历史相似事件\n");
List<IncidentRecord> similarIncidents = incidentHistory.findSimilar(
report.getServiceId(),
report.getAnomalyGroups().keySet(),
5
);
for (IncidentRecord incident : similarIncidents) {
sb.append(String.format("- [%s] 根因: %s, 解决方案: %s\n",
incident.getTimestamp(),
incident.getRootCause(),
incident.getResolution()
));
}
return sb.toString();
}
}3. 自动响应执行引擎
LLM分析出根因之后,低风险的处置可以自动执行,高风险的需要人工确认:
@Service
public class AutoRemediationEngine {
private static final double AUTO_EXECUTE_THRESHOLD = 0.85; // 置信度阈值
private final Map<String, RemediationAction> actionRegistry;
private final NotificationService notificationService;
private final AuditLogger auditLogger;
public RemediationResult execute(RootCauseAnalysisResult rcaResult) {
List<ActionPlan> actions = buildActionPlans(rcaResult);
List<ActionResult> results = new ArrayList<>();
for (ActionPlan plan : actions) {
ActionResult result = executeWithPolicy(plan, rcaResult.getConfidence());
results.add(result);
// 记录所有动作,无论是自动执行还是等待人工
auditLogger.log(AuditEvent.remediation(plan, result, rcaResult));
}
return RemediationResult.of(results);
}
private ActionResult executeWithPolicy(ActionPlan plan, double confidence) {
boolean isLowRisk = plan.getRiskLevel() == RiskLevel.LOW;
boolean isHighConfidence = confidence >= AUTO_EXECUTE_THRESHOLD;
boolean canAutoExecute = isLowRisk && isHighConfidence;
if (canAutoExecute) {
return executeAction(plan);
} else {
// 发送人工审批请求
String approvalId = notificationService.requestApproval(
ApprovalRequest.builder()
.action(plan)
.confidence(confidence)
.reason(plan.getReason())
.timeout(Duration.ofMinutes(5))
.build()
);
return ActionResult.pendingApproval(approvalId);
}
}
private ActionResult executeAction(ActionPlan plan) {
RemediationAction action = actionRegistry.get(plan.getActionType());
if (action == null) {
return ActionResult.failed("未知的动作类型: " + plan.getActionType());
}
try {
ActionContext context = ActionContext.of(plan.getParameters());
boolean success = action.execute(context);
if (success) {
return ActionResult.success(plan.getActionType(), context.getExecutionLog());
} else {
return ActionResult.failed("执行失败: " + context.getFailureReason());
}
} catch (Exception e) {
log.error("执行修复动作失败: {}", plan.getActionType(), e);
return ActionResult.failed("执行异常: " + e.getMessage());
}
}
}4. 内置的修复动作库
有几个最常用的修复动作值得单独实现:
// 重启Pod(Kubernetes环境)
@Component("restart_pod")
public class RestartPodAction implements RemediationAction {
private final KubernetesClient k8sClient;
@Override
public boolean execute(ActionContext context) {
String namespace = context.getParam("namespace");
String podName = context.getParam("pod_name");
try {
// 优雅删除,让K8s自动重建
k8sClient.pods()
.inNamespace(namespace)
.withName(podName)
.withGracePeriod(30)
.delete();
// 等待新Pod启动
boolean started = waitForPodReady(namespace, podName, Duration.ofMinutes(3));
context.log("Pod %s 重启%s".formatted(podName, started ? "成功" : "超时"));
return started;
} catch (Exception e) {
context.setFailureReason(e.getMessage());
return false;
}
}
@Override
public RiskLevel getRiskLevel() {
return RiskLevel.LOW; // Pod重启风险低,可自动执行
}
}
// 扩容服务实例
@Component("scale_up")
public class ScaleUpAction implements RemediationAction {
private final KubernetesClient k8sClient;
@Override
public boolean execute(ActionContext context) {
String namespace = context.getParam("namespace");
String deploymentName = context.getParam("deployment_name");
int targetReplicas = Integer.parseInt(context.getParam("target_replicas"));
// 安全检查:不允许超过最大副本数
int maxReplicas = Integer.parseInt(context.getParam("max_replicas", "20"));
targetReplicas = Math.min(targetReplicas, maxReplicas);
k8sClient.apps().deployments()
.inNamespace(namespace)
.withName(deploymentName)
.scale(targetReplicas);
context.log("扩容 %s 到 %d 副本".formatted(deploymentName, targetReplicas));
return true;
}
@Override
public RiskLevel getRiskLevel() {
return RiskLevel.MEDIUM; // 扩容需要人工确认,避免费用失控
}
}工程落地的几个关键问题
问题一:LLM的分析结果怎么验证质量?
不能盲信LLM的输出。我们的做法是建立一个双重校验机制:LLM给出根因假设后,系统会自动查询历史案例库,计算当前异常模式与历史案例的相似度。如果LLM的分析结论与历史高相似案例的根因一致,置信度提升;如果差异很大,则降低置信度并强制走人工审批。
问题二:训练数据从哪里来?
一开始没有,完全依赖运维同事的经验输入。每次有事故处理完,我们会把完整的异常指标+根因+处置步骤记录到知识库,这是LLM分析时的历史案例来源。运行半年之后,知识库已经有了几百个真实案例,LLM的分析准确率显著提升。
问题三:告警疲劳的问题怎么解决?
AIOps系统不直接发告警,而是发"分析报告"。报告里有置信度分级:高置信度(>0.8)的直接触发自动修复或钉钉一对一通知;中置信度(0.5-0.8)的放入工单队列,值班工程师在空闲时处理;低置信度的只记录不通知。这一套下来,我们的日常告警量从每天200+降到了15以内。
从0到1的落地路径
不要试图一次性搭出完整的AIOps系统。我们的落地路径是:
第一阶段(1个月):只做异常聚合。把相关告警聚合成事件,减少告警数量,让值班工程师先从告警风暴里解脱出来。
第二阶段(2个月):接入LLM分析。每次有事故,系统自动生成分析报告,但结论仅供参考,人工处置。这个阶段主要是在积累知识库和调教Prompt。
第三阶段(3个月+):逐步开放自动修复。从风险最低的动作开始(比如清理磁盘、重启无状态Pod),验证稳定性后再扩大范围。
