如何克服面对Bug时的焦虑

发表于:2017-11-27来源:SexyCode作者:SexyCode点击数: 标签:bug
1947年,一只小飞蛾飞进了继电器,导致设备发生故障,于是,现场一位计算机科学家Grace Hopper 把导致程序发生故障的问题称为Bug,把消灭臭虫的行为,称为Debug(在一个单词前面加上

调试是一门科学,任何不懂原理就进行的操作都是耍流氓

1947年,一只小飞蛾飞进了继电器,导致设备发生故障,于是,现场一位计算机科学家Grace Hopper 把导致程序发生故障的问题称为Bug,把消灭臭虫的行为,称为Debug(在一个单词前面加上De,通常表示反面的意思,如compress和decompress),后来这个叫法渐渐成了计算机术语。

 

今天的我们,如果遵循TDD原则进行开发,那么我们的开发的过程,其实就是先写不能通过的测试用例,制造Bug,然后再写生产代码,然所有测试用例都可以通过,从而消灭所有已知的Bug。由此看来,说Debug是软件开发非常重要的一个过程,一点都不为过。

自学会编程以来,消灭的臭虫不计其数,有的只需几秒钟即可将其消灭,有的则持续数日,而且似乎每个十分难缠的Bug,都会在定位了很久很久之后,在自己某个灵(ren)光(pin)一(bao)现(zha)的时刻,突然就work了!以至于现在遇到Bug,都会告诉自己,别慌,解决它只是时间问题。

直到最近偶然看到了这本书——《调试九法》,这是我看到的第一本讲调试方法论的书,回想自己之前在解决Bug时的经常遇到的手足无措,像无头苍蝇一般四处乱撞,最后瞎猫碰上死耗子般地解决了Bug,我毫不犹豫地买下这本书。

是时候需要一套系统的调试理论了

希望本文介绍的这套调试学的知识,可以帮助你在今后遇到Bug时,思路更加清晰,心情更加淡定,从容不迫地去解决问题。

Bug的种类

Bug,按照其产生的原因,可以分为两种:

  • 由于设计错误导致的Bug,比如由于桥梁建筑师设计时的失误导致的桥梁崩塌,又比如软件开发人员未进行输入校验导致的Bug,我们通常管解决这种Bug的过程叫做调试
  • 在设计没有问题的情况下,由外部环境引起的Bug,比如设备老化导致的问题,又比如内存不够导致的Bug,我们通常把解决这种Bug的过程叫做故障维修

我们这里要介绍的调试方法,既适用于调试,也适用于故障检修,这些技术不关心问题如何产生,而是告诉你如何找到问题发生的具体原因。

下面我将分享我读完这本书之后,形成的一套调试方法论,如果你已经迫不及待地想去读这本书,不妨把下面这段内容作为书的导读。

调试法则总览

这套方法论可以总结成一句话: 
调试是一门科学,任何不懂原理就进行的操作都是耍流氓。

这句话可以分为两个步骤进行实践:

准备工作:

  • 如果你要调试这个系统,首先你必须先理解它。
  • 在开始调试之前,检查一下“插头”,别因为一些简单的问题而瞎忙活半天。
  • 确定“插头”没有问题之后,你需要重现Bug

寻找原因并解决Bug

  • 不要认为你猜测的原因是对的,要去观察,验证你的猜测。
  • 采用分而治之的方法,定位多模块的系统问题和修复由多个子问题引发的问题。
  • 一次只修改一个地方
  • 做好修改记录
  • 如果正向调试走不通,不妨试一下反向调试法
  • 最后一招:求助他人

下面对这些法则逐一进行详细地介绍。

理解系统

理解系统是定位Bug的前提条件。

我之前遇到程序抛出异常时,经常就把异常信息贴到网上搜,然后把网上的解决方案执行一遍,有时work了,就大吉大利,可大多数时候,是不work的,原因很简单,也许对方的JDK版本跟你的不一样,也许你们俩只是报错信息相同,但是抛异常的原因不同,更大的可能,对方的解决方案本来就行不通。 
这就能理解为什么理解系统是定位Bug的前提了,如果你在定位一个JDK异常,那么至少你要掌握Java SE吧,如果你能掌握JVM的垃圾回收原理、类加载机制,自然更好;而如果你在定位一个支付系统为什么没有把账打到客户的账户,那么你得了解支付的流程吧。

总之,在开始调试之前,我们得弄清楚,调试是一门科学,而不是一门概率学,你需要理解整个系统,才能够进行调试。有以下方法可以帮助你理解这个系统:

  • 阅读手册:阅读需求设计文档、产品文档、使用手册等
  • 仔细阅读手册的每一个细节:说不定解决Bug的方案就在某个段落里
  • 掌握基础知识:最后你总是要看源码的,至少你要掌握相应的编程语言把
  • 了解工作流程:从整体的角度来观察,而不是做井底之蛙

可以说,理解系统的最终目的就是为了了解工作流程,了解了工作流程,你才能够从整体的角度来观察,不然就像井底之蛙,以为问题一定出在自己这个模块,而其实问题是在上游的某个模块里。这一点,和《程序员的思维修炼》中提到的,“专家从整体进行思考”的观点不谋而合。

检查插头

这里当然不是让你去问人家插头插了没,插头在这里是泛指一切让产品正常运行的基本要求。这些基本要求通常我们都认为理所当然是正常的,可事实有时并非如此。 
比如你的一个系统,需要在配置界面配置白名单,不然上游的请求就会被拒绝,那么当出现问题时,你应该首先去检查一下这个白名单配置了没,因为对方有可能是个新手。 
甚至当出现一些不可理喻的错误时,你要去软件的运行目录下,比如Tomcat的webapps目录,看看软件包是不是完整。 
当你替换新代码上去后,发现Bug依然存在时,不妨上去看看正在运行着的,是不是还是旧代码。

书中把这条规则放到了倒数第三条,我这里把它放到第二条,原因很简单,通常我们在发现Bug或者别人跟自己说这里有Bug时,心里都会慌,都会紧张,所以不妨先检查一下插头,缓解一下自己紧张的心情,同时也强迫你从整体的视角进行观察,不会局限在一个小模块里。

重现失败

这几乎是一个下意识的动作,就算你之前没读过这本书,在遇到Bug时,你也会去尝试重现它,原因很简单:

  • 重现失败让你可以观察失败发生时的上下文信息,进而找到失败的原因
  • 重现失败让你可以判断是否已经修复了问题

有些问题很好重现,而有些呢,却是要在特定的输入的情况下才会出现的。 
我们犯的绝大多数错误是在重现的方式上,作者对重现提出了两条原则:

  • 模拟失败发生的条件,但是不要模拟失败的机理,因为你认为的导致失败的机理很可能是错误的。举个例子,你认为是高并发导致的bug,于是你模拟了高并发的环境,问题重现了,然后你就说是高并发导致的,其实呢,只不过是高并发提高了问题发生的几率。
  • 只影响错误发生的频率,不影响错误发生的方式。其实高并发的环境可以用来提高错误发生的频率,只不过你要在问题重现时,要找到相应的日志信息,然后定位出问题发生的原因,而不是直接认为就是并发导致的。

不要猜,要观察

现在我们可以重现Bug了,直觉告诉我要在那个地方进行一个字符串编码的转换,且慢,在进行这个武断的尝试之前,先来看看《福尔摩斯》是怎么说的:

主观臆断的人,总是为了套用理论而扭曲事实,而不是用理论来解释事实。

猜测只是为了确定搜索的重点,但是在开始修复之前要观察确认你的猜测。所以在我们修改代码之前,还是看一下发生错误时的日志信息,还可以调试一下代码,在必要的时候打开源码深入研究一下,确定确实是字符串编码的问题,再去修改代码。

有人说,那我直接改代码,然后看结果不就知道是不是字符串编码的问题了。当然不是,要知道一个问题的产生可能是由多处地方的代码引起的,也许解决完这个字符串编码的问题,还需要解决另一个问题,才能把整个问题解决呢?如果你在修改前就没进行观察,就会认为这次修改毫无意义,这样整个调试过程就会陷入死局。

记住,调试是一门科学,任何不懂原理就进行的操作都是耍流氓

分而治之

系统通常都是由很多个模块组成的,这也就要求我们要检查很多个模块的日志才能够确定问题发生的原因。尤其是现在流行的微服务框架,一笔业务出现问题,你需要到很多个服务的机器上去找日志。

但是,如果你的业务执行是线性的,也就是说如果节点A执行失败,那么节点A之后的也都会执行失败,那么你就可以采用二分法的方式来定位了。要知道,在1到100里猜一个数字,最多也就需要7次。 
采用二分法的方式,你将逐步缩小嫌疑的范围,最终找到问题的根因。

当然,如果问题是由多个子问题引起的,那么记住,找到一个,消灭一个,这就是所谓的分而治之

一次只修改一个地方

通过观察,你认为你的修改方法会起作用,但如果实际上你修改完代码之后,并没有起到任何作用,那么请你马上改回去,以免这个修改引入了新的Bug

做好修改记录

把你调试过程中的操作和结果按顺序全部记录下来,方便你在发现做了那么多处修改依然没有解决问题时,进行回溯,反思自己的操作有没有不对的地方。

反向调试法

上面的调试规则,都是从问题出发,去寻找犯错的代码。但有时候反过来也许会更好。 
你可以找到最新一个可以正常运行的版本,然后对比现在这个版本和那个版本之间的差别,通过分析改动的代码,来分析是哪块代码导致的问题。

求助他人

有时候问题比较紧急,这时候不妨问一下专家,正如《程序员的思维修炼》中提到的,专家依靠直觉,他们往往会一针见血的给你指出问题的地方。 
如果你对系统有一定理解的情况下,可以上软件供应商的官网、谷歌、StackOverflow等网站寻找相关的资料。 
在求助的过程中,你只需要描述问题的症状,如果对方没有要求,那么不要给他讲自己的理论,以免将对方带入自己的思维定式。 
而在给他人描述问题的同时,你自己可能也会得到启发。

总结

以上就是我看完《调试九法》这本书之后总结的一套调试方法论,当然还是建议大家看一下原著,说不定会有新的收获。不过书中列举了大量的例子,多的让我感觉有些冗余,建议大家看的时候,先看每一章节的开头,和每章结尾的小总结,看完之后有不理解的,再去看每章中间的案例。

本书的例子虽然大多数都是关于工程技术的,但是里面的一些想法还是可以借鉴到生活中去。比如,夫妻吵架了,表面看上去是因为丈夫不愿意洗碗,但是如果你能从全局的角度去观察,你就知道,其实是因为丈夫情人节时没有给夫人买礼物。

最后再回过头来看这些规则,其实我们在工作和生活中时不时都会用到,但是我们之前一直没有一个系统的理论体系,在掌握了书中介绍的调试规则之后,我们在今后定位错误根源时,会更加井井有条,从容不迫。


原文转自:http://blog.csdn.net/hzy38324/article/details/78639329