生产环境中调试崩溃问题

5
首先,我应该给你一些背景信息。所讨论的程序是一种典型的使用C++实现的服务器应用程序。在整个项目中,以及所有基础库中,错误管理都是基于C++异常的。
我的问题涉及处理无法恢复的错误和/或程序员错误 - 相当于未经检查的Java异常,缺乏更好的并行。我特别关注如何在生产环境中处理这类情况的常见做法。
对于生产环境,存在两个相互冲突的目标:易于调试和可用性(在运行性能方面)。每个目标依次建议了具体的策略:
1. 安装一个顶层异常处理程序来吸收所有未捕获的异常,从而确保持续可用性。不幸的是,这使得错误检查更加复杂,迫使程序员依靠细粒度日志记录或其他“工具化”技术。
2. 崩溃得尽可能严重;这样可以通过核心转储执行导致错误的条件的事后分析。自然地,必须提供一种方式让系统在崩溃后及时恢复运行,而这可能远非微不足道。
因此,我最终得到了两个不完善的解决方案;我希望能在服务可用性和调试功能之间取得折衷。我错过了什么?
注意:我已将问题标记为特定于C ++,因为我对适用于它的解决方案和特殊性很感兴趣;尽管如此,我意识到它与其他语言/环境有很大重叠。

1
单元测试应该被依赖于在发布到生产环境之前捕获任何错误。一旦进入生产环境,可以使用pdb文件进行调试。 - Bathsheba
1
前面的问题中有很好的答案和评论,请见此处。断言是同时记录和测试合同要求的好方法。 - Steger
我知道的方法是使用调试工具之一,如gdb或dbx,并调试核心转储文件以获取一些线索。 - Mehdi Karamosly
一旦程序发布后,使用核心转储就变得很困难。最好的方法是创建相关的单元测试、断言,并且以启用某些日志宏的方式运行您的发布,以允许您的客户提取额外的信息。请记住,异常处理增加了额外的复杂性,因为您需要知道代码块可能抛出的异常类型。例如,未处理的异常可能会冻结线程,导致不是崩溃而是具有未定义行为的奇怪运行时错误。因此,您的客户可能认为一切正常,而实际上一切都是错误的。 - Claudiordgz
尽管如此,如果您的平台支持Valgrind,请务必使用它。 - Steger
显示剩余2条评论
2个回答

3
声明:和提问者一样,我也是为服务器编写代码的,因此本篇答案只关注此特定使用场景。嵌入式软件或部署应用程序的策略可能大相径庭,不了解。

首先,这个问题有两个重要(且非常不同)的方面:

  • 尽可能简化调查
  • 确保恢复

让我们分别处理它们,因为分而治之更好。让我们从更困难的部分开始。


确保恢复

C++ / Java 风格的 try/catch 的主要问题在于,它极易破坏您的环境,因为 trycatch 可以改变其自身范围之外的内容。注意:与 Rust 和 Go 不同,任务不应与其他任务共享可变数据,并且 fail 会杀死整个任务而无法恢复。

因此,有三种恢复情况:

  • 无法恢复:进程内存已经损坏无法修复
  • 可手动恢复:在顶层处理程序中可以恢复进程,但需要重新初始化其内存的重要部分(缓存等)。
  • 可自动恢复:好的,一旦我们到达顶层处理程序,进程就可以再次使用了。

完全无法恢复的错误最好通过崩溃来解决。实际上,在许多情况下(例如指针超出您的进程内存),操作系统将帮助使其崩溃。但不幸的是,在某些情况下,它不会(悬空指针仍然可能指向您的进程内存中),这就是发生内存损坏的原因。哎呀。Valgrind、Asan、Purify 等工具旨在帮助您尽早捕获这些不幸的错误;调试器将为那些通过该阶段的错误提供一定帮助。

需要手动清理的错误是令人烦恼的。在某些很少发生的情况下,您会忘记清理。因此,应该在静态防止这种情况出现。一个简单的变换(将缓存移动到顶层处理程序作用域内)可使您将其转换为可自动恢复的状态。

在后一种情况下,显然,您可以只��获、记录并恢复进程,并等待下一个查询。您的目标应该是,在生产中只发生这种情况(如果它甚至不会发生,则获得额外的奖励积分)。


简化调查

注意:我将利用 Mozilla 的一个项目 rr 来推广一下,一旦它成熟,它真的可以帮助进行调查。请参见本节末尾的快速提示。

毫不意外,为了进行调查,您需要数据。最好尽可能多,并且有序/标记清晰。
有两种(实践过的)获取数据的方法:
连续记录,这样当异常发生时,您就可以获得尽可能多的上下文信息。
异常记录,因此在发生异常时,您可以记录尽可能多的信息。
连续记录意味着性能开销和(当一切正常时)大量无用日志的产生。另一方面,异常记录意味着对系统在异常情况下执行某些操作的能力有足够的信任(在bad_alloc的情况下...嗯)。
总体而言,我建议两者混合使用。
连续记录
每个日志应包含:
时间戳(尽可能精确)。
(可能)服务器名称、进程ID和线程ID。
(可能)查询/会话相关器。
此日志来自的文件名、行号和函数名。
当然,还要包含“消息”,其中应包含动态信息(如果您有静态消息,则可能可以使用动态信息丰富它)。
值得记录的内容是什么?
至少是I/O。所有输入,至少输出都可以帮助发现与预期行为的第一次偏差。I/O包括:入站查询及其相应响应,以及与其他服务器、数据库、各种本地缓存和时间戳交互(用于基于时间的决策)等。
这种记录的目标是能够在控制环境中重现发现的问题(可以通过所有这些信息设置)。作为奖励,它还可以作为粗略性能监视器使用,因为它在过程中提供了一些检查点(注意:我谈论监视而不是分析的原因是,这可以让您引发警报并发现时间大致花费在哪里,但您需要更高级的分析来理解为什么)。
异常记录
另一个选择是丰富异常。对于一个“简陋”的异常,例如:std::out_of_range会产生以下原因(从what中):vector::_M_range_check在抛出libstdc++的向量时。
如果像我一样,vector是您的容器选择,因此在代码中可能有大约3,640个位置可以抛出此异常,则此方法几乎没有用处。
要获得有用的异常,基础知识如下:
  • 一个更精确的信息:"访问大小为4的向量中索引32",这样稍微更有帮助,不是吗?
  • 一个调用堆栈:虽然需要平台特定的代码来检索它,但可以自动插入到您的基本异常构造函数中,所以去做吧!

注意:一旦在您的异常中有了调用堆栈,您很快就会发现自己上瘾了,并将能力较弱的第三方软件包装成适配器层,以便将其异常转换为您的异常;我们都这样做了;)

除了这些基础知识外,还有一个非常有趣的RAII功能:在展开期间将注释附加到当前异常。一个简单的处理程序保留对变量的引用,并在其析构函数中检查是否正在展开异常,通常只需要一个if检查,并在展开时执行所有重要的日志记录(但是,异常传播已经很昂贵了,所以……)。

最后,您还可以在catch子句中丰富并重新抛出异常,但这会迅速使代码混乱,并且会有很多try/catch块,因此我建议改用RAII。

注意:std异常不分配内存的原因是,它允许抛出异常而不会被std::bad_alloc所代替;我建议在一般情况下有意识地选择具有更丰富异常的可能性,并在尝试创建异常时引发std::bad_alloc(我还没有看到这种情况发生)。您必须做出自己的选择。

延迟日志记录?

延迟日志记录的想法是,与其像平常一样调用日志处理程序,您将推迟记录所有更细粒度的跟踪,并仅在出现问题(即异常)时才进行记录。

因此,这个想法是将日志记录分开:

  • 重要信息立即记录
  • 更细粒度的信息写入一个临时区域,在出现异常时可以调用它来记录它们

当然,也有一些问题:

  • 在崩溃的情况下,临时区域(大部分)会丢失;如果您获取了内存转储,则应该能够通过调试器访问它,尽管这并不是很愉快。
  • 临时区域需要策略:何时丢弃?(会话结束时?事务结束时?……),多少内存?(任意数量?有限制的?……)
  • 性能成本如何:即使不将日志写入磁盘/网络,格式化它们仍然需要成本!

实际上,我从未使用过这样的临时区域,现在所有非崩溃错误都是通过I/O记录和丰富的异常解决的。尽管如此,如果我要实施它,我建议将其制作为:

  • 事务本地化:由于I/O被记录,我们不需要更多的洞察力。
  • 内存有限:在我们进展的过程中逐出旧的跟踪。
  • 日志级别驱动:就像常规日志记录一样,我希望能够只启用某些日志以进入Scratch Pad。

那么条件/概率日志呢?

每N个写入一个跟踪并没有什么意义;实际上这比什么都没做还要更令人困惑。另一方面,深入记录每个事务中的一个可以帮助!

这里的想法是在一般情况下减少写入的日志量,同时仍有机会在野外详细观察错误跟踪。该减少通常由日志记录基础设施约束(传输和编写所有那些字节是有成本的)或软件性能(格式化日志会使软件变慢)驱动。

概率日志的想法是在每个会话/事务开始时“抛硬币”,决定它是快速的还是缓慢的:)

类似的想法(条件日志)是在事务字段中读取一个特殊的debug字段,该字段启动完整日志记录(以速度为代价)。

关于rr的简短说明

仅有20%的开销,并且此开销仅适用于CPU处理,因此可能值得系统地使用rr。但是,如果这不可行,那么可以有1个N个服务器在rr下启动并用于发现难以找到的错误。

这类似于A/B测试,但用于调试目的,并且可以通过客户愿意承诺(交易标志)或概率方法来推动。

哦,在一般情况下,当您没有追踪任何东西时,可以轻松地完全停用它。然后没有必要支付这20%。


就这些了

我可以为篇幅冗长的阅读道歉,但事实上,我可能只是浏览了主题。错误恢复很难。我将感谢评论和备注,以帮助改进此答案。


0

如果错误是不可恢复的,根据定义,在生产环境中应用程序无法从错误中恢复。换句话说,顶层异常处理程序并不是真正的解决方案。即使应用程序显示友好的消息,如“访问冲突”,“可能的内存损坏”等,这实际上并不增加可用性。

当应用程序在生产环境中崩溃时,您应该尽可能获取后期分析所需的信息(第二个解决方案)。

也就是说,如果在生产环境中出现不可恢复的错误,则主要问题是产品QA过程(缺乏),以及(在此之前),编写不安全/未经测试的代码。

当您完成调查此类崩溃时,您不仅应修复代码,还应修复开发流程,以便不再可能发生此类崩溃(即,如果损坏是未初始化指针写入,则检查您的代码库并初始化所有指针等)。


如果损坏是未初始化指针写入,请检查您的代码库并初始化所有指针等。这仍然是一个修复,为了防止将来发生这样的崩溃,意味着建立一个定期检查,以确保不再发生这种情况。可以通过投资于静态分析和/或在Valgrind(或其他内存调试工具)下运行测试套件来实现。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接