动手之前:先回答三个问题
拿到 bug 报告,先别打开编辑器。回答这三个问题:
故障的可观察症状是什么? 不是"系统有问题",而是"用户点提交按钮后页面返回 500"。写成一句话。
你对数据流了解到什么程度? 信号从哪进来、经过哪些模块、最终到哪。说不清楚的环节标出来——那里最可能出问题,也最容易误判。
你能稳定复现吗? 如果不能,先找复现条件再做别的。不能复现的 bug,你改了也验证不了。
三个问题答完,你已经完成了"理解系统"和"让它出错"两条规则的最小执行。
做到位的标准:能用两句话向同事描述故障现象和复现步骤。说不清楚,这步还没完成。
先排除最蠢的可能
打开 IDE 或示波器之前,花一分钟做傻瓜检查。
环境变量对吗?配置文件指向的是当前环境吗?依赖版本和预期一致吗?网线、电源、连接器有没有松?服务启动了吗?
这些问题看起来侮辱智商。但 Agans 的案例反复证明:最终的根因有相当比例就是这类简单错误,而它们通常在排查链路的最后才被想到。
判断点:如果傻瓜检查都通过了,进入下一步。如果这步就找到了问题——恭喜,你刚省了三小时。
观察,不是猜测
停止猜测。打开日志、加断点、插探针。看系统实际在做什么,不是你以为它在做什么。
具体动作取决于你的环境。软件:看日志、看监控、加 print 或断点。硬件:用万用表、示波器、逻辑分析仪。关键是:此刻你正在看数据,还是在脑子里编故事?
记下你看到的每个异常现象——不只是你觉得相关的那些。调试中被忽略的"不相关"现象,经常事后证明是关键线索。
偏离信号:如果你花了二十分钟在"想"而不是在"看",强制切换——打开一个新终端,运行一个能产出数据的命令。
二分法缩小范围
范围太大,不知道从哪查?砍一半。
在链路中间插入一个检查点。上游正常还是异常?如果上游正常,问题在下游——再砍一半。每次缩小一半,四五轮就能从千行代码或二十个模块里逼近目标。
具体切法因系统而异。微服务架构:先确认是哪个服务返回了错误。单体应用:在函数调用链中间加日志。硬件:在电路中间断开,分别测两段。
滚回条件:如果连续三次二分都指向同一个模块,但在那个模块里查不到问题——可能切面选错了。退回去,换一个维度切。
一次改一个变量,记下每一步
找到可疑点了。准备改。
改之前:记下当前的状态(代码版本、配置、环境变量、输入数据)。改一个变量。跑一遍复现步骤。观察结果。
有变化?记下来。没变化?退回去,换下一个变量。
整个过程保留日志。不是"大概记得试了什么",是可以回溯的记录:什么时间、改了什么、观察到什么、结论是什么。
完成标准:你改了一个变量,问题消失了。你退回这个变量,问题重现。因果关系确认。
卡住了怎么办
按上面的步骤走了一遍,问题还在——先检查两件事。
复现是否稳定? 有没有可能你之前观察到的"正常"其实是偶然?回去重新确认复现条件。
理解是否到位? 拿一个人当听众,把问题从头到尾讲一遍。不是找人帮你查 bug——是在讲的过程中整理自己的逻辑。超过一半的卡点在这一步就会暴露。
如果讲了之后还是卡住,你可能需要扩大系统理解的范围:读文档、读源码、问维护过这个模块的人。
确认修复:三个问题
问题消失不等于修好了。合代码之前回答三个问题:
根因是什么? 一句话说清楚。如果说不清楚,你可能只是绕过了症状。
为什么之前坏? 指向具体的代码行、配置项或硬件状态。不是"因为那里有个 bug"——是"因为那个函数在输入为空时返回了 null,下游没处理"。
为什么现在好,以后不会再坏? 修复覆盖了触发条件吗?有没有类似的路径也会触发?需要加测试吗?
三个问题都答上来,才算修完。有一个答不上来,你需要继续验证。
产出:根因记录(一段话)、修复变更(代码或配置)、验证方法(复现步骤 + 回归测试)。