原文请见 Minimizing Unreproducible Bugs

不能重现的 bug 是我的灾难。我常常找到一个 bug 后来又听说这不是一个 bug,因为它无法重现。但是这个 bug 仍旧在那里,等着捕食下一个受害者。这些类型的 bug 非常昂贵,因为我们需要花大量的时间去调查。它们也会对产品体验造成破坏性的影响,特别是用户发现并报告了这些被忽略的 bug。所以为了防止这类问题,我们需要做更多。在这篇文章里,我将探讨一些明显或者不是那么明显的开发或者测试的准则,这些准则能多少减少些这些 bug 发生的可能性。

如何避免或者测试竞争,死锁,内存崩溃,时间问题,访问未初始化内存,内存泄露,资源问题

在这一节里我会将许多类型的 bug 混在一起说。这些 bug 在我们如何测试它们和它们难以重现和调试这点上是相关的。
其根源及其影响可以是微秒级别,也可能持续数个小时。它们的堆栈信息可能不存在,也可能会误导人。当遇到不正常的流量高峰或者资源不够的情况下,系统可能会出现怪异的运行故障。单一的访问模式或者资源配置会导致多线/进程竞争或者死锁。当很多组件集成一起,而各组件的性能参数差异和失败/重启/超时时间延迟会让系统一团糟,这时候就会出现时间同步问题。我们在大量的函数调用里可能不会注意到内存崩溃或者访问未初始化内存的问题,但是在一些边缘用例下就很致命。一般只有在负载或者长时间运行,内存泄露才会被发现。

开发准则:

测试准则:

强制实施先决条件

我见过很多有着高容错性的善意函数。比如,看下面的这个函数:

void ScheduleEvent(int timeDurationMilliseconds) {
  if (timeDurationMilliseconds <= 0) {
    timeDurationMilliseconds = 1;
  }
  ...
}

当输入 timeDurationMilliseconds 不合理时,函数可以调整输入为可接受的值,但是它也可能掩盖了一个 bug。调用代码可能会遇到本文中描述的任一问题,而且即使传递垃圾数据给这个函数也能正常工作(只要不小于等于 0)。这种带有容错的函数越多,想要找到错误根源就越难,而且很有可能,最终用户也会看见这些垃圾信息。强制实施先决条件,比如用断言,对于新系统而言,的确可能会导致很多的故障失败。但是随着系统的成熟,可以及早发现很多小的大的问题。这些检查可以帮助提高系统的长期可靠性。

开发准则:

在你的函数里强制实施先决条件,除非你有更好的理由不去做。

使用防御性编程

防御性编程是另一个靠得住的技术,它能够有效的减少无法重现的 bug。如果你的代码使用依赖去做一些事情,但是依赖的代码执行失败却没有抛出错误或者返回了垃圾,你的代码该如何处理?你可以通过模拟或者伪造来测试这种情景。但是或许你在代码对它的依赖做一些健全检查会更好。比如:

double GetMonthlyLoanPayment() {
  double rate = GetTodaysInterestRateFromExternalSystem();
  if (rate < 0.001 || rate > 0.5) {
    throw BadInterestRate(rate);
  }
  ...

开发准则:

尽可能的使用防御性编程,验证你所依赖部分的工作,尤其是会造成故障的已知风险,比如用户提供的数据,I/O 操作和 RPC 调用。

测试准则:

使用 fuzz testing 来测试系统容错的健壮性。

不要从用户角度处理所有的错误问题

近年来有一种趋势,不惜任何代价不然用户看到故障。这在很多案例中,很有意义。但是在一些案例中,我们过头了。
如果用户遇到小故障,但是代码没有抛出异常或者直接放过了,那么无知的用户会在一个失败的状态下继续工作。到最后,软件总会到一个致命的点,所有造成这个致命故障的原因都被忽略了。如果用户不了解先前的错误,他们就不能报告这些错误,你也没办法重现它们。

开发准则:

测试出错处理

错误处理的代码是最常不会被测试的部分。测试覆盖不应该略过这里的。如果不能非常好地处理致命错误,糟糕的错误处理代码会造成不能重现的 bug 并带来风险。

测试准则:

检查重复的键

如果唯一标识或者访问数据的键是通过随机生成的,不能保证全局唯一性的话,重复的键会导致数据损坏或者并发问题。这种问题非常难重现。

开发准则:

测试并发数据访问

有些 bug 只会在多个客户端读写同一块数据时候发生。一般压力测试会覆盖这些用例,但是如果没有的话,你就需要特别设计一些并发数据访问的用例。这种情况下发生的 bug 常常是无法重现的。比如,一个用户可能有两个应用实例运行在同一个账户上,它们可能没有注意到这点,直到错误发生。

测试准则:

绕开不明确的行为和不明确的数据访问

当在某些状态下或者某些输入下,一些 API 和基本操作会对未定义的行为报警。同样,一些数据结构没有办法保证迭代顺序(比如 JAVA 的 Set)。在代码里忽略这些警告大多数时候能很好的工作。但是一旦失败,就很难重现。

开发准则:

你要了解你所使用的 API 和操作可能有未定义的行为,需要预防这些情况。不要过多依赖数据结构的迭代顺序,除非你能保证这个顺序。依赖集合和关联数组的顺序是比较普遍的错误。

记录错误日志故障日志的细节

如果日志包含足够的出错细节,那么本文描述的问题就可以很容易地重现和调试。

开发准则:

测试准则:

保存你的日志以便后续分析。

还有什么要添加?

我有遗漏什么重要的,可以减少这些 bug 的准则吗? 你发现和解决的难以重现的 bug 是什么?


↙↙↙阅读原文可查看相关链接,并与作者交流