
AI 运维自动化把排障 Runbook 变成可执行的诊断链路别再手工跑命令了一、每次故障排查运维都要按照文档手动执行 15 个检查步骤线上推理服务突然报 503运维打开 Runbook 文档——整整 15 个步骤kubectl describe pod→kubectl logs→kubectl top node→nvidia-smi→curl health endpoint→check redis→check kafka lag... 每一步都是手工敲命令每一步都要等结果出来再决定下一步。15 分钟过去了问题定位了——GPU 显存满了。而这 15 分钟里的 14 分钟都是在执行命令和等待命令返回真正的分析数据只需要 1 分钟。基础设施不需要漂亮话Runbook 是运维经验的沉淀但手工执行 Runbook 是人力资源的浪费。每次故障都在重复同样的检查步骤同样的命令在不同人的终端上敲了一遍又一遍。问题不在 Runbook 本身——它记录的知识是有价值的——问题在于Runbook 只是文档不是程序。运维自动化的正确方向不是用 AI 替代人做决策而是把 Runbook 编译成可执行的诊断链路——用代码编排检查步骤、自动收集诊断数据、按决策树自动路由到下一环节。人工介入只发生在两个节点诊断链路无法确定根因时需要专家判断以及执行修复动作前需要人工确认。二、可执行诊断链路的设计模型诊断链路由一系列可组合的检查步骤组成每个步骤有明确的输入、输出和决策分支flowchart TD START([告警触发:br/推理服务 P99 2s]) -- S1 S1[Step 1: 服务健康检查br/HTTP GET /health /metrics] -- D1{服务状态?} D1 --|200 OK| S2 D1 --|5xx| S1A[分支: 服务错误排查br/检查 Pod 状态 日志] D1 --|超时| S1B[分支: 网络排查br/检查 Service/Ingress] S2[Step 2: GPU 状态检查br/nvidia-smi dcgmi] -- D2{GPU 状态?} D2 --|正常| S3 D2 --|ECC Error| S2A[分支: GPU 硬件故障br/Cordon 节点 通知] D2 --|显存 90%| S2B[分支: 显存压力br/检查显存碎片率] S3[Step 3: 推理服务性能br/Prometheus Query] -- D3{延迟来源?} D3 --|模型推理慢| S3A[分支: 模型性能br/检查 batch size / KV Cache] D3 --|预处理慢| S3B[分支: 数据管道br/检查 Tokenizer / 数据格式] D3 --|网络 IO 慢| S3C[分支: 网络排查br/检查 Service Mesh / DNS] S1A -- END1([输出报告:br/错误类型 建议动作]) S1B -- END2([输出报告:br/网络拓扑 丢包率]) S2A -- END3([输出报告:br/GPU 序列号 建议更换]) S2B -- END4([输出报告:br/显存分布 建议重启]) S3A -- END5([输出报告:br/模型推理 Profile]) S3B -- END6([输出报告:br/数据管道瓶颈]) S3C -- END7([输出报告:br/网络延迟分布])诊断链路的设计原则每个步骤是幂等的独立函数Step 1 失败不应该阻止 Step 2 执行。即使 HTTP health check 超时了GPU 状态检查仍然应该运行——因为可能两个问题同时存在健康检查只是提前暴露了第一个。决策树而非线性列表传统 Runbook 是线性列表Step 1 → Step 2 → ... → Step N可执行诊断链路是决策树。每个节点根据检查结果路由到不同分支避免在无关路径上浪费检查时间。输出诊断报告而非命令结果一个步骤的输出不是kubectl top pod 的输出原文而是结构化的诊断结论Pod ai-inference-7d4f-abc123 的 CPU 使用率 230%内存使用率 95%疑似内存泄漏。从原始数据到诊断结论的翻译由每个检查步骤完成人工只看结论。三、生产级诊断链路引擎以下 Go 代码实现了一个可扩展的诊断链路引擎支持步骤注册、并行执行、决策树路由和结构化报告生成package diagnosis import ( context encoding/json fmt sync time github.com/pkg/errors ) // StepResult 单个诊断步骤的执行结果 type StepResult struct { StepName string json:step_name Status string json:status // pass, fail, error, timeout Conclusion string json:conclusion RawData map[string]interface{} json:raw_data,omitempty Duration time.Duration json:duration_ms NextStep string json:next_step,omitempty // 指定下一步骤 Suggestions []string json:suggestions,omitempty } // DiagnosticStep 诊断步骤接口 // 每个步骤是幂等的、无状态的独立函数 type DiagnosticStep interface { // Name 返回步骤名称 Name() string // Execute 执行诊断逻辑 // 返回结论和原始数据nextStep 为空则按默认顺序执行 Execute(ctx context.Context) (*StepResult, error) } // DiagnosticReport 完整的诊断报告 type DiagnosticReport struct { TriggerAlert string json:trigger_alert StartTime time.Time json:start_time EndTime time.Time json:end_time TotalDuration time.Duration json:total_duration_ms Steps []StepResult json:steps RootCause string json:root_cause,omitempty ActionPlan []string json:action_plan Confidence string json:confidence // high, medium, low } // DiagnosticEngine 诊断链路引擎 type DiagnosticEngine struct { mu sync.RWMutex steps []DiagnosticStep // 按注册顺序排列的诊断步骤 // 超时控制 stepTimeout time.Duration // 单步执行超时 totalTimeout time.Duration // 全链路超时 // 事件回调 onStepComplete func(step StepResult) } // NewDiagnosticEngine 创建诊断引擎 func NewDiagnosticEngine(stepTimeout, totalTimeout time.Duration) *DiagnosticEngine { return DiagnosticEngine{ stepTimeout: stepTimeout, totalTimeout: totalTimeout, } } // Register 注册诊断步骤 func (e *DiagnosticEngine) Register(step DiagnosticStep) { e.mu.Lock() defer e.mu.Unlock() e.steps append(e.steps, step) } // SetOnStepComplete 设置每步完成的回调用于实时通知 func (e *DiagnosticEngine) SetOnStepComplete(fn func(StepResult)) { e.mu.Lock() defer e.mu.Unlock() e.onStepComplete fn } // Execute 执行诊断链路 // triggerAlert: 触发本次诊断的告警描述 func (e *DiagnosticEngine) Execute(ctx context.Context, triggerAlert string) (*DiagnosticReport, error) { // 设置全链路超时 ctx, cancel : context.WithTimeout(ctx, e.totalTimeout) defer cancel() report : DiagnosticReport{ TriggerAlert: triggerAlert, StartTime: time.Now(), Steps: make([]StepResult, 0), } e.mu.RLock() steps : make([]DiagnosticStep, len(e.steps)) copy(steps, e.steps) e.mu.RUnlock() // 步骤索引映射用于决策树跳转 stepIndex : make(map[string]int) for i, s : range steps { stepIndex[s.Name()] i } i : 0 for i len(steps) { select { case -ctx.Done(): // 全链路超时记录已完成的步骤和当前中断点 report.EndTime time.Now() report.TotalDuration report.EndTime.Sub(report.StartTime) report.RootCause 诊断超时未能完成全链路检查 report.Confidence low return report, ctx.Err() default: } step : steps[i] stepCtx, stepCancel : context.WithTimeout(ctx, e.stepTimeout) start : time.Now() result, err : step.Execute(stepCtx) stepCancel() if err ! nil { // 步骤执行失败创建错误结果而非中断全链路 result StepResult{ StepName: step.Name(), Status: error, Conclusion: fmt.Sprintf(步骤执行失败: %v, err), Duration: time.Since(start), } } if result nil { result StepResult{ StepName: step.Name(), Status: pass, Duration: time.Since(start), } } result.Duration time.Since(start) report.Steps append(report.Steps, *result) // 触发回调 e.mu.RLock() cb : e.onStepComplete e.mu.RUnlock() if cb ! nil { cb(*result) } // 决策树路由 if result.NextStep ! { nextIdx, ok : stepIndex[result.NextStep] if ok { i nextIdx continue } } i } report.EndTime time.Now() report.TotalDuration report.EndTime.Sub(report.StartTime) // 汇总诊断结论 report.RootCause, report.Confidence, report.ActionPlan e.summarize(report.Steps) return report, nil } // summarize 对所有步骤结果做汇总分析 func (e *DiagnosticEngine) summarize(steps []StepResult) (rootCause string, confidence string, actionPlan []string) { var issues []StepResult for _, s : range steps { if s.Status fail { issues append(issues, s) } } if len(issues) 0 { return 未发现明确根因建议人工深度排查, low, []string{检查最近变更, 查看分布式 Trace, 对比历史基线} } if len(issues) 1 { return issues[0].Conclusion, high, issues[0].Suggestions } // 多步骤同时异常可能是级联故障 rootCause 检测到多处异常疑似级联故障 confidence medium for _, issue : range issues { actionPlan append(actionPlan, issue.Suggestions...) } return }推理服务诊断步骤实现示例package diagnosis import ( context encoding/json fmt net/http os/exec time ) // GPUCheckStep GPU 状态检查步骤 type GPUCheckStep struct { nodeName string } func (s *GPUCheckStep) Name() string { return gpu-health-check } func (s *GPUCheckStep) Execute(ctx context.Context) (*StepResult, error) { // 执行 nvidia-smi 查询 cmd : exec.CommandContext(ctx, nvidia-smi, --query-gpuutilization.gpu,memory.used,memory.total,temperature.gpu,ecc.errors.corrected.volatile.total,ecc.errors.uncorrected.volatile.total, --formatcsv,noheader,nounits) output, err : cmd.CombinedOutput() if err ! nil { return StepResult{ StepName: s.Name(), Status: error, Conclusion: 无法获取 GPU 状态nvidia-smi 命令执行失败, }, err } var gpuUtil, memUsed, memTotal, temp float64 var eccCorr, eccUncorr int _, err fmt.Sscanf(string(output), %f,%f,%f,%f,%d,%d, gpuUtil, memUsed, memTotal, temp, eccCorr, eccUncorr) if err ! nil { return StepResult{ StepName: s.Name(), Status: error, Conclusion: GPU 状态数据解析失败, }, err } memPercent : (memUsed / memTotal) * 100 result : StepResult{ StepName: s.Name(), Status: pass, RawData: map[string]interface{}{ gpu_utilization: gpuUtil, memory_used_pct: memPercent, temperature: temp, ecc_corrected: eccCorr, ecc_uncorrected: eccUncorr, }, } // 异常检测与路由 if eccUncorr 0 { result.Status fail result.Conclusion fmt.Sprintf(GPU 出现不可纠正 ECC 错误 %d 次硬件故障风险, eccUncorr) result.Suggestions []string{立即 Cordon 节点, 联系硬件团队更换 GPU, 迁移该节点上的推理服务} result.NextStep gpu-hardware-failure return result, nil } if memPercent 95 { result.Status fail result.Conclusion fmt.Sprintf(GPU 显存使用率 %.1f%%疑似显存泄漏或碎片化, memPercent) result.Suggestions []string{重启推理服务释放显存, 检查显存碎片率, 如果频繁发生, 考虑排查模型代码中的显存泄漏} result.NextStep memory-fragmentation-check return result, nil } if temp 85 { result.Status fail result.Conclusion fmt.Sprintf(GPU 温度 %.1f°C超过正常工作范围, temp) result.Suggestions []string{检查机房冷却系统, 降低推理 Batch Size 减少 GPU 负载} return result, nil } if memPercent 80 { result.Status pass result.Conclusion fmt.Sprintf(GPU 显存使用率 %.1f%%处于较高水平但暂未触发阈值, memPercent) result.Suggestions []string{持续监控显存使用趋势} } return result, nil } // ServiceHealthCheckStep 推理服务健康检查步骤 type ServiceHealthCheckStep struct { serviceURL string httpClient *http.Client } func (s *ServiceHealthCheckStep) Name() string { return service-health-check } func (s *ServiceHealthCheckStep) Execute(ctx context.Context) (*StepResult, error) { req, err : http.NewRequestWithContext(ctx, http.MethodGet, s.serviceURL/health, nil) if err ! nil { return nil, err } resp, err : s.httpClient.Do(req) if err ! nil { return StepResult{ StepName: s.Name(), Status: fail, Conclusion: fmt.Sprintf(推理服务健康检查失败: %v, err), Suggestions: []string{检查 Pod 是否 Running, 检查 Service/Endpoint 是否正常, 检查网络策略是否阻断}, NextStep: pod-status-check, }, nil } defer resp.Body.Close() if resp.StatusCode ! http.StatusOK { return StepResult{ StepName: s.Name(), Status: fail, Conclusion: fmt.Sprintf(推理服务返回 HTTP %d疑似服务内部错误, resp.StatusCode), RawData: map[string]interface{}{ http_status: resp.StatusCode, }, NextStep: pod-log-check, }, nil } return StepResult{ StepName: s.Name(), Status: pass, Conclusion: 推理服务健康检查通过, }, nil }引擎调用入口func runDiagnosis(triggerAlert string) { engine : NewDiagnosticEngine(30*time.Second, 5*time.Minute) // 设置实时通知回调 engine.SetOnStepComplete(func(result StepResult) { if result.Status fail { sendAlert(fmt.Sprintf([诊断中] %s: %s, result.StepName, result.Conclusion)) } }) // 注册诊断步骤按执行顺序 engine.Register(ServiceHealthCheckStep{ serviceURL: http://ai-inference.default.svc.cluster.local:8080, httpClient: http.Client{Timeout: 5 * time.Second}, }) engine.Register(GPUCheckStep{nodeName: gpu-node-03}) ctx : context.Background() report, err : engine.Execute(ctx, triggerAlert) if err ! nil { sendAlert(fmt.Sprintf([诊断异常] %v, err)) return } // 输出结构化报告 reportJSON, _ : json.MarshalIndent(report, , ) sendReport(string(reportJSON)) }四、诊断链路自动化的人机边界全自动化 vs 半自动化不需要把 Runbook 100% 自动化。某些需要主观判断的步骤如检查日志中的错误信息是否与已知 Bug 匹配交给人工做更可靠。诊断链路的合理分工数据采集 100% 自动化kubectl describe、nvidia-smi、PromQL数据解读 80% 自动化阈值判定、模式匹配修复决策 0% 自动化重启、回滚、迁移全部需要人工确认。诊断链路不是静态的Runbook 会随着系统演进持续更新。如果诊断链路是硬编码在代码里的每次 Runbook 更新都需要代码变更运维迭代成本太高。建议用 YAML/JSON DSL 描述诊断链路诊断引擎只负责解释执行。这样新增一个检查步骤只需要修改配置文件不需要重新编译部署。错误诊断的时效性全套诊断链路可能需要 3-5 分钟执行完成。但对于 P0 级别的故障如服务完全不可用运维需要的是秒级的干预能力不是分钟级的诊断报告。对此的设计分层诊断——L130 秒内完成只检查致命问题服务存活、GPU 掉卡、OOM→ L25 分钟内完成全量诊断链路。L1 发现问题直接触发应急动作切流、重启L2 给详细根因报告。历史诊断数据的复用每次诊断的执行结果应该持久化存储时序数据库或 Elasticsearch。当同一告警再次触发时优先查询历史诊断记录——如果上次 root_cause 是GPU 显存碎片率 85%且当前 GPU 指标和上次高度相似直接复用上次的 action_plan跳过全链路检查。这是诊断缓存——用历史经验加速本次排查。五、总结把 Runbook 从文档变成可执行链路的核心要点诊断步骤 幂等函数每个步骤独立执行互不阻塞。步骤失败不中断全链路继续收集其他维度的数据。决策树替代线性列表根据每一步的检查结果自动路由到相关分支避免在无关路径上浪费时间。输出结论不输出数据每个步骤把原始命令输出翻译成诊断结论 建议动作人工只需要看总结。人机边界清晰数据采集和模式匹配自动化修复决策必须人工确认。分层诊断——L1 秒级响应L2 全量诊断。历史诊断复用持久化诊断记录同类型故障优先查历史加速排查。Runbook 是知识的积累但手工执行是时间的浪费。把它编译成程序让每次故障诊断从手工跑 15 条命令变成等 3 分钟看一份报告。