
凌晨 2 点,告警响了——OOM 杀死。你从床上爬起来,SSH 登录、看日志、重启 Pod、等恢复。整套动作重复了三个月,一模一样的问题,一模一样的操作。能不能让机器自己来?
我负责的订单中心集群有 30+ 微服务,每次故障处理流程是这样的:
步骤 | 操作 | 耗时 |
|---|---|---|
1. 告警通知 | 飞书/短信接收 | 2 分钟 |
2. 登录排查 | SSH 进服务器看日志 | 8 分钟 |
3. 定位根因 | 多日志源关联分析 | 15 分钟 |
4. 执行修复 | 重启/扩容/回滚 | 10 分钟 |
5. 验证恢复 | 确认指标恢复正常 | 10 分钟 |
总计 | — | 45 分钟 |
最抓狂的是什么?80% 的故障都是重复模式——OOM 重启、连接池耗尽、磁盘满——修复动作完全一样,却每次都要人工走一遍。
运维最宝贵的资源是人的注意力,而不是人的手。把"手"的工作交给机器,让人的注意力聚焦在真正的异常上——这就是故障自愈的核心理念。


图:自愈链路可视化,从 Prometheus 告警到 K8s 执行、验证恢复的端到端闭环,本事件总耗时 3m 24s
核心思路很简单:把运维经验和决策逻辑变成可编程的 Runbook,让 AI 分类器决定走自动还是走人工。

维度 | 人工修复 | AI 自愈 |
|---|---|---|
响应速度 | 2-5 分钟(人看到通知) | < 10 秒(自动触发) |
修复执行 | 手动输入命令 | Runbook 自动化 |
风险控制 | 人工判断 | 分级审批 + 回滚保护 |
一致性 | 因人而异 | 标准化流程 |
可审计 | 口头汇报 | 完整操作日志 |
为什么要画 Runbook 执行流程图?Runbook 的核心价值在于"步骤编排 + 条件分支 + 自动回滚",纯文字和代码很难直观体现分支决策逻辑。流程图能让读者一眼看清"匹配 → 采集 → 诊断 → 修复 → 回滚"的完整链路,也方便团队评审 Runbook 设计是否合理。

为什么用 YAML 而不是硬编码?运维修复策略需要频繁调整,YAML 配置让非开发人员也能维护 Runbook。
# runbook-oom-restart.yaml
apiVersion: selfhealing/v1
kind: Runbook
metadata:
name: oom-auto-restart
labels:
alertType: OOMKilled
severity: P2
riskLevel: low
spec:
trigger:
alertName: OOMKilled
matchLabels:
app: "{{ .Labels.app }}"
steps:
- name: 检查最近重启次数
action: check-restart-count
params:
namespace: "{{ .Labels.namespace }}"
pod: "{{ .Labels.pod }}"
windowMinutes: 10
maxRestarts: 3
- name: 记录当前内存配置
action: get-resource-limits
params:
namespace: "{{ .Labels.namespace }}"
deployment: "{{ .Labels.deployment }}"
- name: 提升内存限制
action: patch-deployment
params:
memoryLimit: "2Gi" # 原 1Gi → 2Gi
memoryRequest: "1Gi"
rollback:
memoryLimit: "1Gi"
memoryRequest: "512Mi"
- name: 验证服务恢复
action: health-check
params:
endpoint: "http://{{ .Labels.app }}:8080/actuator/health"
timeoutSeconds: 60
expectedStatus: 200
为什么用 Python 而不是 Shell 脚本?Python 可以更好地处理条件分支、异常捕获和回滚逻辑,Shell 在复杂流程控制上力不从心。
#!/usr/bin/env python3
"""
Runbook 执行引擎
支持步骤编排、条件分支、自动回滚
"""
import subprocess
import json
import logging
from datetime import datetime
logger = logging.getLogger("runbook-engine")
class RunbookExecutor:
def __init__(self, runbook: dict, context: dict):
self.runbook = runbook
self.context = context
self.executed_steps = [] # 记录已执行步骤,用于回滚
self.start_time = datetime.now()
def execute(self) -> dict:
"""执行 Runbook 全部步骤"""
steps = self.runbook["spec"]["steps"]
for step in steps:
try:
result = self._execute_step(step)
self.executed_steps.append({
"step": step["name"],
"status": "success",
"result": result,
"rollback": step.get("rollback")
})
logger.info(f"步骤 [{step['name']}] 执行成功")
except Exception as e:
logger.error(f"步骤 [{step['name']}] 执行失败: {e}")
self._rollback()
return {"status": "failed", "error": str(e)}
return {
"status": "success",
"steps_completed": len(self.executed_steps),
"duration_sec": (datetime.now() - self.start_time).seconds
}
def _execute_step(self, step: dict) -> dict:
"""执行单个步骤"""
action = step["action"]
params = step.get("params", {})
if action == "check-restart-count":
return self._check_restart_count(params)
elif action == "patch-deployment":
return self._patch_deployment(params)
elif action == "health-check":
return self._health_check(params)
else:
raise ValueError(f"未知 action: {action}")
def _check_restart_count(self, params: dict) -> dict:
"""检查 Pod 最近重启次数"""
ns = params["namespace"]
pod = params["pod"]
window = params["windowMinutes"]
max_count = params["maxRestarts"]
cmd = f"kubectl get pod {pod} -n {ns} -o json"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
pod_info = json.loads(result.stdout)
restarts = pod_info["status"]["containerStatuses"][0]["restartCount"]
if restarts >= max_count:
raise Exception(
f"重启次数 {restarts} 超过阈值 {max_count},停止自愈,转人工"
)
return {"restartCount": restarts, "allowAutoFix": True}
def _patch_deployment(self, params: dict) -> dict:
"""修改 Deployment 资源配置"""
# 实际生产中会调用 K8s API
logger.info(f"修改 Deployment: {params}")
return {"patched": True}
def _health_check(self, params: dict) -> dict:
"""服务健康检查"""
import requests
try:
resp = requests.get(
params["endpoint"],
timeout=params.get("timeoutSeconds", 30)
)
return {"status": resp.status_code, "healthy": resp.status_code == 200}
except requests.RequestException as e:
raise Exception(f"健康检查失败: {e}")
def _rollback(self):
"""回滚已执行的步骤"""
logger.warning("开始回滚...")
for step_info in reversed(self.executed_steps):
rollback = step_info.get("rollback")
if rollback:
logger.info(f"回滚步骤 [{step_info['step']}]: {rollback}")
# 执行回滚逻辑
logger.info("回滚完成")
安全红线必须守住:
风险等级 | 修复类型 | 审批方式 | 示例 |
|---|---|---|---|
低风险 | 单 Pod 重启 | 全自动 | OOM 重启、健康检查失败 |
中风险 | 资源调整/扩容 | 飞书审批 | 内存限制提升、HPA 扩容 |
高风险 | 配置变更/回滚 | 人工确认 | 环境变量修改、版本回滚 |
禁止自愈 | 数据操作/删除 | 仅告警 | 数据库 DDL、数据清理 |
为什么每次自愈都要准备回滚方案?自动修复可能引入新问题,回滚是确保安全的兜底机制。
"""回滚保护装饰器"""
from functools import wraps
def with_rollback(rollback_func):
"""为自愈步骤添加回滚保护"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
# 记录回滚上下文
rollback_ctx = {
"step": func.__name__,
"timestamp": datetime.now().isoformat(),
"params": kwargs
}
# 保存回滚信息到 Redis,TTL 1 小时
redis.setex(
f"rollback:{func.__name__}",
3600,
json.dumps(rollback_ctx)
)
return result
except Exception as e:
logger.error(f"步骤 {func.__name__} 失败,触发回滚: {e}")
rollback_func(**kwargs)
raise
return wrapper
return decorator
故障类型 | 人工 MTTR | 自愈 MTTR | 提升幅度 |
|---|---|---|---|
OOM 重启 | 20 分钟 | 2 分钟 | ⬇️ 90% |
连接池耗尽 | 35 分钟 | 4 分钟 | ⬇️ 89% |
磁盘满 | 25 分钟 | 3 分钟 | ⬇️ 88% |
配置错误 | 60 分钟 | 8 分钟 | ⬇️ 87% |
加权平均 | 45 分钟 | 5 分钟 | ⬇️ 89% |
现象:数据库主库宕机,自愈系统自动重启主库,但主库启动后数据不一致,导致更大范围故障。
原因:自愈逻辑没有考虑数据库主从关系的特殊性,盲目重启破坏了数据一致性。
解决:为数据库组件设置"禁止自愈"标签,仅触发告警通知 DBA 人工处理。
提醒:有状态服务(数据库、消息队列)的自愈要极度谨慎,推荐仅告警不修复。
现象:Runbook 的 {{ .Labels.app }} 在部分告警中为空,导致 K8s API 调用报错。
原因:某些告警的 Label 不包含 app 字段,模板渲染后变成了空字符串。
解决:为所有模板变量设置默认值,并在执行前校验必填参数:
# 执行前校验模板参数
REQUIRED_PARAMS = ["app", "namespace", "deployment"]
def validate_context(context: dict) -> bool:
missing = [p for p in REQUIRED_PARAMS if not context.get(p)]
if missing:
raise ValueError(f"缺少必填参数: {missing}")
return True
提醒:模板参数校验是自愈系统的基本防线,缺失参数宁可跳过也不能乱执行。
现象:自愈提升内存限制后,Pod 重启触发 Ready 告警,又触发新一轮自愈,反复循环。
原因:自愈系统没有"冷却期"机制,修复动作产生的预期告警被当作新故障处理。
解决:自愈执行后设置 5 分钟静默窗口,期间同类告警不触发自愈:
def should_auto_heal(alert: dict) -> bool:
"""判断是否应触发自愈"""
key = f"silence:{alert['name']}:{alert['labels']['app']}"
if redis.exists(key):
logger.info(f"告警 {alert['name']} 在静默期,跳过自愈")
return False
# 设置 5 分钟静默
redis.setex(key, 300, "healing_in_progress")
return True
提醒:自愈必须有冷却机制,否则就是"自动故障制造机"。
现象:自愈失败触发回滚,但回滚方案使用的是旧配置,导致服务用了过期参数。
原因:回滚上下文是从自愈前快照的,但自愈过程中有其他人工变更,回滚覆盖了人工修改。
解决:回滚前先 diff 当前配置与回滚配置,有冲突时转人工决策而非强制回滚。
提醒:回滚不是"时间倒流",要考虑并发变更的情况。

图:自愈事件日志,24h 内 28 次自愈,成功率 92.9%,平均耗时 3m 24s,节省人工工时约 14h
自愈系统自身也需要监控——谁来看护看护者?
监控项 | 采集方式 | 更新间隔 | 告警阈值 | 严重级别 |
|---|---|---|---|---|
自愈执行成功率 | Runbook 日志统计 | 60s | < 90% | P1 |
Runbook 执行超时 | 日志监控 | 60s | > 300s | P2 |
自愈回滚触发率 | 日志统计 | 3600s | > 5% | P1 |
人机审批超时未处理 | 审批系统状态 | 60s | > 300s | P0 |
自愈风暴检测 | 频次统计 | 60s | > 5次/10min 自动熔断 | P0 |
# Crontab 配置:自愈 Runbook 定时健康检查
0 * * * * /opt/scripts/self_healing_health.sh # 每小时检查自愈服务状态
0 3 * * * /opt/scripts/runbook_backup.sh # 每天凌晨备份 Runbook
*/5 * * * * /opt/scripts/check_healing_storm.sh # 每5分钟检查自愈风暴
故障自愈的核心价值:
适合谁用:高频重复故障(OOM、连接池、磁盘满)、告警量大、值班人力紧张的运维团队
不适合谁用:有状态服务(数据库主从)、数据变更操作、业务逻辑类故障——这些场景,人比机器更靠得住
💬 你的团队有故障自愈实践吗?MTTR 大概多少?评论区聊聊~