「奇怪,刚才还是好的」「重启一下就好了」「反正现在能跑」—— 这些都是没排查完的信号。这篇文章讲怎么把这类问题变成可重复的实验。
什么叫「玄学问题」
它的特征很好认:
- 不稳定:时好时坏,自己也说不清规律
- 不可重复:想复现的时候它就不出现了
- 改了就对了:动了某个地方之后好了,但说不清是不是它起的作用
这类问题之所以难,是因为它缺少一个可重复的实验。没有实验,就只能靠猜;靠猜,就只能靠「多试几次」。
排查的目标只有一个:把不可重复的现象,变成一个能稳定触发的实验。
三步法
第一步:固定观察条件
先把「观测」这件事本身稳定下来:
- 用同一条命令,而不是每次临时想一条
- 记录完整输出,而不是只记「成没成」
- 把关键中间值落盘(写文件),而不是只留在屏幕上
一个很实用的做法是:把验证写成一个固定的小脚本,每次跑它、输出带时间戳的结果。这样你比较的是「两次运行」,而不是「两次记忆」。
记忆是不可靠的观测工具。人会不自觉地记住符合自己假设的那一次。
第二步:构造最小复现
把问题从真实场景里剥出来:删掉所有与它无关的部分,直到剩下最小的一小块。
判断标准是:删完之后问题依然能触发。如果删掉某个部分之后问题消失了,那部分就是嫌疑人。
这一步的价值在于:真实场景里变量太多,无法归因;最小复现里变量少到可以数清。
第三步:单变量对比
一次只改一个东西,然后跑同一个实验。
违反这条会付出很大代价:
- 同时改了三个地方,问题消失了 → 你不知道是哪一个起的作用
- 同时改了三个地方,问题还在 → 你也不知道是哪一个在拖后腿
两种结果都无法归因,等于没排查。
四个好用的具体手法
1. 二分法
面对「一大堆配置 / 一大堆代码」,不要逐行读。注释掉一半,看问题还在不在:
- 还在 → 问题在剩下那一半
- 不在了 → 问题在被注释掉的那一半
每次砍一半,十次以内就能从上千行里定位到一行。
2. 对照组
拿一个已知正常的同类对象做对照。这是区分「目标坏了」和「尺子坏了」的唯一办法。
典型场景:某个校验工具报错。先别怀疑被校验的对象 —— 拿一个你确定没问题的对象,用同一个工具、同一台机器再测一次。如果它也报错,那问题在工具或环境,不在对象。
这一步经常能省掉一整天。
3. 把「我以为」变成「日志里能看到」
大部分玄学来自没有被观测的中间状态。你不知道某个变量在关键那一刻是什么值,只能事后推测。
做法很朴素:在关键路径上打印 / 记录。宁可多打几行日志,也不要靠推理。
尤其是在异步、并发、缓存场景下 —— 你以为的时序往往不是实际的时序。只有日志能告诉你真相。
4. 用「等待」来验证「异步」假设
很多诡异行为的根源是时序:某件事还没做完,另一件事就开始了。
验证方法很简单:在两步之间插入一个等待,看问题是否消失。
- 消失了 → 确实是时序问题(竞态,或者「完成通知」早于「实际完成」)
- 没消失 → 排除时序,回去查逻辑
注意:插入等待只是为了验证假设,不是修复方案。真正的修复应该让程序等待正确的事件,而不是 sleep 一个「大概够久」的时间。
三个反模式
| 反模式 | 为什么有害 |
|---|---|
| 一次改多个地方 | 无法归因,等于没排查 |
| 「再跑一次看看」 | 没有新信息,只是把不确定性再抽一次 |
| 看到现象就下结论 | 现象和原因之间通常隔着好几层 |
还有一条最隐蔽的:「重启之后好了」就收工。
重启会让很多东西回到初始状态,它能让症状消失,但不代表你知道原因 —— 下次它还会回来,而且可能带着更严重的后果。
一个真实的例子
我遇到过「配置明明已经加载了新版本,但请求仍然走旧逻辑」的情况。
按三步法走:
- 固定观察条件:写了一条固定命令,同时输出「当前实际生效的状态」和「一次测试请求的结果」,两行并排看
- 最小复现:去掉所有无关配置,只留一个最小场景 —— 问题稳定复现
- 单变量对比:唯一的变量是「改完之后立刻验证」还是「等两秒再验证」
结论很清楚:「重载」这个动作是异步的 —— 它返回成功之后,旧进程还会继续处理一段时间的请求。所以「立刻验证」在这类场景下是无效观测,读到的必然是旧状态。
修复很简单:验证前留一个生效窗口。但更重要的收获是 —— 从此我把「异步动作之后的即时验证」列进了「不可信观测」清单。
小结
排查问题的本质,是把「我观察到它坏了」变成「我知道它为什么坏,而且能稳定让它坏」。
- 先固定观测,再谈分析
- 先剥离变量,再谈归因
- 一次只改一个东西
- 现象不等于原因,能稳定复现才算摸到边
「能稳定复现」这四个字,比任何调试技巧都值钱。