排查的本质:假设驱动的科学方法#
每次看到一个故障,脑子里第一反应往往是"上次也是这个问题,肯定是 XXX"。这种直觉有时有用,但在复杂系统里经常把你带进死胡同,浪费大量时间。
真正有效的排查,本质上是一个科学实验的过程:
- 观察现象
- 提出假设
- 设计验证实验
- 根据结果更新假设
- 循环直到找到根因
听起来像废话,但很多人在第 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/events | K8s 内部状态 |
# 快速三板斧
# 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 产生告警疲劳,真正重要的告警被忽略。定期清理无用告警,比增加新告警更重要。






