如何调试任何东西

2025-06-10

如何调试任何东西

最初发布于HTTP Toolkit 博客

对于任何开发人员来说,调试都是一项重要技能。如果从广义上考虑调试,它可以说是最重要的技能:探索系统,解释其行为,并找出改变它的方法。

尽管如此,我们大多数人都不擅长这件事。我们不会系统地完成一个流程。相反,我们胡乱猜测,随意记录日志,盲目地改变事情,直到问题消失。

幸运的是,我们可以改进!我一直在与众多优秀的开发人员交流,了解他们提出的顶级调试建议,并根据他们多年的专业知识以及我自己的软件开发经验,整理出一本超级调试指南。

那么,不用多说:我该如何调试?

缩小问题范围

在大多数调试情况下,您首先会发现期望与现实不匹配。

  • 用户点击了我们网站的链接。我以为它会加载,但一直没有加载。
  • 服务器A向服务器B发送了一个请求,我预计它能在100ms内收到结果,但是服务器B花了20秒。
  • 顾客输入的信用卡信息正确无误。我以为他们会被扣款,结果根本没被扣款。

为了能够修复问题,我们需要充分理解问题,以便找到清晰且可接受的修复方案。需要注意的是,“充分”与上下文相关:有时“服务器 B 坏了,我们重置一下就可以了”,有时“服务器 B 的磁盘损坏是由于我们的 IO 使用模式造成的”,这仅仅是个开始。

让我们首先看看如何隔离问题的位置,然后再探讨如何解释它(并修复它)。

缩小可能出错的范围:这里不行,那么之前 x 点的所有东西都正确吗?是吗?那么从这里到 x 点之间的 y 点呢?—— Nicole Williams

您的第一步是将调试集中到足够小的区域,以便您可以开始考虑修复。

实际上,你正在反复运行测试,以区分系统中运行正常的部分和运行异常的部分。这是一个循序渐进的过程,我强烈建议你在测试过程中仔细记录,以便随时掌握进展。

有时,区分界限很清晰。如果你有一个函数,它获得了正确的输入,但却产生了错误的输出,那么下一步就是检查该函数内部各个点的值,找出出错的地方。

有时候,情况不太清楚。以下是一些建议:

如果系统处于故障状态

  • 到底哪个州“崩溃”了?
    • 如果您的数据存储由于数据不一致而中断,那么哪些不一致的部分是不正确的?
    • 如果您的服务器运行但停止响应:所有端点都会停止响应,还是只有少数端点停止响应?
  • 它最后一次处于良好状态是什么时候?
  • 究竟什么事件使它从好状态变成坏状态?
  • 其状态的哪一部分首先崩溃?
    • 对于非平凡状态,解开其中的依赖关系并了解它们如何相互影响很有用。
    • 这有助于确定变量 A 和 B 是否都设置错误,或者变量 A 设置错误而变量 B 设置正确,但基于错误的数据。
    • 适用于变量等低级状态,也适用于“服务器是否响应”等高级状态。如果服务器 A 和 B 有时同时宕机,它们是因为相同的原因崩溃,还是其中一个崩溃会导致另一个崩溃?

如果系统由通信部分组成

  • 哪一个人首先犯错误?
  • 例如,如果您的应用无法从服务器加载数据:
    • 它是否向服务器请求正确的数据?
    • 服务器是否返回正确的数据?
  • 如果您使用 HTTP,HTTP Toolkit非常适合执行此操作!
  • 如果您能回答这些问题,您就会立即知道服务器或应用程序是否有故障(假设只有一个坏了......)

如果问题间歇出现

  • 尽一切可能将其缩小到特定且可持续重现的错误。
  • 它出现的时间之间有什么共同因素吗?
    • 用户的操作系统/浏览器、一天中的时间、数据大小和系统负载都是很好的候选因素。
  • 尝试确定它是否是由竞争条件或特定的罕见输入引起的。
    • 如果您可以采取一组安全可靠的操作,快速运行其中的很多操作,然后它们不断失败,则可能是竞争条件。
    • 如果出现任何与系统负载无关的共同因素,则可能是其他因素。
  • 一旦有了明确的竞争条件,您就可以删除并缩小并行操作,直到找到竞争的原因。
  • 一旦您获得导致失败的特定输入,您就可以重现该问题并调查原因。

您不必只搜索代码来寻找原因!

有时您可以在一组服务器或用户内进行搜索,以将错误出现的范围缩小到执行错误操作的特定机器,这可以提供大量的线索。

有时,及时缩小问题范围也很有用,找到这种行为发生变化的时刻(如果您确信它在过去有效),这样您就可以看到系统的哪些部分在相似的时间发生了变化,并更仔细地调查这些变化。

尽可能多地隔离变量,并逐一测试,直到找出问题所在
- Stacy Caprio,Accelerated Growth Marketing


如果某个数据为空或缺失,它应该来自哪里?顺着流程往回追溯,不要只盯着错误发生的地方
 ——Nicole Williams

在许多此类情况下,存在多个维度:问题发生在哪里,以及是什么输入导致了问题。

一旦找到了出错的地方,这些输入就变得至关重要。你需要通过同样的流程来缩小问题的范围,确定是哪部分输入导致了问题。

这里的“输入”非常笼统:可能是你收到的 HTTP 请求、正在处理的数据库记录、函数参数、当前时间或网络延迟。任何影响应用程序的变量都是如此。

这里的过程大致相同,但对于复杂的输入,一个好的 diff 值会非常有价值。找到一个有效的输入,找到一个错误的输入,然后比较它们。问题就出在这组差异中!你可以用正确的输入替换错误输入的部分内容(反之亦然),直到失败,从而确定触发问题所需的最少错误数据。

对于简单的输入来说,这比较容易,但仍然需要进行一些比较。例如,如果您的 UI 在尝试显示某些价格时抛出错误:这是针对哪些价格发生的?是输入过大、意外的负值输入,还是某些输入导致后续计算错误?

二分查找所有事物;如果你能一步排除一半的可能性,那就这么做吧——汤姆·哈德森

一旦你对问题的成因有了一系列可能性,某种程度上来说,就应该在两者之间进行测试。你对问题所在位置的直觉可能是错误的。考虑到这一点,中间方法比其他任何方法都能让你更快、更可靠地找到正确答案。

这对于从调试单个损坏的函数(检查中间值的状态)到整个代码库(非常值得学习如何git bisect)的所有事情都是可靠的建议。

获得可见性

高级流程都很好,但有时您无法清楚地看到系统内部的损坏部分,也无法进行更深入的挖掘。

生产服务器就是一个典型的例子,此外还存在一些问题,这些问题只影响特定客户的设备,而不会影响到您的设备,或者您无法可靠地复现的间歇性问题。您需要了解系统在每个步骤中执行的操作,以便缩小问题范围。

第一个也是最好的选择是,在你能够看到的环境(例如本地)中以某种方式重现该问题。收集所有你能收集到的信息,并尝试重现它。如果你能做到,那就太好了!你赢了。这绝对应该是你的第一选择。

但通常你无法做到这一点,要么是因为在其他环境中很难复现,要么是因为即使在你选择的环境中也看不到细节。幸运的是,有很多工具可以帮助你获得这种可见性:

日志记录和可观察性工具

如果我手头有有问题的函数,我有一个像 Chrome DevTools 这样的好用的记录器,而且我的构建不需要很长时间,那么记录变量内容就是我快速而粗糙的第一步。
- Aaron Yoshitake,来自Pick a Kit


每当我尝试一门新的编程语言时,错误日志记录都是我首先在谷歌上搜索的内容。

虽然大多数平台都已经提供了全面的调试和基准测试工具包,但简单的错误日志记录无论在本地测试还是生产环境中都能发挥奇效。

根据应用程序的不同,日志可以生成在文件中,传递给第三方,甚至存储在数据库中。一个简单的日志框架可以方便地导航用户流程,目前每种编程语言都开箱即用地支持该框架。——
Mario Peshev

每种语言和工具都有内置的日志记录工具。它们易于访问、简单灵活,可以快速帮助您缩小问题范围,以便更好地理解问题,或者至少更接近于重现问题。

然而,日志记录可能是一种迟钝的工具:难以大规模地有效执行,或难以深入了解正在发生的事情,并且通常无法在您需要的地方进行。

除此之外,自动化日志记录工具也值得一试。例如,像Sentry这样的错误监控服务会自动记录错误,以及系统的堆栈跟踪和上下文,以及错误发生前不久的有趣事件的详细信息,从 HTTP 请求到控制台消息。

与此同时,像LogRocket这样的工具可以让你重放用户会话,查看他们看到的内容,并了解你自己无法准确复现的问题。这很强大,但记录用户会话也可能带来隐私问题。

最后,还有更多重型可观察性工具可用,例如HoneycombNew Relic

这些工具需要进行大量的设置,但可以为您提供更多数据,并增强探索能力:从检查由给定传入 HTTP 请求触发的所有 SQL 查询,到探索每周二各服务器之间的延迟精确分布。它们会自动收集一些数据,但也需要您在应用程序中的相关位置记录数据点,因此需要一些工作。但是,如果您运行的是大型系统,并且经常在生产环境中调试问题,那么这些工具绝对值得投资。

对于这类工具,最好事先设置好!不过,在调查问题时设置它们通常仍然很有价值,所以如果您还没有设置,也不要低估它们的作用。

调试器

你的语言应该有合适的调试工具,让你可以逐行检查你的系统。这主要适用于本地环境,但一些技巧,例如向客户发送应用程序的调试版本来重现问题,在其他情况下也会有所帮助。

无论哪种方式,一旦您确定问题出在哪里,并且想要仔细检查以找出确切的细节,调试器就会变得非常有用。

熟悉适用于您环境的标准调试工具至关重要。不要只学习基础知识;许多工具不仅能添加断点和检查变量,还包含更强大的功能,可以帮助您更快速有效地找到问题:

  • 条件断点,仅当满足某些条件时才会在代码中的某个点暂停执行。
  • 在运行时操纵状态甚至代码本身的能力,以重现问题和测试修复。
  • 时间旅行,让您充分探索流程的执行流程。

不要回避对平台本身进行调试。即使错误是由你的代码引起的,有时单步执行你正在使用的内置函数也能让你发现它们是如何被错误使用的。

交互检查员

通常,问题会出现在系统之间的交互中,并且能够直接查看和与支持这些交互的通信进行交互可以快速解决这些问题。

HTTP Toolkit非常适合这种情况。HTTP Toolkit 可以轻松拦截和查看来自客户端、后端服务器之间或发送到 API 的 HTTP 或 HTTPS 流量,然后还可以实时编辑这些流量,以便您可以测试极端情况并缩小导致问题的输入范围。

或者,如果您使用的是其他协议,或者需要在原始 TCP 级别进行检查,Wireshark可能是个好帮手。Wireshark 可以捕获和查看原始数据包数据,并提供一些工具来解释和过滤数据包,让您能够理解各种协议,尽管这意味着它的学习曲线比较陡峭。

当然,交互通常发生在联网系统之间,但您也可以检查其他交互。例如, StraceDTruss允许您检查和修改进程与内核之间的交互。它们跟踪系统调用,包括每个单独的文件和套接字操作以及许多其他操作。这可以帮助您了解底层操作系统问题,准确查看程序正在尝试使用哪些文件或套接字,或者探索非常复杂的性能或死锁问题。

交互式代码探索

对于调试棘手的算法代码,探索数据及其处理可能非常有效。

有一些工具可以让你以交互方式执行此操作,将代码转换为概念上更像电子表格的东西:你可以同时看到每个中间值,并更改一个值或计算以查看它如何影响其他所有内容。

Quokka.js为 JavaScript 和 TypeScript 提供了类似的功能,可以作为各种编辑器的插件。Light Table是一款功能齐全的 IDE,专为此工作流程而设计,最初是为 Clojure 设计的,但现在也提供其他语言的插件。

解释问题

希望此时,通过利用您对系统的可见性逐步缩小问题范围后,您可以很好地了解问题出在哪里以及如何出问题。

下一步是找出原因。

很多情况下,一旦你缩小了系统或状态错误的具体范围,错误就会一目了然。例如,你把一个值做成了加法而不是乘法,或者你在使用输入数据之前忘记验证它是否正确。但在其他情况下,错误并非显而易见,解释清楚问题以便修复它本身就是一个巨大的挑战。

检查你的假设

验证所有假设。
该函数真的返回了你认为的结果吗?仔细
阅读文档 检查拼写、大小写和标点符号。 认真阅读错误信息,而不是粗略浏览。—— 汤姆·哈德森


作为人类,我们会根据事物的运作方式做出假设并建立抽象概念,以避免不断思考每一个可能的细节。

有时这些是错误的。

这很容易导致严重的错误,即使是最简单的假设也难以解决。如果一个问题看起来无法解释,就好像计算机只是做了“错误”的事情,那么你几乎肯定会遇到这种情况,而且你在某个地方做出了错误的假设。

  • 检查是否调用了正确的函数,或者是否与正确的服务器进行通信。
  • 检查您正在运行的代码版本。
  • 检查您在其他地方检查过的值是否稍后发生变化。
  • 检查您是否确实返回了您的值,并正确等待异步事件。
  • 检查日志中您尝试解释的错误是否是第一个意外错误,而不仅仅是先前问题的症状。

寻找答案

在互联网上搜索令人困惑的行为的解释是调试器的一个古老传统。

不过,你经常会遇到麻烦,对于复杂的问题,解决起来并不像听起来那么容易。如果仅仅根据问题描述进行搜索不起作用,你可以尝试以下方法:

  • 寻找问题的潜在答案,而不仅仅是问题本身。与其说是“对 X 的 fetch 请求失败”,不如试试“X 不支持 gzip 压缩请求”或“fetch 无法发送 JSON”。
  • 搜索您看到的任何错误消息的片段或任何其他相关日志,即使它不是问题本身。
  • 直接搜索 StackOverflow,通过标签过滤问题以优化您的结果。
  • 在问题跟踪器中搜索所涉及的工具,以查找与您的问题相关的错误报告。
  • 搜索可能包含类似代码的工作项目示例,将您的方法与他们的方法进行比较,并仔细查看它们不同的地方。

检查常见的嫌疑人

汤姆·哈德森 (Tom Hudson)列出了需要注意的常见事项:

导致怪异行为的常见原因:

  • 没有磁盘空间(或没有可用的 inode!)
  • 网络问题(尤其是 DNS)
  • 系统时间设置错误(这给我带来了一些非常奇怪的问题)
  • 防病毒干扰
  • 文件名大小写(例如,Linux 区分大小写,但 Mac 或 Windows 不区分大小写)

其中任何一个都可能在其他地方引起奇怪的错误,这些错误看似无关,而且极难追踪!

收集你自己的这类问题列表很有用。有些常见问题可能非常普遍,比如这些,但也有一些特定于你的平台或系统的特定问题。把清单放在某处,记下你需要调试的每个问题的原因,这样你就能快速建立一个将来需要注意的事项库。

谈论它

向其他人解释这个问题。 - Veronika Milic

还是卡住了?如果其他方法都失败了,有时候小黄鸭调试往往是最好的解决方案。可以和同事聊聊,在 Stack Overflow 上提问,或者在 Twitter 上发帖。寻求帮助很重要,而且有很多人愿意探讨你的问题。

试着解释你所了解的一切,包括当前正在发生的事情以及难以解释的部分。一半情况下,你最终会自己解决问题,而另一半情况下,至少会有其他人尝试提供帮助!

修复它

希望你现在可以解释一下你的系统哪个部分出了问题,以及为什么会这样。恐怕最后一步就交给你了:修复它。幸运的是,如果你了解代码哪里出了问题以及为什么出错,这通常是一个相当清晰的过程(尽管不一定很快)。

一旦你解决了问题,请帮自己一个忙,记住:

  • 写完修复方案后,务必彻底重新测试,而不是根据你对问题的理解假设它有效。不这样做会非常痛苦,浪费大量时间,但这种情况却非常常见。
  • 写下你是如何调试问题的,以及你对潜在问题及其发生原因的最佳理解。这至少能帮助你将来调试类似的问题,并且在某些重要情况下,这能凸显出你的修复方案实际上与你的解释不符,所以两者之一就是错误的。

祝你好运!

还卡住吗?对本文有任何疑问或意见吗?您有什么好的调试技巧吗?欢迎通过 Twitter联系我们或在下方评论。

最初发布于HTTP Toolkit 博客

鏂囩珷鏉ユ簮锛�https://dev.to/pimterry/how-to-debug-anything-11o3
PREV
一周内将副项目发展到 10 万独立访客
NEXT
介绍:Pika CDN + Deno