系统性调试(Systematic Debugging)
摘要
系统性调试是一套用于处理各类技术问题的规范调试方法,核心原则是在尝试修复前必须先找到问题的根本原因,避免随机修改浪费时间、引入新问题。该方法将调试拆分为四个必须按顺序完成的阶段,能够大幅提升调试效率、提高一次修复成功率,适用于所有类型的技术问题,尤其是时间紧张、问题看似简单的场景。
核心原则
核心铁律:在没有完成根本原因调查前,绝不进行修复,未完成第一阶段调查就提出修复方案属于违反调试精神的行为。
适用场景
适用于所有技术问题:
- 测试失败、生产环境Bug、异常行为
- 性能问题、构建失败、集成问题
尤其需要使用该方法的场景:
- 时间压力大(紧急情况容易引发猜测式修复)
- 看起来可以”快速修复”
- 已经尝试过多次修复均失败
- 不完全理解问题本身
- 问题看起来非常简单、开发者赶时间、管理者要求立即修复
四个调试阶段
必须按顺序完成每个阶段,才能进入下一阶段。
阶段1:根本原因调查
尝试任何修复前必须完成:
- 仔细阅读错误信息:不要跳过错误/警告,完整阅读调用栈,记录行号、文件路径、错误码,错误本身通常已经包含解决方案
- 稳定复现问题:确认能否稳定触发、记录准确复现步骤,如果无法稳定复现需要先收集更多数据,不能猜测
- 检查最近变更:查看Git提交差异、最近的依赖变更、配置变更、环境差异
- 多组件系统收集证据:在每个组件边界记录输入输出数据、验证配置传播、检查各层状态,先确定故障发生在哪一个组件,再针对性调查
- 追踪数据流:对于调用栈深处的错误,从错误位置反向追溯,直到找到错误值的起源,在源头修复而非症状处修复
阶段2:模式分析
修复前先归纳模式:
- 找到代码库中类似的可运行示例
- 和标准参考实现完整对比,不要略读
- 列出正常工作和故障代码的所有差异,哪怕看起来很小的差异也不要忽略
- 梳理清楚所有依赖、依赖所需的环境配置、依赖的隐含假设
阶段3:假设与验证
使用科学方法验证:
- 提出单个明确的假设,清晰写出”我认为X是根因,因为Y”
- 做最小修改验证假设,一次只测试一个变量,不要同时修改多个地方
- 验证结果:假设成立则进入下一阶段,不成立则提出新假设,不要在错误假设基础上叠加新修复
- 无法确定问题时,坦诚说明并寻求帮助、补充调研
阶段4:落地修复
修复根因而非症状:
- 先创建可复现问题的失败测试用例,尽可能自动化
- 只针对找到的根因做单个修复,不要顺便做其他改进或重构
- 验证修复结果:测试通过、没有破坏其他测试、问题确实解决
- 如果修复失败:尝试少于3次则回到阶段1重新分析,3次及以上修复失败则需要停止,反思是否存在架构问题,讨论后再继续
需要停止并回归流程的红色信号
出现以下任何想法/情况都需要停止当前操作,回到阶段1重新开始:
- 想先快速修复以后再调查
- 想随便改个东西试试效果
- 同时加多个修改再跑测试
- 跳过测试,只做手动验证
- 不确定根因就觉得大概是某个问题
- 不完全理解问题但觉得修改可能有用
- 3次修复失败后还想尝试”再改一次”
- 每次修复都在不同位置引出新问题
实际效果
根据实际调试统计:
- 系统性调试:15-30分钟完成修复,一次修复成功率95%,几乎不会引入新Bug
- 随机修改调试:2-3小时无效折腾,一次修复成功率仅40%,经常引入新Bug
快速参考表
| 阶段 | 核心活动 | 成功标准 |
|---|---|---|
| 1. 根本原因调查 | 读错误、复现、查变更、收集证据 | 明确问题是什么、为什么出现 |
| 2. 模式分析 | 找可用示例、对比 | 找出正常和故障的差异 |
| 3. 假设验证 | 提出假设、最小验证 | 假设得到验证或提出新假设 |
| 4. 落地修复 | 建测试、修复、验证 | 问题解决、所有测试通过 |