1. 山重水复疑无路

有效的调试步骤:

  • 弄清楚为什么运行异常
  • 修复问题
  • 避免破坏其他部分
  • 保持或者提高代码总体质量(可读性,架构,测试覆盖,性能等)
  • 确保同样问题不要再次发生

实证方法:构建实验,观察结果

核心调试过程:

  • 问题重现
  • 问题诊断
  • 缺陷修复
  • 反思,吸取教训

澄清问题:

  • 发生了什么,应该发生什么
  • 一次一个问题
  • 先检查简单的事情

2 重现问题

2.1 重现第一,提问第二

如何重现:

  • 明确开始要做的事情(比如缺陷报告的步骤)
  • 抓住重点

控制因素:

  • 软件本身
  • 运行环境
  • 提供的输入

2.2 控制软件

2.3 控制环境

可能包含所有环境

2.4 控制输入

推测可能输入:

  • 回溯工作
  • 探测可能输入值
    • 边界分析
    • 分之覆盖
    • 利用错误条件
    • 引入随机性:模糊测试;生成模糊器;变异模糊器
  • 记录输入值
    • 日志框架。可以开启 or 关闭;日志级别;上下文(代码行数等);分析工具(ELK)
    • 外部日志。代理服务器等
  • 负载和压力。有些缺陷只有在某种压力才表现出来(高负载网络流量,有限的可用内存)

2.5 改进问题重现

2.5.1 最小化反馈周期。编辑-执行-重新问题时间最短
  • 尽可能简单,通过二分法快速定位
  • 自动化最小输入
  • 最大限度减少所需时间,比如营造资源耗尽的局面。
2.5.2 不确定缺陷变为确定

软件之美在于确定性,但是不确定性从哪里来?

  • 开始于不可预知的初始状态。从未经初始化的内存读取数据(强制初始化,或者内存完整性检测软件)
  • 与外部系统交互: 精确控制从外部系统接收了什么
  • 故意使用随机性: 伪随机数
  • 多线程: 通过 sleep 等增加出现竞争的可能性
2.5.3 自动化
  • 自动化测试
  • 重放日志

2.6 无法重现怎么办

  • 缺陷真的存在么?
  • 在相同区域解决不同问题
  • 让其他人参与其中。不同人的视角
  • 充分利用用户群体
  • 推测法:逻辑推理,想象出错的可能性

3 诊断

3.1 不要急于动手,试试科学方法

  • 调试方法:
    • 根据你对软件运行情况的理解,提出一个可以导致这种情况的假设
    • 设计一个实验,证明假设是否正确
    • 如果无法证明你的假设,重新设计然后再次实验
    • 如果实验支持你的假设,继续证明,知道能够证明或者证伪
  • 实验必须起到验证的作用。(与他人争辩试图推翻你的假设)
  • 每次只做一个修改。多处修改可能导致错误的结论。
  • 记录你做的调试。定期回顾你已经尝试过的实验和学到的东西,使用日记簿或者电子笔记。
  • 不要忽略任何细节。凡是你不明白的都是潜在的缺陷。(Anything that you don't understand is potentially a bug)
    • 写出假设。曾经做过的假设写到纸上
    • 可以跟踪详细信息
    • 确保不会忘记需要做的事情
    • 短暂休息,可以尝试头脑风暴

3.2 相关策略

3.2.1 插桩

使用日志,但是不要引入额外的副作用影响了原始代码逻辑。增加日志的时候,不要同时修改代码逻辑。

3.2.2 分而治之

二分法,排除一部分模块或者代码的问题。

3.2.3 源码控制工具

git 找出历史提交,分析引入缺陷的提交

3.2.4 聚焦差异

找到特殊用例,缺陷的边界

3.2.5 向他人学习

网络搜索,同事求助

3.2.6 奥卡姆剃刀

其他条件相同情况下,最简单的解释是最好的。

3.3 调试器

代码检查,断点,单步调试,检查运行状态

  • 开发初期调试器非常有用
  • 让代码以特定方式运行
  • 探究看不懂的代码

随着经验增加和TDD 思想出现,调试器使用会变少

3.4 陷阱

  • 你做的修改是正确的么?如果没有,说明你没修改到点子上。尝试引入明显的失败比如断言让其暴露出来
  • 验证假设。思维定势造成的先验假设可能会让你得不到结果
  • 多重原因。问题有时候是复杂的,
    • 对问题进行隔离,找到一个方法重现缺陷。
    • 先找同一个区域内其他较明显的缺陷,扫清障碍,让缺陷更加明显
  • 查出变化并且控制它。第三方系统或者数据库。

3.5 思维游戏

  • 旁观调试法(小黄鸭)。求助其他人。倾听;提问;留意没有探究过的地方;了解接下来将会发生什么。解释问题会帮你理清思路
  • 角色扮演
  • 换换脑筋。潜意识帮助你
  • 做些改变,什么改变都行。出去散个步最好
  • 福尔摩斯原则:当你排除了一切不可能之后,无论它多么不可思议,它也一定是真相

3.6 验证诊断

  • 向其他人解释你的诊断
  • 检查源代码原始副本
  • 多和其他人讨论,并假设你是错的

4 修复缺陷

4.1 清楚障碍

从一个干净的代码树开始修改。

4.2 测试

  • 保证现有测试都可以通过
  • 添加一个或者多个新的测试程序
  • 修复 bug
  • 证明修复起了作用
  • 证明回归测试没有失败

4.3 修复原因而非修复现象

理解问题的症结所在

4.4 重构

重构:改善代码设计而不改变其行为的过程。

  • 重构应该在有全面单测套件的安全网中才可以安全修改既有代码
  • 重构或者修改功能,不能同时进行。 重构的同时决不能修改代码功能,同样也不能修复有缺陷的代码。(一次只做一件事)

4.5 签入

一次逻辑修改只做一次签入。方便快速回滚和定位代码问题。

4.6 审查代码

5. 反思

5.1 这到底是怎么搞的

  • 如果缺陷仅仅发生在少数用户身上,你就要反思是否还有其他问题?
  • 一个你以为会失败的 case,结果却成功了

5.2 哪里除出了问题?

五个为什么?

  • 我们已经做到了么?
  • 根本原因分析。责备没有用
    • 需求是否正确,是否是模糊的,误解了
    • 架构或者设计。有没有正确按照设计来做
    • 测试。测试覆盖率足够么?
    • 构造

5.3 它不会再发生了

  • 自动验证,检查是否存在问题
  • 和同事交流
  • 重构代码避免被不正确使用
  • 过程。检查工作过程

5.4 关闭循环

编码规范;测试规范;文档规范;报告/跟踪过程;设计指南;性能需求

6. 发现代码存在问题

6.1 缺陷追踪

  • 使用缺陷追踪系统。
  • 缺陷报告。具体,明确,详细的。
  • 环境和配置报告。自动收集环境和配置信息,不要指望用户

6.2 与用户合作

  • 简化流程
  • 自动化
  • 提供多种选择
  • 尽量简单
  • 模板不要死板
  • 尊重用户隐私

有效的沟通:

  • 心智模式。从用户角度设想一下
  • 和非技术人员沟通
  • 发布缺陷数据库
  • 隐私问题;提供反馈;拜访用户

6.3 与支持人员协同工作

学会沟通;和 QA 搞好关系,以便在修复缺陷的过程中得到帮助。

7. 务实的零容忍策略

7.1 缺陷优先

早期修复缺陷是一个好的策略:

  • 可能发现缺陷的过程(比如测试、代码审查、用户使用等)要连续贯穿整个开发过程
  • 缺陷修复优先于其他任何事情
  • 质量底下具有传染性。
  • 破窗理论。发现缺陷就立马解决,不要留破窗户

7.2 调试的思维模式

既要完美,又要实用。接近零容忍,但是用务实的心态去实现。

7.3 自己来解决质量问题

面对大量bug 如何摆脱困境?

  • 没有灵丹妙药。
  • 停止开发有缺陷的程序。阻止事情恶化。
  • 从不干净的代码中分离干净代码
  • 缺陷分类
  • 缺陷闪电战。集中较短时间专门修复缺陷

8 特殊案例

8.1 修复已经发布软件

集中精力减少风险。

8.2 向后兼容

将确定兼容问题加入到你的缺陷修复检查列表中

  • 提供迁移方法
  • 实现一个兼容模式。比如 word 就是这么做的
  • 提供预警。java api 升级
  • 不修复。极少数情况是一个务实的方案

8.3 并发

简单和控制

让并发软件中问题更少地发生不是一个可取修复办法。 修复并发缺陷时,避免实用 sleep 方法。

8.4 海森堡缺陷

观察者效应。尽量减少收集信息带来的副作用

8.5 性能缺陷

  • 寻找瓶颈。性能分析器。通过检查代码有时候预测瓶颈是不可靠的。
  • 没有瓶颈呢?是否是资源耗尽;垃圾收集;缓存丢失等

8.6 嵌入式软件

仿真器;硬件还是软件问题;

8.7 第三方软件缺陷

  • 不要太快去指责。首先怀疑你自己的代码
  • 报告别人代码中的缺陷。慎重自己直接修复
  • linux 法则:眼睛足够多,bug 无处藏
  • 报告问题:检查文档;尽可能给出足够信息

9. 理想的调试环境

9.1 自动化测试

  • 结果明确,通过还是失败
  • 环境独立
  • 所有测试可以独立运行
  • 全面覆盖

9.2 源程序控制

  • 合理使用代码分支
  • 坚持分之单一层级
  • 设计 CI 构建所有正在活跃的分之
  • 小的修改更加容易理解,合并和撤销
  • 不要一次合并多个分之

9.3 自动构建

  • 自动化构建整个过程,从开始到完成。
  • 持续集成
  • 静态分析(lint工具)。不要依赖未定义行为;谨慎对待编辑器警告

10. 让软件学会自己寻找缺陷

10.1 假设和断言

fail early,使用断言保护代码。

契约式编程:

  • 先决条件。不如参数不能是空
  • 后置条件。调用之后保持的条件,比如一个 http addHeader() 方法调用之后 headers map 比原来多一个
  • 不变量。方法调用之前它的先决条件被满足就始终为真的数据。

支持开启和关闭断言功能。

软件在产品阶段应该是鲁棒的,在调试阶段应该是脆弱的。 断言是一个缺陷检测机制,不是一个错误处理机制。

10.2 调试版本

不要等待资源泄露表现出来,主动今早地检测它们。

11. 反模式

11.1 夸大优先级

质量差才是导致大量缺陷的原因。

  • 定期清理缺陷
  • 控制优先级
  • 不要使用数字表示优先级

11.2 超级巨星

巨星效应会破坏团队。一开始匆忙实现看似非常高效率(所以被称为"巨星"),但是遗留了一堆棘手的烂摊子。

  • 确保完成就是完成,需要功能被测试过了,检查过了,记录文档了。
  • 任务分解为小任务
  • 自负自责。谁搞得 bug 谁负责修理它。(有时候你可能要考虑拍屁股走人了,比如一堆 c 艹的烂摊子???)

11.3 维护团队

最终开发和维护都要由一个团队来完成,保持连续性。不要区分开发和维护团队。

11.4 救火模式

退一步,找出根本原因,再去解决问题

救火模式永远也不会修复任何质量问题。

11.5 重写

从心理学角度来看,开发新代码比重写旧的代码让人更加舒服。天生乐观会让我们低估了复制旧功能要付出的精力和时间。 避免彻头彻尾重写,增量式重写代码。

11.6 没有代码所有权

集体代码所有权(极限编程)

11.7 魔法

任何你不理解的事情都可能隐藏有缺陷。唯一的方法就是纪律,把你不理解的事物都当成缺陷。