调试的基本哲学
作者:Max Kanat-Alexander
发布日期:2017年7月17日
有些人调试时感到非常困难。主要是那些认为调试系统需要靠思考而不是观察的人。
让我举个例子说明。假设你有一个Web服务器,有5%的时间会静默地无法向用户提供页面。你对"为什么?“这个问题的反应是什么?
你会立即尝试想出答案吗?你会开始猜测吗?如果是这样,你就做错了。
对这个问题的正确答案是:“我不知道。”
这给我们带来了成功调试的第一步:
开始调试时,要意识到你并不知道答案。
认为自己已经知道答案很诱人。有时你猜对了。这种情况不常发生,但发生的频率足以让人误以为猜测答案是调试的好方法。然而大多数时候,你会花费数小时、数天或数周猜测答案并尝试不同的修复方法,结果除了让代码更复杂外一无所获。事实上,一些代码库中充满了对"bug"的"解决方案”,这些实际上只是猜测——而这些"解决方案"是代码库复杂性的重要来源。
实际上,顺便说一句,我要告诉你一个有趣的原则。通常,如果你很好地修复了一个bug,你实际上会让系统的某部分消失、变得更简单、设计更好等等,作为修复的一部分。我可能会在某个时候更详细地讨论这一点,但现在就先说到这里。很多时候,修复bug的最佳方法实际上是删除代码或简化系统。
但回到调试过程本身,你应该做什么?猜测是浪费时间,想象问题的原因是浪费时间——基本上,当你第一次遇到问题时,脑海中发生的大部分活动都是浪费时间。你唯一需要做的事情是:
- 记住正常系统的行为是什么样的
- 弄清楚你需要查看什么以获得更多数据
因为这给我们带来了调试的最重要原则:
调试是通过收集数据直到你理解问题原因来完成的。
你收集数据的方式几乎总是通过观察某些东西。对于不提供页面的Web服务器,你可能会查看它的日志。或者你可以尝试重现问题,以便在问题发生时观察服务器的情况。这就是为什么人们经常想要一个"重现案例"(一系列允许你重现确切问题的步骤)——这样他们就可以在bug发生时观察发生了什么。
有时你需要收集的第一条数据是bug实际上是什么。用户经常提交数据不足的bug报告。例如,假设用户提交了一个bug:“当我加载页面时,Web服务器没有返回任何东西。“这不是足够的信息。他们尝试加载了什么页面?“没有返回任何东西"是什么意思?只是一个空白页面吗?你可能会假设用户是这个意思,但很多时候你的假设是错误的。用户的编程或计算机技术经验越少,他们就越难在没有你询问的情况下具体表达发生了什么。在这些情况下,除非是紧急情况,我做的第一件事就是向用户发送具体请求以澄清他们的bug报告,并在他们回复前不做任何处理。在他们澄清事情之前,我完全不会去研究它。如果我在完全理解问题之前就去尝试解决问题,我可能会浪费时间研究系统中与问题完全无关的随机角落。更好的做法是在等待用户回复时把时间花在更有成效的事情上,然后当我有了完整的bug报告时,再去研究现在已理解的问题的原因。
关于这一点需要注意的是,不要因为用户提交了不完整的bug报告就对他们粗鲁或不友好。你对系统了解更多而他们对系统了解更少,这并不会使你成为一个优越的存在,可以从"比你聪明山"闪闪发光的顶峰上的高城堡中轻蔑地俯视所有用户。相反,以友善或直接的方式提问并获取信息。提交bug的人很少是故意愚蠢——他们只是不知道,帮助他们提供正确信息是你工作的一部分。如果人们经常不提供正确的信息,你甚至可以在bug提交页面上包含一个小问卷或表格,让他们填写正确的信息。关键是要帮助他们,这样他们才能帮助你,你才能轻松解决出现的问题。
一旦你澄清了bug,你必须去查看系统的各个部分。查看系统的哪些部分基于你对系统的了解。通常是日志、监控、错误消息、核心转储或系统的其他输出。如果你没有这些东西,你可能需要启动或发布一个提供信息的新版本系统,然后才能完全调试系统。虽然仅仅为了修复一个bug而这样做似乎很麻烦,但实际上,发布一个提供足够信息的新版本通常比在没有信息的情况下在系统中四处搜寻和猜测发生了什么要快。这也是支持快速、频繁发布的另一个好论点——这样你可以快速发布一个提供新调试信息的版本。有时你也可以将系统的新构建仅发布给遇到问题的用户,作为获取所需信息的快捷方式。
现在,记得我上面提到你必须记住正常系统的样子吗?这是因为调试还有另一个原则:
调试是通过将你拥有的数据与你所知道的正常系统数据应该是什么样子进行比较来完成的。
当你看到日志中的消息时,那是正常消息还是实际上是错误?也许日志说:“警告:所有用户数据都丢失了。“这看起来像是一个错误,但实际上你的Web服务器每次启动时都会打印这个。你必须知道正常工作的Web服务器会这样做。你要寻找正常系统不会显示的行为或输出。此外,你必须理解这些消息的含义。也许Web服务器可选地有一些你没有使用的用户数据库,这就是你收到该警告的原因——因为你希望所有"用户数据"都丢失。
最终你会找到正常系统不会做的事情。但当你看到这个时,你不应该立即假设你已经找到了问题的原因。例如,也许它记录了一条消息说:“错误:昆虫正在吃掉所有cookie。“你可以"修复"该行为的一种方法是删除日志消息。现在行为正常了,对吧?不,错了——实际的bug仍在发生。这是一个相当愚蠢的例子,但人们会做不那么愚蠢的版本,这些版本并不能修复bug。他们没有深入到问题的基本原因,而是用一些变通方法掩盖了bug,这些变通方法永远存在于代码库中,并给以后在该代码区域工作的每个人带来复杂性。甚至说"你会知道你找到了真正的原因,因为修复它就能修复bug"也不够充分。这很接近真相,但更准确的说法是:“当你确信修复它会使问题永远不会再出现时,你会知道你找到了一个真正的原因。“这不是一个绝对的陈述——bug的"修复程度"有一种尺度。一个bug可以被更多地修复或更少地修复,通常基于你希望解决方案"深入"到什么程度,以及你希望花多少时间在上面。通常当你找到了问题的合适原因并可以宣布bug已修复时,你会知道——这很明显。但我想警告你不要通过消除症状但不处理原因来掩盖bug。
当然,一旦你有了原因,你就修复它。如果你其他事情都做对了,这实际上是最简单的步骤。
所以基本上,这给了我们调试的四个主要步骤:
- 熟悉正常系统的行为
- 理解你还不知道问题的原因
- 查看数据直到你知道问题的原因
- 修复原因而不是症状
这听起来很简单,但我看到人们一直在违反这个公式。根据我的经验,大多数程序员在面对bug时,想要坐着思考或谈论可能的原因——这两种形式都是猜测。与可能拥有系统信息或建议在哪里寻找有助于调试的数据的人交谈是可以的。但是坐着集体猜测bug可能的原因并不比你自己坐着做这件事好多少,除非也许你可以与同事聊天,如果你喜欢他们的话这可能很好。但在那种情况下,你主要是在浪费一群人的时间,而不仅仅是浪费你自己的时间。
所以不要浪费人们的时间,也不要在代码库中创建不必要的复杂性。这种调试方法是有效的。它在每个代码库、每个系统上每次都有效。有时"数据收集"步骤相当困难,特别是对于无法重现的bug。但在最坏的情况下,你可以通过查看代码并尝试查看是否能发现bug,或者绘制系统行为图并查看是否能察觉到问题来收集数据。我只建议将其作为最后的手段,但如果你必须这样做,它仍然比猜测出了什么问题或假设你已经知道要好。
有时,仅仅通过查看正确的数据直到你知道,bug就解决了,这几乎是神奇的。自己试试看。实际上这可能很有趣。
-Max