线上报错,第一反应常是"把日志级别调高再重启看看"。这个动作不坏,但它不是排查,是碰运气。排查的本质是在信息不全时用最小代价缩小可能性空间——每做一次动作,都要能回答"这次排除了哪些可能"。
下面按执行顺序拆:定现象 → 列假设 → 排优先级 → 单变量验证 → 二分收敛 → 确认机制 → 留证。
1. 排查的本质:缩小可能性空间
一个故障的可能原因空间是有限的(几十到几百个候选),排查就是不断切分这个空间。两种做法在动作上很像,在收敛速度上差很远:
| 维度 | 凭感觉试 | 假设驱动 |
|---|---|---|
| 动作来源 | 记忆里"上次是这个" | 从机制推"哪一环" |
| 每次动作的信息量 | 常为 0(改了也不知排除了啥) | 明确排除一个分支 |
| 变量控制 | 同时改几处 | 一次只改一处 |
| 收敛方式 | 随机游走 | 每轮至少减半 |
| 结束后 | 不知为什么好了 | 能写出机制与复发条件 |
**差别在"每次动作后能排除什么"。**答不上来,这个动作就只是在消耗时间。
自检:**你能说出三个可能原因吗?各自被什么证据区分?**只能说一个,说明你在验证猜测,不是排查。
2. 第一步:把现象说清楚,而不是猜原因
现象不清,后面的推理都建在流沙上。先固定四件事:
| 要素 | 要问的问题 | 为什么必须先定 |
|---|---|---|
| 可复现性 | 必现 / 偶发 / 只在某环境 | 决定能否做实验验证 |
| 影响面 | 哪些用户、接口、多少比例 | 定优先级,也缩小范围 |
| 时间边界 | 从何时开始,与某事件是否同时 | 指向"最近变更" |
| 最近变更 | 部署、配置、依赖、数据、证书、流量 | 先查这一项命中率高 |
写成一句话的现象陈述,比"系统很慢"有用得多:
现象陈述模板
- 现象:<接口> 在 <条件> 下 <表现>,错误原文:<粘贴>
- 范围:影响 <用户/接口>,占比 <n/总数>,其他接口是否正常
- 时间:首现 <时间>,是否持续,是否与 <发布/配置/流量> 同时
- 复现:<环境> 下按 <步骤> 可复现 <n/m> 次
- 已排除:<已验证不成立的假设及证据>
最后一行最容易被跳过,也最能防重复劳动:没有它,三个人会把同样的三个假设各验一遍。
3. 假设从机制来,不从记忆来
从记忆里捞,只能捞到见过的;从机制里推,才能覆盖没见过的。做法是把请求链路画成线,逐个节点问"这里坏了会有什么表现":
机制树(沿请求路径逐层展开)
入口 → DNS/CDN → 网关 → 认证 → 线程池 → 缓存 → DB/下游 → 磁盘/网络
每层问三个问题:
a. 这一层失败时现象是什么?(报错 / 超时 / 错数据 / 静默变慢)
b. 可观测证据有哪些?(指标 / 日志 / 链路 / 系统命令)
c. 最快的区分动作是什么?
展开后按层的性质归类,可以快速定位"现象与层的对应关系":
| 层 | 典型现象 | 首选证据 |
|---|---|---|
| 网络/DNS | 连接超时、解析失败、间歇失败 | curl -v、dig、建连耗时 |
| 网关/限流 | 429、502、504,只影响部分请求 | 网关日志、限流计数 |
| 线程池/队列 | 延迟升高但 CPU 不高 | 线程栈、队列长度、park 占比 |
| 缓存 | 命中率骤降、DB QPS 突增 | 命中率、慢查询数 |
| 数据库 | 慢查询、锁等待、连接打满 | 慢日志、show processlist |
| 资源 | OOM、磁盘满、句柄耗尽 | 内存/磁盘/inode/ulimit |
**列假设先求覆盖,不求正确。**典型情况:列 5–8 个候选,其中 3 个能用一条命令当场否掉,剩下 2–3 个才需要实验。
4. 验证的优先级:三类假设先做
不是所有假设都值得验证。三条依据,命中任意一条就提前:
| 依据 | 含义 | 例子 |
|---|---|---|
| 一击排除 | 一个观测否掉一整片可能 | 看连接数,同时排除"池满"与"下游挂" |
| 代价最低 | 一条命令 vs 一次发布 | 先 curl,后改代码 |
| 覆盖最广 | 一个证据服务多个假设 | 时间线对齐一次,验"最近变更"全部子项 |
排序后的执行顺序大致是:
第 1 轮(分钟级,只读)
□ 时间线:现象起点 vs 发布/配置/证书/流量变更点是否重合
□ 范围:单接口还是全站?单实例还是全集群?
□ 资源:CPU/内存/磁盘/inode/连接数/fd 是否触顶
□ 错误原文:日志里第一条异常,而非最后一条
第 2 轮(十几分钟,轻量实验)
□ 复现:可控环境复现一次,确认不是看错了
□ 二分:范围对半切(实例/时间/参数/数据)
第 3 轮(需要改动)
□ 单变量变更 + 明确预期 + 明确回滚
第 1 轮刻意全用只读动作:不引入新变量、不污染证据;任何一次重启或改配置都会把现场洗掉。
5. 一次只改一个变量
同时改三处然后好了,信息量为零:不知哪一处生效,也不知另两处是否在掩盖问题。紧急时最易破这条纪律,代价也最大。
| 动作 | 可归因 | 后续代价 |
|---|---|---|
| 改 A,观察,记录 | 是 | 多花一轮时间 |
| 同时改 A/B/C | 否 | 复发无法定位,可能引入新问题 |
| 改 A 回滚再改 B | 是 | 需要能快速回滚 |
| 重启+改配置+扩容 | 否,且破坏现场 | 证据丢失,复盘无据 |
紧急时允许"同时改以止损",但守两条:**记下同时改了哪几处;止损后逐个回滚复现。**止损是恢复服务,归因是另一件事。
变更记录(每次改动一行)
时间 | 改动 | 预期 | 实际 | 回滚
10:02 | 线程池 200→400 | 队列下降 | 仍涨 | 是
10:15 | 关掉新上线的 N+1 查询 | 慢查询归零 | 归零,延迟恢复 | 否
这张表写起来三十秒,却直接决定复盘时能不能说出根因。
6. 二分法:适用范围比想象中窄
二分的前提是有"可判定的分界",且两边是同一维度的两段。五种最常用:
| 维度 | 怎么切 | 判定信号 | 注意 |
|---|---|---|---|
| 版本 | 上一版 / 当前版 | 切回后现象是否消失 | 数据库变更常不可回退 |
| 配置 | 一半实例用旧配置 | 是否只在旧配置实例上 | 需要能定向路由 |
| 流量 | 按用户/租户/IP | 两组现象差异 | 两组条件要相近 |
| 数据 | 按大小/编码/地区 | 只有某类数据出错 | 要能定位具体样本 |
| 时间 | 按时间窗口 | 起点前后行为差异 | 注意缓存与预热 |
不适用三种:原因是单点且确定(证书过期看日期即可);切分会破坏现场(只能重启验证);两侧不同质(灰度实例规格本就不同)。切之前先确认两组在其他维度可比,否则比的是两个混杂变量。
7. "改了就好了"不等于找到了原因
现象消失有两种可能:原因被移除,或原因还在但触发条件暂不满足。区分只看一条:能否用机制解释"为什么这个改动会让现象消失"。
| 状态 | 判据 | 风险 |
|---|---|---|
| 已定位 | 能说出机制链 A→B→C→现象 | 低 |
| 已规避 | 现象消失但说不清机制 | 高,会复发 |
| 假性恢复 | 因流量降了或重启清空状态 | 很高,下次更难查 |
| 归因错误 | 有解释但与证据冲突 | 高,会改错地方 |
典型情况:重启后恢复,结论写成"内存泄漏,已重启解决"。但重启同时清空了连接池、缓存、队列、临时文件。证据不足时正确写法是"现象随重启消失,机制未确认"。
判据:**解释能否预测一个尚未观察到的现象?**若你说"连接未释放导致耗尽",就该能观察到连接数单调上升并在重启前触顶。
8. 排查日志:把过程变成可复用的资产
排查结束后能留下的只有记录。价值不在存档,在两点:复发不用重来,别人能直接用。
## 排查记录 YYYY-MM-DD HH:MM
### 现象
<一句话现象 + 错误原文>
### 假设与验证
| # | 假设 | 验证手段 | 结果 | 结论 |
|---|---|---|---|---|
| 1 | 下游超时致线程堆积 | 下游 P99 + 线程栈 | P99 正常,栈多在 DB 等待 | 排除 |
| 2 | 慢查询打满连接池 | 慢日志 + 连接数 | 连接数打满 | 成立 |
| 3 | 缓存命中率下降 | 命中率指标 | 平稳 | 排除 |
### 结论
机制链:<查询未走索引> → <单次 800ms> → <连接池占满> → <排队> → <超时>
### 复发判据(下次先看这三处)
1. 连接池 active 与 wait 同时升高
2. 慢日志条数与延迟同步上升
3. 缓存命中率不变(用于区分"缓存失效")
### 未解决
<现在还解释不了的部分>
表格式的"假设→验证→结论"是核心:后来者能直接看到哪些已排除。
9. 排错:五种排查本身失效的情况
| 现象 | 原因 | 处理 |
|---|---|---|
| 查很久没收敛 | 假设都来自同一层 | 重画链路,从没查过的层列假设 |
| 每次结论都不同 | 同时改多处,证据被污染 | 回滚到已知状态,单变量重来 |
| 复现不了 | 依赖环境/数据/时序 | 用流量或数据二分,不强求本地复现 |
| 修好两周后复发 | 只做规避,未确认机制 | 补"复发判据",观测项进告警 |
| 三人查出三结论 | 没共享"已排除"清单 | 建共享记录,先对齐已排除项 |
第三条展开:**"不能复现"不是终点,而是要求换验证手段。**偶发问题多源于竞争或时序,靠单机复现很难,但可用"哪类请求出错、哪类不出错"来二分。
10. 边界:什么情况不该先排查
| 场景 | 为什么 | 先做什么 |
|---|---|---|
| 正影响大量用户 | 排查耗时不可接受 | 止损(回滚/降级/扩容)再排查 |
| 现场正被破坏(磁盘满) | 证据会丢 | 先保全证据:导出日志、抓快照 |
| 原因是明确单点事实 | 排查是浪费 | 直接核对证书日期、配置值、版本号 |
| 需跨团队协作 | 单人会卡在权限 | 先拉人对齐"已排除"清单 |
| 无可观测性 | 没有证据可依 | 先补最小观测,再谈定位 |
取舍一句话:止损优先于归因,证据保全优先于操作。
11. 自测
- 排查的本质是什么?凭什么判断一次动作有没有价值?
- 现象陈述里"已排除"这一栏为什么必须写?
- 假设应该从哪里推出来?为什么不从记忆里捞?
- 验证优先级的三条依据分别是什么?为什么第 1 轮要全是只读动作?
- "重启后恢复正常"为什么不能直接写成"已定位"?正确的写法是什么?
- 二分的三种不适用情况是什么?