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 魔法¶
任何你不理解的事情都可能隐藏有缺陷。唯一的方法就是纪律,把你不理解的事物都当成缺陷。