你如何处理间歇性的错误?

时间:2008-12-09 14:31:55

标签: debugging validation

方案

你有几个错误报告都显示同样的问题。他们都是神秘的,有关问题如何发生的类似故事。您按照步骤操作但不能可靠地重现问题。经过一些调查和网络搜索,你怀疑可能会发生什么,你很确定你可以解决它。

问题

遗憾的是,如果没有可靠的方法来重现原始问题,您无法验证它是否实际修复了问题,而不是完全没有效果或加剧和掩盖真正的问题。您可能无法修复它,直到它每次都可以重现,但这是一个很大的错误而且修复它会导致用户遇到很多其他问题。

问题

如何验证更改?

对于设计软件的人来说,我认为这是一个非常熟悉的场景,因此我确信有很多方法和最佳实践来解决这样的错误。我们目前正在研究我们项目中的其中一个问题,我花了一些时间来确定问题,但无法证实我的怀疑。一位同事正在测试我的修复,希望“没有崩溃的一天运行”等同于“它是固定的”。但是,我更喜欢更可靠的方法,我认为这里有丰富的经验。

18 个答案:

答案 0 :(得分:12)

难以复制的错误是最难解决的问题。您需要确保找到问题的根源,即使问题本身无法成功复制。

最常见的间歇性错误是由竞争条件引起的 - 通过消除竞争,或确保一方赢得你已经消除了问题的根源,即使你无法通过测试结果成功确认它。你唯一可以测试的是原因确实需要重复。

有时修复被视为根的东西确实解决了问题,但却没有解决问题 - 没有避免它。避免间歇性错误的最佳方法是对系统设计和架构进行谨慎和有条理的处理。

答案 1 :(得分:7)

如果没有确定根本原因并提出可靠的方法来重现错误,您将永远无法验证修复。

用于确定根本原因:如果您的平台允许,请将一些事后调试挂钩到问题中。

例如,在Windows上,让代码在遇到此问题时创建一个minidump文件(Unix上的核心转储)。然后,您可以让客户(或Windows上的WinQual)向您发送此文件。这应该可以为您提供有关代码在生产系统中出错的更多信息。

但如果没有这个,你仍然需要提出一种可靠的方法来重现这个bug。否则你永远无法验证它是否已修复。

即使掌握了所有这些信息,您最终也可能会修复一个看起来像客户看到的错误的错误。

答案 2 :(得分:5)

使用更广​​泛(可选)的日志记录和数据保存对构建进行检测,从而可以准确再现用户在崩溃发生之前所采用的变量UI步骤。

如果该数据不能可靠地让您重现问题,那么您已经缩小了类的bug。是时候查看随机行为的来源了,例如系统配置的变化,指针比较,未初始化的数据等。

有时你“知道”(或者更确切地说)你可以在没有大量测试或单元测试脚手架的情况下修复问题,因为你真正理解了这个问题。但是,如果你不这样做,它往往归结为“我们运行了100次并且错误不再发生,所以我们会在下次报告时将其视为固定。”。

答案 3 :(得分:5)

我在所有看似由问题链接的模块中使用我称之为“重型防御性编程”添加断言。我的意思是,添加大量断言,断言证据,断言所有成员中的对象状态,断言“环境”状态等。

断言可帮助您识别与问题无关的代码。

大多数时候,我只是通过编写断言来找到问题的根源,因为它会强制您重新阅读所有代码并在应用程序的内容下进行理解。

答案 4 :(得分:4)

这个问题没有一个答案。有时您找到的解决方案可帮助您找出重现问题的方案,在这种情况下,您可以在修复之前和之后测试该方案。但有时候,你发现的解决方案只能解决其中一个问题,而不是全部问题,或者就像你说的那样掩盖了一个更深层次的问题。我希望我能说“做这个,它每次都有效”,但是没有适合那种情况的“这个”。

答案 5 :(得分:2)

首先,您需要从客户端获取堆栈跟踪,这样您实际上可以进行一些取证。

接下来用随机输入进行模糊测试,并保持这些测试长时间运行,他们非常善于找到那些不合理的边界情况,人类程序员和测试人员可以通过用例和对代码的理解找到它们。

答案 6 :(得分:2)

你在评论中说你认为这是一种竞争条件。如果您认为您知道代码的“特征”是什么产生了这个条件,那么您可以编写一个测试来试图强制它。

以下是c:

中的一些危险代码
const int NITER = 1000;
int thread_unsafe_count = 0;
int thread_unsafe_tracker = 0;

void* thread_unsafe_plus(void *a){
  int i, local;
  thread_unsafe_tracker++;
  for (i=0; i<NITER; i++){
    local = thread_unsafe_count;
    local++;
    thread_unsafe_count+=local;
  };
}
void* thread_unsafe_minus(void *a){
  int i, local;
  thread_unsafe_tracker--;
  for (i=0; i<NITER; i++){
    local = thread_unsafe_count;
    local--;
    thread_unsafe_count+=local;
  };
}

我可以测试(在pthreads enironment中):

pthread_t th1, th2;
pthread_create(&th1,NULL,&thread_unsafe_plus,NULL);
pthread_create(&th2,NULL,&thread_unsafe_minus,NULL);
pthread_join(th1,NULL);
pthread_join(th2,NULL);
if (thread_unsafe_count != 0) {
  printf("Ah ha!\n");
}

在现实生活中,你可能不得不以某种方式包装你的可疑代码,以帮助比赛更进一步。

如果有效,请调整线程数和其他参数,以使其在大多数时间点击,现在你有机会。

答案 7 :(得分:1)

您可以问自己一些问题:

  • 这段代码什么时候没有问题。
  • 自停止工作以来所做的一切。

如果代码从未起作用,那么这种方法自然会有所不同。

至少当许多用户一直在更改大量代码时,这是一种非常常见的情况。

答案 8 :(得分:1)

一旦你完全理解了这个bug(这是一个很大的“曾经”),你应该可以随意重现它。当编写再现代码(自动测试)时,您可以修复该错误。

如何达到理解错误的程度?

检测代码(像疯了似的日志)。使用您的质量保证 - 他们擅长重新创建问题,您需要安排在他们的计算机上为您提供完整的开发工具包。将自动化工具用于未初始化的内存/资源。只是简单地盯着代码。那里没有简单的解决方案。

答案 9 :(得分:1)

除非有严重的时间限制,否则在我能够可靠地重现问题之前,我不会开始测试更改。

如果您真的不得不这样做,我想您可以编写一个似乎有时会触发问题的测试用例,并将其添加到您的自动测试套件中(您确实有自动测试套件,对吗?),然后制作您的改变并希望测试用例再也不会失败,知道如果你没有真正解决任何问题,至少你现在有更多的机会去抓它。但是当你编写一个测试用例时,你几乎总是将事情减少到你不再处理这种(显然)非确定性情况的程度。

答案 10 :(得分:1)

我遇到了似乎一直导致错误的系统上的错误,但是当在调试器中单步执行代码时,问题就会神秘地消失。在所有这些情况下,问题都是时间问题。

当系统正常运行时,资源存在某种冲突,或者在最后一步完成之前采取下一步措施。当我在调试器中逐步完成它时,事情进展缓慢,问题就消失了。

一旦我发现它是一个时间问题,很容易找到修复。我不确定这是否适用于您的情况,但是当调试器中的错误消失时,问题是我的第一个嫌疑人。

答案 11 :(得分:1)

对于难以重现的错误,第一步通常是文档。在失败的代码区域中,将代码修改为超显式:每行一个命令;重型,差异化的异常处理;详细,甚至是冗余的调试输出。这样,即使您无法重现或修复错误,您也可以在下次看到失败时获得有关原因的更多信息。

第二步通常是假设和界限检查的断言。你认为你所知道的有关代码的一切,写。断言和检查。具体来说,检查对象的无效性和(如果您的语言是动态的)存在。

第三,检查您的单元测试覆盖率。您的单元测试是否实际涵盖执行中的每个分支?如果你没有单元测试,这可能是一个很好的起点。

不可重现的错误的问题在于它们只对开发人员不可再现。如果您的最终用户坚持要复制它们,那么它就是利用现场崩溃的有效工具。

答案 12 :(得分:1)

具体方案

虽然我不想只关注我所遇到的问题,但这里有一些关于我们当前面临的问题以及到目前为止我如何解决这个问题的细节。

当用户在进程的特定阶段与用户界面(确切地说是TabControl)进行交互时,会出现问题。它并不总是发生,我相信这是因为展示问题的时间窗口很小。我怀疑UserControl(我们在.NET中,使用C#)的初始化与来自应用程序的另一个区域的状态更改事件一致,这导致字体被处置。同时,另一个控件(Label)尝试使用该字体绘制其字符串,从而导致崩溃。

然而,实际上确认导致字体处理的原因已经证明是困难的。目前的修复方法是克隆字体,以便绘图标签仍然具有有效字体,但这确实掩盖了根本问题,即首先处理的字体。显然,我想追踪整个序列,但事实证明这很困难,而且时间很短。

方法

我的方法是首先查看崩溃报告中的堆栈跟踪,然后使用Reflector检查Microsoft代码。不幸的是,这导致了一个带有少量文档的GDI +调用,只返回错误的数字 - .NET将其转换为一个非常无用的消息,表明某些内容无效。大。

从那里开始,我看看代码中的调用会导致这个问题。堆栈以消息循环开始,而不是在我们的代码中,但我发现在怀疑的一般区域中调用Update(),并使用仪器(跟踪等),我们能够确认大约75%的确定性是油漆消息的来源。然而,它不是错误的根源 - 要求标签绘画是没有犯罪的。

从那里,我查看了油漆调用的每一个方面(DrawString),看看什么可能是无效的,并开始统治每一个,直到它落在一次性物品上。然后我确定了我们控制了哪些,而字体是唯一的。所以,我看看我们如何处理字体,以及在什么情况下我们处理它以确定任何潜在的根本原因。我能够提出适合用户报告的合理事件序列,因此能够编写低风险修复程序。

当然,我突然意识到这个错误存在于框架中,但我想假设在将责任归咎于微软之前搞砸了。

结论

所以,这就是我如何处理这类问题的一个特定例子。正如你所看到的,它不太理想,但与许多人所说的相符。

答案 13 :(得分:1)

这些都是可怕的,几乎总是能抵抗工程师认为他所投入的“修复”,因为他们有几个月后回来咬的习惯。警惕任何对间歇性错误的修复。准备好进行一些繁琐的工作和密集的日志记录,因为这听起来更像是一个测试问题而不是开发问题。

当克服这些错误时,我自己的问题是我经常过于接近问题,而不是站在后面看着更大的画面。尝试让其他人看看你如何解决问题。

具体来说,我的错误与设置超时和各种其他魔术数字有关,回想起边缘线几乎所有时间都如此。在我自己的案例中的诀窍是对设置进行大量实验,我可以找出哪些值会“破坏”软件。

在特定时间段内是否发生了故障?如果是这样,何时何地?是否只有某些人似乎重现了这个错误?什么样的输入似乎引起了这个问题?该应用程序的哪个部分失败了?这个漏洞在现场看起来或多或少是间歇性的吗?

当我是一名软件测试人员时,我的主要工具是用笔和纸记录我以前的行为记录 - 记住许多看似微不足道的细节至关重要。通过观察和收集一点点数据,虫子似乎变得不那么间歇了。

答案 14 :(得分:1)

在这种情况下,没有其他工作,我引入了额外的日志记录。

我还添加了电子邮件通知,告诉我应用程序崩溃时的状态。

有时我会添加性能计数器...我将这些数据放在一个表中并查看趋势。

即使没有任何显示,你也在缩小范围。不管怎样,你最终得到有用的理论。

答案 15 :(得分:1)

这些类型的错误非常令人沮丧。将它推广到不同类型的定制硬件(可能在我的公司)中的不同机器,男孩哦,男孩会变成一场噩梦。我目前在工作时遇到了几个这样的错误。

我的经验法则:我不会修复它,除非我自己可以复制它或者我会看到一个清楚地显示错误的日志。否则我无法验证我的更改,也无法验证我的更改是否已破坏其他任何内容。当然,这只是一个经验法则 - 我确实做了例外。

我认为你很关心你的同事的方法。

答案 16 :(得分:0)

这些问题一直是由以下原因造成的:

  1. 内存问题
  2. 线程问题
  3. 要解决问题,您应该:

    • 检测代码(添加日志语句)
    • 代码审核线程
    • 代码审核内存分配/解除引用

    代码审查很可能只会在优先级发生时发生,或者您对多个错误报告共享哪些代码存在强烈怀疑。如果这是一个线程问题,那么检查线程的安全性 - 确保两个线程都可以访问的变量受到保护。如果这是一个内存问题,那么请检查您的分配和解除引用,尤其是对分配和返回内存的代码或使用可能正在释放它的其他人使用内存分配的代码持怀疑态度。

答案 17 :(得分:0)

简单地说:询问报告该用户的用户。

我只是使用其中一名记者作为验证系统。 通常,愿意报告错误的人非常乐意帮助您解决问题[1]。 只需给她一个可能的修复版本,然后询问问题是否已解决。 在错误是回归的情况下,可以使用相同的方法通过向用户提供要测试的多个版本的问题来平分问题发生的位置。 在其他情况下,用户还可以通过为她提供具有更多调试功能的版本来帮助您调试问题。

这将限制对该人可能修复的任何负面影响,而不是猜测某些东西会修复该错误,然后意识到您刚刚发布了一个无效的错误修复或最坏情况下对系统稳定性的负面影响。

您还可以通过将新版本提供给有限数量的用户(例如,报告问题的所有用户)并仅在此之后发布修复来限制“错误修复”可能产生的负面影响。

此外,她可以确认您所做的修复工作,很容易添加测试,以确保您的修复将保留在代码中(至少在单元测试级别,如果错误很难重现更多更高的系统级别。)

当然,这要求无论你在做什么都支持这种方法。但如果不是,我会尽我所能来实现它 - 终端用户会更满意,许多最难的技术问题就会消失,当开发可以直接与系统最终用户交互时,优先级就会变得清晰。

[1]如果你曾经报告过一个错误,你很可能知道很多时候开发/维护团队的响应从最终用户的角度来看是负面的,或者根本就没有响应 - 特别是在开发团队无法复制错误的情况。