--- id: locate-unknown-fault type: sop group: B status: published version: 1 updated: 2026-11-06 token_estimate: 2275 --- # 如何定位一个说不清原因的故障 > **用途**:现象说不清、原因不知道、也不知道错在哪一层 —— 把它缩小到一层、一个环节、一行。 > > **覆盖**:软件、站点、脚本、自动化流程。有明确报错的除外。 > > **产出**:一句「问题在这一层」+ 一条能重复触发的路径 + 已经排除掉的范围。 > > 拿到就能跑:**先别改。** 改动会毁掉证据,这是排查里最常见的自伤。 --- ## 触发的变异问法 - 「不知道为什么就这样了」 - 「刚才还好好的」 - 「有时候对有时候不对」 - 「改了一处,结果另一处坏了」 - 「日志里没报错,但结果不对」 - 「怎么查这个问题出在哪」 - 「排查了半天没头绪」 - 「是前端的问题还是后端的问题」 - 「重启一下就好了,但过几天又犯」 --- ## 什么时候不要用这份文件 - **有明确报错和堆栈** —— 直接读报错,不需要排查流程。 - **现象能一句话说清原因** —— 那不是排查,是修复。 - **是「不好用」而不是「坏了」** —— 需求问题,改代码解决不了。 - **环境根本没在跑** —— 先启动,再谈定位。 --- ## 判断:先问三个问题 **问之前先做一件事:把现在的现象原样记下来。** 什么时候、做了什么操作、看到什么。改了之后就再也回不到这个状态。 | # | 问什么 | 怎么问出口(原话) | 判据 | 答不上来时 | |---|---|---|---|---| | Q1 | 能稳定复现吗 | 「用刚才那套操作再做三次,是不是三次都出现?」 | 三次都出现 → 能复现,转 Q2;时有时无 → **路径 C** | 让他把操作写成步骤,你照着做;你这边不复现、他那边复现,本身就是一条信息 | | Q2 | 知道什么时候开始不对吗 | 「上一次它是好的,是什么时候?」 | 说得出时间点 → **路径 B**;说不出 → Q3 | 翻最近一次正常的结果,看它和现在差在哪 | | Q3 | 那前后有改动吗 | 「最后一次正常之后,改过什么?代码、配置、依赖、数据都算。」 | 有改动 → **路径 B**;没有 → **路径 A** | 查文件修改时间、版本记录、部署记录,别靠回忆 | ### 分支 | 条件 | 路径 | |---|---| | Q1 = 能复现,Q2 = 知道时间点 | **B** · 变更回溯 | | Q1 = 能复现,Q3 = 有改动 | **B** · 变更回溯 | | Q1 = 能复现,其余无线索 | **A** · 二分定位 | | Q1 = 不能复现 | **C** · 记录等待 | --- ## 路径 A · 二分定位 能复现但不知道从哪下手时用。核心是**每次切掉一半**,而不是「猜一个试试」。 做法: 1. **先确定两端。** 「能触发」的一端(什么操作必出问题)和「不能触发」的一端(什么操作正常)。两端之间就是问题所在的范围。 2. **在中间取一个点。** 把流程从中间截断——只跑到一半,看结果对不对。 3. **根据结果砍掉一半。** 前半段有问题 → 后半段可以不管;前半段正常 → 问题在后半段。 4. **重复。** 每轮范围减半,十轮以内能从上千行缩到几行。 判据:**每做完一步,范围必须明显变小。** 如果做了一步范围没变小,说明那一步没切在有效的分界上,换一个切点。 ⛔ 不要同时改两个地方再看结果。改了两个,就不知道是哪个起的作用,等于白做一轮。 ## 路径 B · 变更回溯 知道时间点或有明确改动时,这条路最快——**问题几乎总在最近变过的地方**。 做法: 1. **列出改动清单**:代码、配置、依赖版本、数据、权限、证书、外部服务。⛔ 只列代码是不够的,一半的问题出在别处。 2. **从最近的开始,逐个回退验证。** 回退一个、验一次,确认后再动下一个。 3. **一次只回退一个。** 一次回退三个,好了也不知道是哪个,坏了也不知道是哪个。 如果全部回退后问题还在 → **说明它本来就在,只是现在才被发现**。这跟「改坏了」是两回事:前者要查的是「什么时候引入的」,后者要查的是「改了什么」。判断错了方向,会一直在最近的改动里绕圈。 ## 路径 C · 不能复现时 时有时无最难受。这时**不要猜着改**——改对了也不知道为什么好,改错了会引入新问题。 做法: 1. **加记录,不改逻辑。** 在关键节点打日志或留痕:输入是什么、走了哪个分支、输出是什么。 2. **等它再犯。** 记录比猜测可靠,因为它不依赖你想到。 3. **拿到一次现场再判断。** 有了一份完整的「犯病时是什么样」,它通常就能变成能复现的问题,转路径 A。 ⛔ 不要因为「这次没出现」就宣布修好了。没复现不等于不存在。 --- ## 四条通用原则 1. **先别改。** 排查时最贵的动作是改代码——它会把可复现变成不可复现,把证据变成回忆。 2. **一次只动一个变量。** 两个一起改,结果无论好坏都无法归因。 3. **排除法优于猜测。** 猜对了是运气,排除了才是知识。每排除一项就写下来,避免重复排。 4. **记录走过的路。** 排查中断再回来时,没有记录就得从头试一遍。 --- ## 输出规范 ### 人读 ``` 现象:<原样记录,含时间与操作> 能否复现:能(步骤…)/ 不能 路径:A 二分 / B 变更回溯 / C 记录等待 已排除:<哪些层、哪些改动,各自用什么方法排除的> 判断:问题在 <哪一层 / 哪个环节> 下一步:<具体动作> ``` ### 机器读 ``` symptom: <原始描述> reproducible: true|false path: binary|change|wait ruled_out: [{layer, method, evidence}] located_at: <层 / 环节 / 文件:行> confidence: confirmed|likely|guess next_action: <具体动作> ``` `confidence` 必须是三档之一。**「guess」也算合法结论**——比装作确定好,它告诉下一个人该先验证什么。 ### 一句话版 `问题在 X 层,已排除 A、B;下一步验 C。` 或 `暂不能定位,已加记录等复现。` ⛔ 不要说「应该是缓存问题」就结束。没有排除记录的判断,都是猜。 --- ## 常见错误 - **边查边改** —— 改完现象变了,原来的复现步骤失效,被迫从头再来。 - **同时改两处** —— 好了不知道哪个起的作用,坏了也不知道哪个引起的。 - **在最近改动里绕圈** —— 如果全部回退后问题还在,说明它本来就有,方向错了。 - **把「重启后好了」当修好** —— 重启掩盖症状,不解决原因。 - **不看日志就说没日志** —— 日志可能在别的位置、别的级别,或者根本没打。 - **跳过「什么时候开始的」** —— 这一个问题常常直接给出答案,却最容易被跳过。 - **猜着改,改完就上线** —— 没验证过原因的修复,只是把故障换成另一个故障。 --- ## 最后核对 2026-11-06。方法与工具无关;三条路径的选择判据(能否复现 / 是否知道时间点)通用。与所执行系统的既定标准冲突时,以那个系统的标准为准。