你如何重现零星发生的错误?

时间:2010-03-25 13:33:32

标签: debugging language-agnostic logging

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

免责声明:此错误存在且我已经看过了。它不是pebkac或类似的东西。

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

28 个答案:

答案 0 :(得分:27)

分析一对中的问题并成对阅读代码。记下你知道的问题是真的,并尝试断言必须的逻辑前提条件是否适用于此。遵循像CSI这样的证据。

大多数人本能地说“添加更多日志记录”,这可能是一个解决方案。但是对于很多问题,这只会使事情变得更糟,因为日志记录可以充分改变时序依赖性,从而使问题或多或少地发生。将频率从1000变为1到1,000,000中的1不会使您更接近问题的真正来源。

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

答案 1 :(得分:25)

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

答案 2 :(得分:21)

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

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

  2. 向后工作,并将其视为科学调查。从错误开始,你看到的是错误的。开发关于可能导致它的原因的假设(这是创造性/想象力的部分,不是每个人都有才能的艺术) - 并且它有助于了解代码的工作原理。对于这些假设中的每一个(最好按照您认为最可能的方式排序 - 再次纯粹的肠道感觉),开发一个试图消除它作为原因的测试,并测试假设。任何给定的未能达到预测并不意味着假设是错误的。测试假设,直到确认错误为止(尽管你不太可能首先想要转向另一个假设,但是在你明确失败之前不要对这个假设进行折扣)。

  3. 在此过程中收集尽可能多的数据。广泛的日志记录和其他适用的内容。不要因为缺乏数据而忽视假设,而是要弥补数据的缺乏。通常,正确假设的灵感来自于检查数据。注意堆栈跟踪中的某些内容,日志中的奇怪问题,数据库中应该存在的缺失等等。

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

  5. 如果你的假设用尽了,那通常是由于对系统知识不足造成的(即使你自己编写了每一行代码也是如此),并且你需要贯穿并审查代码并获得对系统提出了一个新的想法。

    当然,以上都不能保证任何东西,但这就是我发现的方法可以得到一致的结果。

答案 3 :(得分:8)

程序员不能仅仅因为你已经开发了一个特定的工作流程和使用该应用程序的习惯而不能重复用户体验的崩溃,这是非常常见的。

在这个1/100的频率下,我要说的第一件事就是处理异常并在任何地方记录任何东西,否则你可能会花费一周时间来寻找这个bug。 还要列出项目中可能敏感的关联和功能的优先级列表。例如 : 1 - 多线程 2 - 狂野指针/松散阵列 3 - 依赖输入设备 等等 这将有助于您根据其他海报的建议,将您可以蛮力破坏的区域再次划分。

答案 4 :(得分:7)

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

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

不同的用户,同一台电脑? 相同的用户,不同的电脑? 该事件是强烈周期性的吗?重启会改变周期吗?

仅供参考 - 我曾经看到一个人经历过的错误。我的意思是人,而不是用户帐户。用户A永远不会在他们的系统上看到问题,用户B将坐在该工作站上,以用户A 身份登录,并可立即重现该错误。应用程序应该没有可能的方法来了解椅子中的物理身体之间的差异。然而 -

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

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

答案 5 :(得分:5)

您的应用很可能是MTWIDNTBMT(多线程,当它不需要多线程),或者可能只是多线程(礼貌)。在多线程应用程序中重现偶发错误的一种好方法是在(C#)周围撒出这样的代码:

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

和/或这:

for (int i = 0; i < 4000000000; i++)
{
    // tight loop
}

模拟线程在不同时间完成任务,或者将处理器长时间拉长。

多年来,我继承了许多错误的多线程应用程序,而上述示例之类的代码通常会使得偶发错误更频繁地发生。

答案 6 :(得分:4)

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

编辑:多一点想法:如果这是一个gui应用程序运行测试与qa自动化工具,它允许您重播宏。如果这是一个服务类型的应用程序,尝试至少猜测发生了什么,然后以编程方式创建“怪胎”使用模式,这将运用您怀疑的代码。创建高于平常的负载等。

答案 7 :(得分:3)

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

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

答案 8 :(得分:2)

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

答案 9 :(得分:2)

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

答案 10 :(得分:2)

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

您可以查看Design by contract

答案 11 :(得分:2)

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

说过没有银弹。我感觉到你的痛苦。

答案 12 :(得分:2)

假设我开始使用生产应用程序。

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

  2. 我重复步骤1,直到我知道在调试器中开始调试代码的地方

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

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

  5. 调试,调试,调试。如果几个小时后你无处可去,请休息一下,继续研究一段时间无关的事情。带着清新的头脑和观点回来。

  6. 如果你现在已经无处可去,请回到第1步并进行另一次迭代。

  7. 对于真正困难的问题,您可能不得不在发生错误的系统上安装调试器。结合第4步的测试工具,通常可以解决真正令人困惑的问题。

答案 13 :(得分:2)

伴随着很多的耐心,一个安静的祈祷&amp;诅咒你需要:

  • 记录用户操作的良好机制
  • 用户执行某些操作时收集数据状态的良好机制(应用程序,数据库等中的状态)。
  • 检查服务器环境(例如,在特定时间运行的防病毒软件等)&amp;记录错误的时间&amp;看看你是否能找到任何趋势
  • 更多的祈祷&amp;咒骂...

HTH。

答案 14 :(得分:2)

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

如果部署了应用程序并且无法调整日志记录,这些可能会特别痛苦。在这一点上,你唯一的选择就是用精细的梳子梳理代码并试图推断程序如何进入糟糕状态(科学方法来拯救!)

答案 15 :(得分:2)

以上所有内容,再加上一些半随机的暴力软机器人,并通过代码调整了许多断言/验证(c / c ++,可能与其他lang类似)

答案 16 :(得分:2)

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

记录你能做什么,所以当它发生时你可以捕获正确的系统状态。

另一件事 - 尝试应用自动化测试,这种测试可以覆盖更多的领域,而不是基于人类的测试。这是一个长镜头,但总的来说是一个很好的做法。

答案 17 :(得分:1)

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

tl; dr - 由于时间而构建失败

你提到了一天的时间,这引起了我的注意。有一个错误曾经有人晚上晚上工作,试图建立和承诺他们离开之前,并继续失败。他们最终放弃了回家。当他们第二天早上发现它建好了,他们承诺(可能应该更加怀疑=])并且构建适用于每个人。一两个星期后,有人迟到了,意外的构建失败了。事实证明,代码中存在一个错误,它在7PM休息后进行任何构建&gt;。&gt;

我们还发现了一个很少使用项目角落的错误,导致问题在不同的模式之间进行编组,因为我们没有考虑基于0和1个月的不同日历。因此,如果没有人搞砸了项目的这一部分,我们就不会在jan之前找到这个bug。 2011

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

答案 18 :(得分:1)

如果您无法确定性地重现问题,则调试非常困难且耗时。我给你的建议是找出确定性地(不仅仅是有时)重现它的步骤。

过去几年在失败再现领域进行了大量研究,并且仍然非常活跃。记录和重放技术(迄今为止)是大多数研究人员的研究方向。这是你需要做的:

1)分析源代码并确定应用程序中非确定性的来源,即,您的应用程序可能通过不同的执行路径(例如用户输入,OS信号)的方面是什么

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

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

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

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

祝你好运

答案 19 :(得分:1)

雇用一些测试人员!

答案 20 :(得分:1)

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

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

整个系统都出现了问题,所以制作一个测试夹具来锻炼整个系统。 假设它是带有GUI的服务器端事物,您可以使用GUI测试框架运行整个过程,执行必要的输入以引发问题。

它不会100%失败,因此您必须更频繁地失败。

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

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

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

最糟糕的情况是,您使用夹具并进行数周的测试以获得有意义的统计数据。这很难;但请记住,夹具正在开展工作。

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

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

对于线程/多进程问题:

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

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

如果这没有帮助,但您怀疑它是同步或线程,请尝试更改操作系统时间片大小。 使其与您的操作系统供应商允许的一样好! 有时这几乎每次都会发生竞争条件!

相反,试着在时间片上慢一点。

然后设置运行的测试夹具,调试器遍布整个地方并等待测试夹具停止发生故障。

如果所有其他方法都失败了,请将硬件放入冰箱并在那里运行。一切的时机都会改变。

答案 21 :(得分:1)

这种情况会有所不同(如你所说),但有些方便的事情可以是

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

答案 22 :(得分:1)

与我合作的团队已经邀请用户记录他们在使用CamStudio的应用程序中花费的时间,当我们有一个讨厌的bug追踪时。它易于安装和使用,并且可以更容易地再现那些烦人的错误,因为您可以观察用户正在做什么。它与您正在使用的语言没有任何关系,因为它只是记录Windows桌面。

然而,只有当您正在开发企业应用并与用户建立良好关系时,这条路线似乎才可行。

答案 23 :(得分:1)

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

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

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

除此之外,我只建议尽可能多地从用户那里获取信息,并全面了解项目工作流程

  

他们大多晚上出来....   大多

答案 24 :(得分:1)

我建议写下用户一直在做的所有事情。如果您可以说10个这样的错误报告您可以尝试找到连接它们的东西。

答案 25 :(得分:1)

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

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

这并不能保证我们可以集体找到它,或者......

答案 26 :(得分:1)

仔细阅读堆栈跟踪并尝试猜测可能发生的事情; 然后尝试跟踪\ log可能导致问题的每行代码。

专注于处理资源;我发现许多鬼鬼祟祟的零星错误与关闭\处置事物有关:)。

答案 27 :(得分:0)

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

如果这是少数用户发生的事情,但是你无法重现它,他们可以随便和他们一起观看。如果不明显,切换座位 - 你“开车”,他们告诉你该怎么做。你会发现那种微妙的可用性问题。双击单击按钮,例如,在OnClick事件中启动重新进入。诸如此类的事情。如果用户是远程用户,请使用WebEx,Wink等记录它们使其崩溃,以便分析回放。