Root Cause First
Effort: free — 纯调查纪律,通常还直接降低净成本:一根决定性探针,替掉“把整条流水线点着看看会怎样”。消除:修错对象的补丁——那种藏起真 bug、还弄坏下游的症状修复。
不调查就没有修复。在理解失败之前打的补丁,修的是错的东西,藏起真正的 bug,还会弄坏下游。你的产品不是一块补丁——是一个由决定性探针证明的根因,加上一个被证明不带任何回归的修复。
下面的一切由两条法则统辖:
- 不做假设——代码、数据和实况系统才是真相;笔记只是线索。 一条注释、一段记忆、一个先前的结论,甚至你自己的上一句话,在探针确认之前都只是假设。"所有 / 每个 / 没有"这类字眼触发三点检查:环境、全仓库搜索、扫遍每一个调用方。
- 一个被验证的反例,立即击杀先前的结论。 探针推翻了你相信的东西,就直说:"我错了——实际是 X",然后从新事实继续。绝不粉饰过去。
循环(按顺序跑;不许跳步)
- 读报错。 用一句精确的话说出症状。读实际的报错信息,不是你以为它会说什么。点出爆炸半径:你怀疑的那个东西,都有谁依赖它?
- 复现。 让失败随叫随到——在实况里,或在一条失败的测试里。**掐个时间。**真实工作要花几秒、"失败"却毫秒级就返回,说明是早早被吞掉的异常,不是真实工作在失败。时间差本身就是线索。
- 查最近的改动。 diff 出上次正常以来变了什么——代码、配置、环境、依赖。历史太长就二分。
- 画出调用方地图。 共享面上的 bug,列出每个调用方以及各自的用法(精确字符串匹配?布尔?列表?)。真正的回归通常藏在下游某处的精确匹配比较里,不在你正拧的那个旋钮上。
- 在边界上打探针。 在每条组件接缝上打日志或探针——进什么、出什么。沿着坏数据一个边界一个边界往回追,直到源头。修源头,绝不修症状。
- 靠假设找根因。 提出一个可证伪的假设。找到能把它与其他解释分开的那一个决定性探针,只跑这一个。别为了"看看会怎样"把整条流水线点着。
- 在对的接缝上外科手术式地修。 解决根因的最小改动。优先修那个唯一的共享源头(一个规范化器、一个执行器),而不是改 N 个调用点。可能的话让修复在正常路径上惰性——可证明它在那里什么都不改,只在坏的路径上生效。不许捎带相邻的重构。
- 证明它。 写出复现这个 bug 的失败测试;看它变红;修;看它变绿。然后跑第 4 步画出的每条调用路径的测试——那里的绿就是你的零回归地板。mock 了恰好失败的那条接缝的套件什么都证明不了。
- 实况验证。 开动真实系统——真实请求、真实数据库、真实日志。绝不用一个把代码 import 进自己进程的旁挂脚本。留下前后对比证据。
- 沉淀。 写下症状、决定性探针、根因,以及把它藏起来的反模式,让下一个同形状的 bug 变便宜。
先搭复现循环,再谈理论
发现自己在"红色可复现命令"存在之前就读代码搭理论——停。没有能变红的命令,就没有理论。一个在这个 bug 上会变红的紧凑通过/失败信号,是调试里最大的单项增益。在这里花不成比例的力气。
搭建方式,大致按序:一条失败测试;一个打向 dev 服务器的 HTTP 脚本;用固定输入跑 CLI、与已知良好快照做 diff;无头浏览器脚本;抓一份真实载荷、隔离重放进代码路径;一个只调一个函数的一次性脚手架;随机输入的 fuzz 循环;让自动二分能跑的二分脚手架;差分循环(同一输入过新旧两版,diff 输出)。
然后把它拧紧:更快(缓存准备工作、收窄范围)、更锐(断言具体症状,而不是"没崩")、确定性(钉死时间、固定随机种子、冻结网络)。一个两秒的确定性循环是超能力。
对付偶发 bug,追求更高的复现率,而不是干净的复现:把触发器循环 100 次、加压、收窄时间窗。50% 的偶发能调;1% 的偶发不能。
真的搭不出循环,就停下来直说。列出你试过的,向你的人请求访问权限、一份抓取的产物,或临时插桩。没有循环就不搭理论。而且,如果不存在任何能复刻真实调用模式的接缝,这个"缺失"本身就是一个发现——修复落地后,把这个架构缺口标出来。
反模式(难缠的 bug 靠这些活着)
- 凭一条笔记或注释下结论,没打探针。
- 没复现就修。
- 相信一个 mock 了实况恰好失败那条接缝的绿色套件。
- 旁挂验证——import 代码,而不是开动实况系统。
- 没画出精确匹配的下游调用方,就去拧配置旋钮。
- 修复捎带大范围重构。
- 没做三点检查就说"所有 / 每个 / 没有"。
搭配使用
- red-first — 修复之前先 commit 那条失败测试。
- sniper-testing — 迭代时只跑限定范围的测试。
- seam-engineering — 修掉一类,不是一例。
- repair-loop — 完整的修复加落地周期。
脚手架致谢:Matt Pocock, diagnosing-bugs (mattpocock/skills)。此处的组合方式与硬性规则属于 BACKS AIOS。