【问题标题】:Debugging crashes in production environments调试生产环境中的崩溃
【发布时间】:2014-03-19 16:11:24
【问题描述】:

首先,我应该给你一些背景信息。有问题的程序是 一个用 C++ 实现的相当典型的服务器应用程序。横跨 项目以及所有底层库中的错误 管理基于 C++ 异常。

我的问题与处理不可恢复的错误和/或 程序员错误——“未经检查的”Java 的松散等价物 例外,因为想要更好的并行。我特别感兴趣 在生产中处理此类条件的常见做法 环境。

特别是对于生产环境,存在两个相互冲突的目标 出现上述类错误时排除:易于调试 和可用性(在操作性能的意义上)。每一个 这些反过来又建议了一个具体的策略:

  • 安装顶级异常处理程序以吸收所有未捕获的 例外,从而确保持续可用性。很遗憾, 这使得错误检查更加复杂,迫使程序员 依赖细粒度的日志记录或其他代码“工具” 技术。

  • 尽可能地崩溃;这使人们能够进行验尸 通过核心分析导致错误的条件 倾倒。自然,必须为系统提供一种恢复方式 坠机后及时操作,这可能还远 从琐碎。

所以我最终得到了两个半生不熟的解决方案;我想要妥协 在服务可用性和调试设施之间。我是什么 不见了?

注意:我已将问题标记为特定于 C++,因为我很感兴趣 在特别适用于它的解决方案和特质中; 尽管如此,我知道与其他领域会有相当大的重叠 语言/环境。

【问题讨论】:

  • 在发布到生产环境之前,应该依靠单元测试来捕获任何错误。一旦投入生产,就可以使用 pdb 文件进行调试。
  • 上一个问题here 提供了很好的答案和评论。断言是同时记录和测试合同要求的好方法。
  • 我知道的方法是使用gdb或dbx之类的调试工具之一调试核心转储,以获得一些线索。
  • 程序发布后很难使用核心转储。您最好的选择是创建相关单元测试、断言,以及一种在启用某些日志记录宏的情况下运行您的发布的方法,以允许您的客户可能提取的额外信息。请记住,异常处理增加了额外的复杂性,因为您需要知道代码块可能抛出的异常类型。例如,未处理的异常可能会冻结线程,导致的不是崩溃,而是运行时出现未定义行为的奇怪错误。因此,您的客户可能认为一切正常,但实际上一切都错了。
  • 您的目标是什么平台?如果是 Windows,应用程序恢复肯定是可能的。

标签: c++ debugging production


【解决方案1】:

免责声明:很像服务器的 OP I 代码,因此整个答案都集中在这个特定的用例上。嵌入式软件或已部署应用程序的策略可能应该大不相同,不知道。

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

  • 放宽调查(尽可能)
  • 确保恢复

让我们分别对待两者,因为分裂就是征服。让我们从更艰难的开始。


确保恢复

try/catch 的 C++/Java 风格的主要问题是它非常容易破坏您的环境,因为 trycatch 可以改变它们自己范围之外的内容。 注意:与 Rust 和 Go 相比,其中一个任务不应与其他任务共享可变数据,fail 将杀死整个任务而没有恢复的希望。

结果有3种恢复情况:

  • 不可恢复:进程内存已损坏,无法修复
  • 可手动恢复:进程可以在顶级处理程序中被挽救,代价是重新初始化其大部分内存(缓存,...)
  • 可自动恢复:好的,一旦我们到达顶级处理程序,该进程就可以再次使用了

完全不可恢复的错误最好通过崩溃来解决。实际上,在许多情况下(例如进程内存之外的指针),操作系统将有助于使其崩溃。不幸的是,在某些情况下它不会(悬空指针可能仍指向您的进程内存),这就是内存损坏发生的方式。哎呀。 Valgrind、Asan、Purify 等……是旨在帮助您尽早发现那些不幸错误的工具;调试器将(在某种程度上)帮助那些通过该阶段的人。

可以恢复但需要手动清理的错误很烦人。您在一些很少发生的情况下忘记清洁。因此它应该被静态地阻止。一个简单的转换(在顶级处理程序范围内移动缓存)允许您将其转换为自动可恢复的情况。

显然,在后一种情况下,您可以捕获、记录并恢复您的进程,等待下一个查询。您的目标应该是让这成为生产中唯一发生的情况(cookie 点,如果它甚至没有发生)。


放宽调查

注意:我将借此机会推广 Mozilla 的一个名为 rr 的项目,一旦它成熟,它真的可以帮助调查。查看本节末尾的快速说明。

毫无疑问,为了进行调查,您需要数据。最好是尽可能多,并且有序/标记良好。

有两种(实践过的)获取数据的方法:

  • 连续记录,以便在发生异常时尽可能多地获得上下文
  • 异常记录,以便在出现异常时尽可能多地记录

连续记录意味着性能开销和(当一切正常时)大量无用的日志。另一方面,异常日志记录意味着对系统在异常情况下执行某些操作的能力有足够的信任(在bad_alloc的情况下......哦,好吧)。

一般来说,我建议两者兼而有之。

连续记录

每个日志应包含:

  • 时间戳(尽可能精确)
  • (可能)服务器名称、进程 ID 和线程 ID
  • (可能)查询/会话相关器
  • 此日志来自的文件名、行号和函数名
  • 当然是message,它应该包含动态信息(如果你有静态消息,你可能可以用动态信息来丰富它)

什么值得记录?

至少 I/O。至少,所有输入和输出都可以帮助发现与预期行为的第一个偏差。 I/O 包括:入站查询和相应响应,以及与其他服务器、数据库、各种本地缓存的交互、时间戳(用于与时间相关的决策)、...

此类日志记录的目标是能够重现在控制环境中发现的问题(可以根据所有这些信息进行设置)。作为奖励,它可以用作粗略的性能监控器,因为它在过程中提供了一些检查点(注意:我说的是监控而不是出于某种原因进行分析,这可以让您发出警报并发现 ,粗略地说,时间是花的,但你需要更高级的分析来理解为什么)。

异常记录

另一个选项是丰富异常。作为粗略异常的示例:std::out_of_range 产生以下原因(来自what):vector::_M_range_check 当从 libstdc++ 的向量中抛出时。

如果像我一样,vector 是您选择的容器,那么这几乎没有用,因此您的代码中有大约 3,640 个位置可能会被抛出。

获得一个有用的例外的基础是:

  • 精确消息:"access to index 32 in vector of size 4" 更有帮助,不是吗?
  • 调用堆栈:虽然它需要特定于平台的代码来检索它,但可以自动插入到你的基础异常构造函数中,所以去吧!李>

注意:一旦你的异常中有一个调用堆栈,你很快就会发现自己上瘾了,并且将能力较差的 3rd 方软件包装到适配器层中,如果只是为了将它们的异常转换为你的异常;我们都做到了;)

在这些基础之上,RAII 有一个非常有趣的特性:在展开期间将注释附加到当前异常。保留对变量的引用并检查异常是否在其析构函数中展开的简单处理程序通常只花费一次if 检查,并且在展开时会执行所有重要的日志记录(但是,异常传播已经很昂贵了,所以。 ..)。

最后,您还可以丰富并重新抛出 catch 子句,但这很快就会在代码中添加 try/catch 块,因此我建议改用 RAII。

注意:std 异常不分配内存是有原因的,它允许在 throw 本身不被 std::bad_alloc 抢占的情况下抛出异常;我建议有意识地选择具有更丰富的异常,在尝试创建异常时可能会抛出std::bad_alloc(我还没有看到发生)。你必须做出自己的选择。

还有延迟记录?

延迟记录背后的想法是,您不会像往常一样调用您的日志处理程序,而是推迟记录所有更细粒度的跟踪,并且仅在出现问题(也称为异常)时才访问它们。

因此,我们的想法是拆分日志记录:

  • 立即记录重要信息
  • 更细粒度的信息被写入暂存器,在出现异常时可以调用它来记录它们

当然有问题:

  • 在崩溃的情况下,便签本(大部分)丢失;如果你得到一个内存转储,你应该可以通过你的调试器访问它,虽然它不是那么愉快。
  • 便笺本需要一个策略:何时丢弃它? (会话结束?事务结束?...),有多少内存? (随心所欲?有界?...)
  • 什么性能成本:即使不将日志写入磁盘/网络,格式化它们仍然需要成本!

我实际上从未使用过这样的便签本,因为现在我曾经遇到的所有非崩溃错误都是仅使用 I/O 日志记录和丰富的异常来解决的。不过,我是否应该实施它,我会推荐它:

  • 本地事务:由于记录了 I/O,因此我们不需要更多了解这一点
  • 内存受限:随着我们的进展逐出旧痕迹
  • 日志级别驱动:就像常规日志记录一样,我希望能够只启用 一些 日志进入便笺簿

还有条件/概率日志记录?

每 N 写一条迹线并不是很有趣;它实际上比任何事情都更令人困惑。另一方面,每 N 次深入记录一个事务会有所帮助!

这里的想法是总体上减少写入的日志数量,同时仍然有机会在野外详细观察错误痕迹。减少通常是由日志基础设施限制(传输和写入所有这些字节需要成本)或软件性能(格式化日志会减慢软件速度)驱动的。

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

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

关于rr的快速说明

只有 20% 的开销,而且这个开销仅适用于 CPU 处理,实际上可能值得系统地使用 rr。但是,如果这不可行,则可以在rr 下启动 N 台服务器中的 1 台并用于捕获难以发现的错误。

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

哦,在一般情况下,当您不寻找任何东西时,它可以很容易地完全停用。那么支付那 20% 是没有意义的。


就是这样

我可以为冗长的阅读道歉,但事实上我可能只是略过了这个话题。错误恢复是困难。我会感谢 cmets 和评论,以帮助改进这个答案。

【讨论】:

    【解决方案2】:

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

    当应用程序在生产环境中崩溃时,您应该获取尽可能多的信息以进行事后分析(您的第二种解决方案)。

    也就是说,如果您在生产环境中遇到不可恢复的错误,主要问题是您的产品 QA 流程(缺少该流程),以及(远在此之前)编写不安全/未经测试的代码。

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

    【讨论】:

    • 如果损坏是未初始化的指针写入,请检查您的代码库并初始化所有指针等等 =>这仍然是一个修复,以防止发生此类崩溃将来意味着设置定期检查以确保不再发生这种情况。通过投资静态分析和/或在 Valgrind(或其他内存调试工具)下运行测试套件。
    猜你喜欢
    • 2018-05-22
    • 2011-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-08
    • 2016-06-14
    • 2010-09-05
    • 2012-01-25
    相关资源
    最近更新 更多