您如何重现偶尔出现的错误?

Posted

技术标签:

【中文标题】您如何重现偶尔出现的错误?【英文标题】:How do you reproduce bugs that occur sporadically? 【发布时间】:2011-01-31 17:38:33 【问题描述】:

我们的应用程序中存在一个并非每次都会发生的错误,因此我们不知道它的“逻辑”。今天我什至没有把它复制 100 次。

免责声明:这个错误存在并且我已经看到了。这不是pebkac 或类似的东西。

重现此类错误的常见提示是什么?

【问题讨论】:

祷告? span> 重新崩溃:groups.csail.mit.edu/pag/reCrash 只需在错误跟踪器中将其标记为“无法重现”并忘记它。 ^_^ 有时最简单的方法是停止寻找它。去在应用程序中做点别的事情,你明天可能会遇到问题...... 告诉你的老板它已经准备好部署了。他们会在三秒钟内找到它。 【参考方案1】:

成对分析问题并结对阅读代码。记下您知道是真的问题,并尝试断言哪些逻辑前提条件必须为真。像 CSI 一样追踪证据。

大多数人本能地说“添加更多日志记录”,这可能是一个解决方案。但是对于很多问题,这只会让事情变得更糟,因为日志记录可以充分改变时间依赖性,从而使问题或多或少变得频繁。将频率从千分之一更改为百万分之一不会让您更接近问题的真正根源。

因此,如果您的逻辑推理不能解决问题,它可能会为您提供一些细节,您可以在代码中使用日志记录或断言进行调查。

【讨论】:

“将频率从千分之一更改为百万分之一不会让您更接近问题的真正根源。”但是将其更改为 1 in 1 会。如果您可能遇到会增加错误频率的更改,那么您就更接近于找到并修复它。 我不相信偶然发现解决方案,经验告诉我,仔细分析和排除在这些情况下几乎总是赢家。我认为一些背景变量,如测试覆盖率、编程语言特性和对运行系统的访问,会影响你用来解决此类问题的措施。如果您在解决问题时无法使用数据库,那么您几乎必须依赖日志或其他报告,对吧?【参考方案2】:

这个问题没有一般的好答案,但这是我发现的:

    这种事情需要天赋。并非所有开发人员都最适合它,即使他们是其他领域的超级明星。所以了解你的团队,他们有这方面的天赋,并希望你能给他们足够的糖果,让他们对帮助你感到兴奋,即使这不是他们的领域。

    逆向工作,将其视为科学研究。从bug开始,你看到的是错误的。对可能导致它的原因提出假设(这是创造性/想象力的部分,不是每个人都有天赋的艺术)——这有助于了解代码的工作原理。对于这些假设中的每一个(最好按您认为最有可能的排序 - 在这里再次纯粹的直觉),开发一个试图消除它作为原因的测试,并测试假设。任何给定的未能满足预测并不意味着假设是错误的。测试假设,直到确认它是错误的(尽管您可能希望先转移到另一个假设的可能性较小,但在您确定失败之前不要忽视这个假设)。

    在此过程中尽可能多地收集数据。广泛的日志记录和任何其他适用的。不要因为缺少数据而打消假设,而是要弥补数据的不足。很多时候,正确假设的灵感来自于对数据的检查。注意到堆栈跟踪中的某些问题、日志中的奇怪问题、数据库中应该存在的某些缺失等等。

    仔细检查每个假设。很多次我看到一个问题没有很快得到解决,因为一些通用的方法调用没有被进一步调查,所以这个问题被认为是不适用的。 “哦,那个,应该很简单吧。” (见第 1 点)。

如果你用尽了假设,那通常是由于对系统的了解不足(即使你自己编写了每一行代码也是如此),你需要运行和审查代码并获得对系统的更多了解系统想出一个新的想法。

当然,以上都不能保证任何事情,但这是我发现的始终如一的方法。

【讨论】:

#2 实际上是非常好的建议......如果你还记得的话。停下来真正地​​思考几分钟可以省去很多悲伤,但当时总是有太大的压力来敲击键盘。【参考方案3】:

添加某种日志记录或跟踪。例如,记录用户在导致错误之前提交的最后 X 个操作(仅当您可以设置条件以匹配错误时)。

【讨论】:

如果问题不是 Heisenbug,或者不是复制步骤从构建到构建的变化。您可以使用像 UserMetrix 这样的服务,该服务会自动对遇到相同错误的所有用户的操作进行数据挖掘,并生成最可能重现错误的步骤列表,如下例所示:usermetrix.com/log_messages/1384444/error【参考方案4】:

程序员无法重复用户体验过的崩溃是很常见的,因为您在使用应用程序时已经养成了一定的工作流程和习惯,显然可以解决该错误。

在这个 1/100 的频率下,我想说首先要做的是处理异常并在任何地方记录任何内容,否则您可能会再花一周的时间来寻找这个错误。 还要列出项目中潜在敏感的发音和功能的优先级列表。例如 : 1 - 多线程 2 - 野指针/松散数组 3 - 依赖输入设备 等等。 这将帮助您按照其他发帖人的建议细分可以暴力破解直到再次破解的区域。

【讨论】:

【参考方案5】:

由于这与语言无关,我将提及一些调试公理。

计算机所做的一切都不是随机的。 “随机出现”表示尚未发现的模式。调试从隔离模式开始。改变各个元素并评估导致错误行为发生变化的原因。

不同的用户,同一台计算机? 同一个用户,不同的电脑? 发生是否具有很强的周期性?重启会改变周期吗?

仅供参考-我曾经看到一个人经历过的错误。我的字面意思是人,而不是用户帐户。用户 A 永远不会在他们的系统上看到问题,用户 B 会坐在那个工作站上,以用户 A 的身份登录,并且可以立即重现该错误。应用程序应该没有可以想象的方式来知道椅子上的身体之间的区别。然而——

用户以不同的方式使用该应用程序。用户 A 习惯性地使用热键来调用操作,用户 B 使用屏幕控制。用户行为的差异会在几个操作之后级联成可见的错误。

应该调查影响错误行为的任何差异,即使它没有意义。

【讨论】:

我见过类似的问题 - 一个不耐烦的用户不仅点击,而且(如果没记错的话)三次点击按钮。当时的系统正在缓存点击,所以如果后续屏幕在同一位置有一个按钮......你明白了。我只是通过实际与用户坐在一起并看着她使用应用程序来解决这个问题。 没有什么是随机的,但我想很多事情都可能是伪随机的。例如异步请求。可能会先收到 1000 次第一个请求,但有 1000 次可能会迟到。是什么原因?由于系统另一部分的一些“随机”滞后?【参考方案6】:

您的应用程序很有可能是 MTWIDNTBMT(当不需要多线程时多线程),或者可能只是多线程(礼貌)。在多线程应用程序中重现零星错误的一个好方法是在 (C#) 周围散布这样的代码:

Random rnd = new Random();
System.Threading.Thread.Sleep(rnd.Next(2000));

和/或这个:

for (int i = 0; i < 4000000000; i++)

    // tight loop

模拟线程在与平时不同的时间完成其任务或长时间占用处理器。

这些年来,我继承了许多有缺陷的多线程应用程序,而像上述示例这样的代码通常会使零星错误发生得更加频繁。

【讨论】:

【参考方案7】:

添加详细日志记录。需要多次(有时是几十次)迭代才能添加足够的日志记录来理解场景。 现在的问题是,如果问题是竞争条件,如果它不能可靠地重现,那么日志记录可以改变时间并且问题将停止发生。在这种情况下,不要将日志记录到文件中,而是在内存中保留日志的旋转缓冲区,并仅在检测到问题发生时将其转储到磁盘上。

编辑:再多思考一下:如果这是一个 gui 应用程序,则使用 qa 自动化工具运行测试,该工具允许您重播宏。如果这是一个服务型应用程序,请尝试至少猜测正在发生的事情,然后以编程方式创建“怪异”的使用模式,以执行您怀疑的代码。创建比平时更高的负载等。

【讨论】:

嗯,我喜欢旋转日志缓冲区的想法。不一定在所有情况下都易于实施,但仍然是一个好主意【参考方案8】:

什么开发环境? 对于 C++,您最好的选择可能是 VMWare Workstation 记录/回放,请参阅: http://stackframe.blogspot.com/2007/04/workstation-60-and-death-of.html

其他建议包括检查堆栈跟踪和仔细的代码概述......真的没有灵丹妙药:)

【讨论】:

【参考方案9】:

尝试在您的应用中添加代码,以便在错误发生后自动跟踪错误(甚至通过邮件/短信提醒您)

尽可能记录任何内容,以便在发生这种情况时捕获正确的系统状态。

另一件事 - 尝试应用自动化测试,它可以比基于人工的测试覆盖更多领域。这是一个长远的目标,但总的来说是一个很好的做法。

【讨论】:

【参考方案10】:

以上所有内容,加上一些半随机的蛮力软机器人,并通过代码分散大量断言/验证(c/c++,可能在其他语言中类似)

【讨论】:

【参考方案11】:

大量的日志记录和仔细的代码审查是您唯一的选择。

如果应用程序已部署并且您无法调整日志记录,这些可能会特别痛苦。到那时,你唯一的选择就是用细齿梳理代码并尝试推理程序如何进入不良状态(救援的科学方法!)

【讨论】:

【参考方案12】:

这类错误通常与损坏的内存有关,因此它们可能不会经常出现。您应该尝试使用某种内存分析器(例如 valgrind)运行您的软件,以查看是否出现问题。

【讨论】:

【参考方案13】:

假设我从一个生产应用程序开始。

    我通常会在我认为出现错误的区域周围添加调试日志记录。我设置了日志记录语句,让我深入了解应用程序的状态。然后我打开调试日志级别并要求用户/操作员通知我下一个错误发生的时间。然后我分析日志以查看它对应用程序的状态提供了哪些提示,以及是否可以更好地了解可能出现的问题。

    我重复第 1 步,直到我知道可以在调试器中从哪里开始调试代码

    有时代码运行的迭代次数是关键,但有时它可能是组件与外部系统(数据库、特定用户机器、操作系统等)的交互。花一些时间来设置一个尽可能匹配生产环境的调试环境。虚拟机技术是解决这个问题的好工具。

    接下来我通过调试器继续。这可能包括创建某种测试工具,将代码/组件置于我从日志中观察到的状态。了解如何设置条件断点可以节省大量时间,因此请熟悉调试器中的条件断点和其他功能。

    调试,调试,调试。如果几个小时后你无处可去,那就休息一下,做一些不相关的事情。带着全新的思想和观点回来。

    如果您现在还没有取得任何进展,请返回第 1 步并进行另一次迭代。

    对于真正棘手的问题,您可能不得不求助于在出现错误的系统上安装调试器。结合第 4 步中的测试工具,通常可以解决真正令人费解的问题。

【讨论】:

【参考方案14】:

单元测试。在应用程序中测试一个错误通常是可怕的,因为有太多的噪音,太多的可变因素。一般来说,(干草)堆越大,查明问题就越困难。创造性地扩展您的单元测试框架以包含边缘案例可以节省数小时甚至数天的筛选时间

话虽如此,没有灵丹妙药。我感觉到你的痛苦。

【讨论】:

【参考方案15】:

在与此错误相关的方法中添加前置条件和后置条件检查。

你可以看看Design by contract

【讨论】:

是的,我在 C++ 中以这种方式使用 assert() 取得了成功。检查一切【参考方案16】:

除了耐心之外,你还需要安静的祈祷和诅咒:

记录用户操作的良好机制 当用户执行某些操作(应用程序、数据库中的状态等)时收集数据状态的良好机制 检查服务器环境(例如在特定时间运行的杀毒软件等)并记录错误发生的次数,看看是否可以发现任何趋势 更多的祈祷和诅咒......

HTH。

【讨论】:

【参考方案17】:

假设您使用的是 Windows,并且您的“错误”是非托管代码 (C/C++) 中的崩溃或某种损坏,那么请查看 Microsoft 的 Application Verifier。该工具有许多停止点,可以启用以在运行时验证事物。如果您对发生错误的场景有所了解,请尝试在运行 AppVerifer 的情况下运行该场景(或该场景的压力版本)。确保在 AppVerifier 中打开 pageheap,或者考虑使用 /RTCcsu 开关编译代码(有关详细信息,请参阅 http://msdn.microsoft.com/en-us/library/8wtf2dfz.aspx)。

【讨论】:

【参考方案18】:

“Heisenbugs”诊断需要高超的技巧,如果你需要这里的人帮助你必须更详细地描述这个,耐心地听各种测试和检查,在这里报告结果,然后迭代直到你解决它(或决定它在资源方面太昂贵)。

您可能需要告诉我们您的实际情况、语言、数据库、操作系统、工作量估计、过去发生的时间以及无数其他事情,列出您已经做过的测试,它们是如何进行的,并准备好做更多事情并分享结果。

这也不能保证我们可以集体找到它……

【讨论】:

【参考方案19】:

我建议写下用户一直在做的所有事情。如果你有 10 个这样的错误报告,你可以尝试找到将它们联系起来的东西。

【讨论】:

【参考方案20】:

仔细阅读堆栈跟踪并尝试猜测可能会发生什么; 然后尝试跟踪\记录可能导致问题的每一行代码。

将注意力集中在处理资源上;我发现的许多鬼鬼祟祟的零星错误都与 close\dispose 事物有关 :)。

【讨论】:

【参考方案21】:

对于 .NET 项目,您可以使用 Elmah(错误记录模块和处理程序)来监控您的应用程序是否存在未捕获的异常,它安装非常简单,并提供了一个非常好的界面来浏览未知错误

http://code.google.com/p/elmah/

这让我今天避免了在注册过程中发现一个非常随机的错误

除此之外,我只能建议尝试从您的用户那里获取尽可能多的信息,并对项目工作流程有透彻的了解

他们大多在晚上出来...... 主要是

【讨论】:

【参考方案22】:

当我们有一个讨厌的错误要追查时,我的合作团队已经邀请用户记录他们在我们的应用中使用 CamStudio 所花费的时间。它易于安装和使用,并且使重现那些烦人的错误变得更加容易,因为您可以看到用户在做什么。它也与您使用的语言无关,因为它只是记录 Windows 桌面。

不过,这条路线似乎只有在您开发企业应用并与用户保持良好关系的情况下才可行。

【讨论】:

【参考方案23】:

这会有所不同(如你所说),但一些方便的东西可以是

出现问题时立即进入调试器并转储所有线程(或等效的,例如立即转储内核或其他)。 在日志记录打开的情况下运行,但在其他方面完全处于发布/生产模式。 (这在一些随机环境中是可能的,比如 c 和 rails 但不是很多其他环境。) 做一些事情使机器上的边缘条件变得更糟...强制低内存/高负载/更多线程/服务更多请求 确保您真正听到了遇到问题的用户实际所说的话。确保他们实际上在解释相关细节。这似乎是让该领域的人们大为震惊的一个。试图重现错误的问题很无聊。 习惯于阅读通过优化编译器生成的程序集。这似乎有时会阻止人们,它并不适用于所有语言/平台,但它可以提供帮助 准备好接受这是您(开发人员)的错。不要陷入坚持代码完美的陷阱。 有时您需要实际跟踪问题所在机器上的问题。

【讨论】:

【参考方案24】:

@p.marino - 没有足够的代表发表评论 =/

tl;dr - 由于一天中的时间构建失败

你提到了一天中的时间,这引起了我的注意。曾经有一个错误是有人在晚上工作得晚点,在他们离开之前尝试构建和提交并不断失败。他们最终放弃了回家。当他们在第二天早上发现它构建良好时,他们承诺(可能应该更可疑=])并且构建对每个人都有效。一两个星期后,有人迟到了,并且发生了意外的构建失败。结果发现代码中存在一个错误,导致晚上 7 点休息后进行任何构建>.>

我们还在今年 1 月在项目的一个很少使用的角落发现了一个错误,该错误导致不同模式之间的编组问题,因为我们没有考虑基于 0 和 1 个月的不同日历。因此,如果没有人弄乱项目的那一部分,我们可能要到 1 月才能发现这个错误。 2011

这些比线程问题更容易解决,但我认为仍然很有趣。

【讨论】:

:) 实际上我暗示的是与时区相关的错误,系统时间设置与“实时”时间不同,夏令时等 - 但是,有时这是完全不可想象的...... 【参考方案25】:

聘请一些测试人员!

【讨论】:

【参考方案26】:

这适用于非常奇怪的海森虫。 (我还建议获取 Dave Argans 的“调试”副本,这些想法部分源自他的想法!)

(0) 使用 Memtest86 之类的工具检查系统的内存!

整个系统都出现了问题,所以制作一个测试夹具来锻炼整个系统。 假设它是一个带有 GUI 的服务器端的东西,你用一个 GUI 测试框架运行整个东西,做必要的输入来引发问题。

它不会 100% 失败,所以你必须让它更频繁地失败。

首先将系统切成两半(二进制切碎) 更糟糕的情况是,您必须一次删除一个子系统。 如果它们不能被注释掉,就把它们删掉。

看看它是否仍然失败。它是否经常失败?

保持正确的测试记录,一次只改变一个变量!

最坏的情况是您使用夹具并测试数周以获得有意义的统计数据。这很难;但请记住,夹具正在发挥作用。

我没有线程,只有一个进程,而且我不与硬件对话

如果系统没有线程,没有通信进程,也没有联系硬件;这很棘手; heisenbugs 通常是同步的,但是在 no-thread no processes 的情况下,它更有可能是未初始化的数据,或者是释放后使用的数据,无论是在堆上还是在堆栈上。尝试使用像 valgrind 这样的检查器。

对于线程/多进程问题:

尝试在不同数量的 CPU 上运行它。如果它在 1 上运行,请尝试在 4 上运行!尝试将 4 计算机系统强制为 1。 它主要是确保事情一次发生。

如果有线程或通信进程,这可以消除错误。

如果这没有帮助,但您怀疑它是同步或线程,请尝试更改操作系统时间片大小。 在您的操作系统供应商允许的范围内让它变得更好! 有时这会导致几乎每次都发生竞争条件!

相反,尝试在时间片上放慢速度。

然后,您设置测试夹具运行,并在各处安装调试器,并等待测试夹具因故障而停止。

如果一切都失败了,请将硬件放入冰箱并在那里运行。一切的时间都会改变。

【讨论】:

【参考方案27】:

调试既困难又耗时,尤其是在您无法确定地重现问题的情况下。我给你的建议是找出确定性地重现它的步骤(不仅仅是有时)。

在过去的几年中,故障再现领域的研究已经很多,并且仍然非常活跃。 Record&Replay 技术一直是(迄今为止)大多数研究人员的研究方向。这是你需要做的:

1) 分析源代码并确定应用程序中非确定性的来源,即哪些方面可能使您的应用程序通过不同的执行路径(例如用户输入、操作系统信号)

2) 下次执行应用程序时记录它们

3) 当您的应用程序再次失败时,您的日志中有重现失败的步骤。

如果您的日志仍然没有重现故障,那么您正在处理并发错误。在这种情况下,您应该看看您的应用程序如何访问共享变量。不要尝试记录对共享变量的访问,因为您会记录太多数据,从而导致严重的减速和大型日志。不幸的是,我不能说太多可以帮助您重现并发错误,因为在这个主题上的研究还有很长的路要走。我能做的最好的就是提供一个参考(到目前为止)在并发错误的确定性重放这一主题中的最新进展:

http://www.gsd.inesc-id.pt/~nmachado/software/Symbiosis_Tutorial.html

最好的问候

【讨论】:

【参考方案28】:

使用增强的崩溃报告器。在 Delphi 环境中,我们有 EurekaLog 和 MadExcept。其他工具存在于其他环境中。或者您可以诊断核心转储。您正在寻找堆栈跟踪,它将向您显示它在哪里爆炸,它是如何到达那里的,内存中有什么等等。如果它是一个用户交互的东西,拥有应用程序的屏幕截图也很有用。以及有关它崩溃的机器的信息(操作系统版本和补丁,当时正在运行的其他内容等)。我提到的两个工具都可以做到这一点。

如果这是少数用户发生的事情,但您无法重现它,而他们可以,请与他们坐在一起观看。如果不明显,换个座位——你“开车”,他们会告诉你该怎么做。您将通过这种方式发现细微的可用性问题。双击单击按钮,例如,在 OnClick 事件中启动重新进入。之类的东西。如果用户是远程的,使用WebEx、Wink等,记录他们崩溃的情况,这样你就可以分析回放了。

【讨论】:

以上是关于您如何重现偶尔出现的错误?的主要内容,如果未能解决你的问题,请参考以下文章

迭代器偶尔会出现分段错误

偶尔会出现“处理调整请求的时间过多”错误

使用 SUM 函数时偶尔会出现转换错误

在 laravel 5.7 中偶尔会出现 419 错误

spi flash偶尔出现写入错误的情况

滑动删除 UITableViewCell 时,偶尔会出现红色减号删除图标