跳过正文
故障排查方法论:从现象到根因

故障排查方法论:从现象到根因

·622 字·3 分钟·
目录
SRE 实战手册 - 这篇文章属于一个选集。
§ : 本文

排查的本质:假设驱动的科学方法
#

每次看到一个故障,脑子里第一反应往往是"上次也是这个问题,肯定是 XXX"。这种直觉有时有用,但在复杂系统里经常把你带进死胡同,浪费大量时间。

真正有效的排查,本质上是一个科学实验的过程:

  1. 观察现象
  2. 提出假设
  3. 设计验证实验
  4. 根据结果更新假设
  5. 循环直到找到根因

听起来像废话,但很多人在第 2 步就失控了——只提出一个假设,然后花几小时证明它。这就是锚定效应的典型表现。


黄金三步
#

第一步:准确描述现象
#

“系统挂了"不是现象,是情绪。准确的现象描述应该包含:

  • 什么坏了:哪个服务、哪个接口、哪个功能
  • 怎么坏的:错误率上升?延迟飙升?数据不一致?完全不可用?
  • 影响范围:所有用户还是部分用户?所有接口还是特定接口?所有区域还是单个 AZ?
  • 量化指标:错误率从 0.1% 涨到 45%,P99 延迟从 200ms 涨到 8s
# 快速获取现象的基础命令
# 检查 Pod 状态
kubectl get pods -n production --sort-by='.status.startTime'

# 看最近的事件
kubectl get events -n production --sort-by='.lastTimestamp' | tail -30

# 看 HPA 状态(是否在疯狂扩容)
kubectl get hpa -n production

# 看 Node 资源压力
kubectl top nodes
kubectl describe nodes | grep -A 5 "Conditions:"

第二步:构建时间线
#

时间线是排查的脊梁。没有时间线,你只能靠猜;有了时间线,相关性就变得可见。

关键原则:多系统日志时间对齐

不同系统的时区配置可能不一致,日志时间格式也不同。先统一到 UTC,再对齐。

# 从 Kubernetes 事件提取时间线
kubectl get events -n production \
  --sort-by='.lastTimestamp' \
  -o json | jq -r '.items[] | "\(.lastTimestamp) \(.reason) \(.message)"'

# 从 Pod 日志提取关键时间点
kubectl logs deployment/api-server -n production \
  --since=2h \
  | grep -E "(ERROR|WARN|panic|timeout)" \
  | head -50

# 如果用 Loki,跨服务时间线查询示例
# {namespace="production"} |= "error" | json | line_format "{{.ts}} {{.service}} {{.msg}}"

一个好的时间线长这样:

14:23:15 UTC  监控告警触发:API 成功率 < 95%
14:23:08 UTC  [api-server] 开始出现 "connection refused" 到 db-service
14:22:55 UTC  [db-service] Pod db-service-7d9f8b-xxx 进入 CrashLoopBackOff
14:22:40 UTC  Kubernetes Event: db-service OOMKilled (exit code 137)
14:22:30 UTC  [db-service] GC pause 超过 10s(来自 JVM 日志)
14:20:00 UTC  Deployment db-service 滚动更新完成(版本 v2.3.1 → v2.3.2)

时间线一出来,根因方向就清晰了:新版本上线导致 OOM。

第三步:假设验证
#

基于时间线提出多个假设,不要只提一个。然后按两个维度排序:

  • 可能性:基于经验和数据,哪个假设最可能是真的
  • 验证成本:哪个假设最容易验证(一条命令能确认的,先验证)
假设优先级矩阵:

              容易验证    难验证
可能性高  →  【立刻验证】  先验证其他,再回来
可能性低  →  最后验证    基本不用管

在排查过程中,每验证一个假设,要么排除它,要么发现新线索。不要把"暂时没证据"当成"这个方向错了”。


时间线构建技巧
#

日志时间对齐
#

# 将不同格式的时间戳转为 Unix 时间方便对齐
# RFC3339 格式
date -d "2025-12-09T14:22:40Z" +%s

# 毫秒时间戳转人类可读
date -d @1733752960

# 在查询 Loki 时,用 Unix 时间戳更精确
logcli query \
  --from="2025-12-09T14:20:00Z" \
  --to="2025-12-09T14:30:00Z" \
  '{namespace="production"}'

多服务日志并行采集
#

# 同时 tail 多个 Pod 的日志(用 stern)
stern -n production "api|db|cache" --since 30m --color always

# 或者用 kubectl 并行查多个
for svc in api-server db-service cache-proxy; do
  echo "=== $svc ===" &&
  kubectl logs -n production deployment/$svc --since=30m --tail=20
done

关联指标与日志
#

最有效的时间线是把 Prometheus 指标和日志混合在一起看。当你看到 P99 延迟在 14:22 开始飙升,立刻去找那个时间点前后 30 秒的 Pod 日志。

# Prometheus 查询:找到指标异常的精确时间
# error rate 突变点
rate(http_requests_total{status=~"5.."}[1m])

# 内存使用突变
container_memory_working_set_bytes{pod=~"db-service.*"}

常见认知陷阱
#

1. 锚定效应
#

第一个看到的信息会过度影响后续判断。“上次也是这样,肯定是数据库” —— 然后花 2 小时翻数据库,发现根因是上游服务超时。

对策:强制列出至少 3 个假设,再开始排查。

2. 幸存者偏差
#

只看到出错的请求,忽略了"为什么其他请求还在正常工作"。有时候正常工作的部分才是关键线索——比如只有特定用户受影响,说明问题在路由层或用户数据层,不是底层基础设施。

对策:主动问"哪些用户没受影响?为什么?"

3. 相关 ≠ 因果
#

“监控告警和部署时间很接近,肯定是部署导致的” —— 但也可能是定时任务在那个时间点运行,或者是流量模式的自然变化。

对策:找到因果链,不能只靠时间相关性。“A 发生,然后 B 发生” 不等于 “A 导致了 B”。

4. 确认偏误
#

找到一个支持自己假设的证据就停手,忽略反对证据。

对策:主动寻找"能推翻我当前假设的证据是什么?"


工具选择:什么问题用什么工具
#

问题类型首选工具原因
“出了什么错”日志(Loki/ELK)错误信息最直接
“什么时候开始的”指标(Prometheus/Grafana)时序数据更直观
“哪里慢”链路追踪(Jaeger/Tempo)可视化调用链延迟分布
“为什么 CPU/内存高”top/kubectl top + pprof进程级别的资源消耗
“网络包丢了没”tcpdump/Wireshark网络层排查
“K8s 资源状态”kubectl describe/eventsK8s 内部状态
# 快速三板斧
# 1. 日志:最近的错误
kubectl logs -n production -l app=api-server --since=10m | grep -i error | tail -20

# 2. 指标:快速看资源
kubectl top pods -n production --sort-by=cpu | head -10

# 3. 事件:K8s 内部发生了什么
kubectl get events -n production --sort-by='.lastTimestamp' | grep -v Normal | tail -20

联系他人的时机
#

这是一个容易被忽视但非常重要的判断:什么时候该找人帮忙?

个人原则:单独排查不超过 30 分钟没有实质进展,立刻拉人。

为什么 30 分钟?因为:

  • 30 分钟内你已经验证了自己最可能的几个假设
  • 如果还没找到,往往是思维定势,需要不同视角
  • 对于 P0/P1 故障,每分钟都有业务损失,协作的效率收益远超单人排查

找人的正确姿势:不是"你来帮我看看",而是:

“故障现象是 XXX,影响范围是 YYY,发生时间 ZZZ。我已经排除了 A 和 B,目前倾向于 C 假设,但卡在 D 这里,你有没有其他思路?”

带着上下文找人,对方能立刻进入状态,不需要重新从头了解情况。


复盘模板
#

故障结束后 24-48 小时内完成复盘,越快越准确。

## 故障复盘报告

**标题**: [服务名] [故障类型] - YYYY-MM-DD

### 基本信息
- 开始时间: 
- 恢复时间: 
- 持续时长: 
- 影响范围: 
- 严重等级: P0/P1/P2

### 时间线
| 时间 | 事件 | 操作人 |
|------|------|--------|
| HH:MM | 监控告警触发 | 自动 |
| HH:MM | On-call 开始排查 | @xxx |
| HH:MM | 定位根因 | @xxx |
| HH:MM | 执行临时恢复措施 | @xxx |
| HH:MM | 服务完全恢复 | @xxx |

### 根因分析(5W1H)
- What: 具体坏了什么
- When: 何时开始
- Where: 哪个组件/模块
- Who: 谁的变更触发(如果是变更引起的)
- Why: 根本原因(技术层面)
- How: 如何触发的(触发路径)

### 为什么没有被提前发现
- 监控盲区?
- 告警阈值不合理?
- 测试用例缺失?

### 行动项
| 行动 | 负责人 | 截止日期 | 优先级 |
|------|--------|----------|--------|
| 补充监控告警 | @xxx | YYYY-MM-DD | P1 |
| 增加自动化测试 | @xxx | YYYY-MM-DD | P2 |
| 更新 Runbook | @xxx | YYYY-MM-DD | P2 |

Blameless 原则:复盘的目的是改进系统,不是追责。“是谁的问题"这个问题在复盘里没有意义,“系统为什么允许这个问题发生"才有意义。


从真实故障中总结的经验
#

在处理过数十次生产故障后,有几条真实有效的经验:

1. 最近的变更永远是头号嫌疑人。 代码上线、配置变更、依赖升级,把这些时间点和故障时间线对比,命中率极高。养成好习惯:每次上线在变更日志里记录时间。

2. 数据库连接池耗尽比数据库宕机更常见,也更难发现。 报错通常是 “connection timeout” 而不是 “connection refused”,看起来像网络问题。

3. 内存泄漏通常在流量高峰被引爆,但根因在代码里。 在低流量时无法复现,让人误以为是"一过性问题”。

4. DNS 解析失败在 Kubernetes 里出现频率比你想象的高。 特别是服务发现依赖 CoreDNS 时,DNS 的轻微抖动会被应用层放大成严重的连接失败。

5. 告警越多越失效。 没有优先级的告警轰炸会让 On-call 产生告警疲劳,真正重要的告警被忽略。定期清理无用告警,比增加新告警更重要。

Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。
SRE 实战手册 - 这篇文章属于一个选集。
§ : 本文

相关文章

SRE 实践心得:从运维到 SRE 的思维转变

·531 字·3 分钟
SRE 不是换了个头衔的运维,而是一套用软件工程思维解决可靠性问题的方法论。这篇文章记录了我在实践过程中最有感触的几个转变。

可观测性建设:从 Prometheus 采集到 Grafana 告警联动

·861 字·5 分钟
可观测性不是装几个监控工具,而是让系统在出问题时能快速定位根因。这篇文章从采集架构到 PromQL 到告警路由,覆盖我们在生产环境中实际遇到的 cardinality 爆炸、告警噪音等问题。