生产环境中的调试崩溃

Debugging crashes in production environments

本文关键字:崩溃 调试 生产环境      更新时间:2023-10-16

首先,我应该给您一点上下文。有问题的程序是一个相当典型的用C++实现的服务器应用程序。穿过项目以及所有底层库中,错误管理是基于C++异常的。

我的问题与处理不可恢复的错误和/或程序员错误——相当于"未检查"的Java例外,因为没有更好的平行。我特别感兴趣在生产中处理此类条件的常见做法中环境。

特别是对于生产环境,存在两个相互冲突的目标在存在上述错误类别的情况下:易于调试以及可用性(从操作性能的意义上讲)。每个这些反过来又提出了一个具体的策略:

  • 安装一个顶级异常处理程序来吸收所有未捕获的异常例外情况,从而确保连续可用性。不幸地这使得错误检查更加复杂,迫使程序员依赖细粒度日志记录或其他代码"检测"技术。

  • 尽可能用力撞击;这样就可以进行事后分析通过岩心分析导致误差的条件倾倒自然,必须为系统恢复提供一种方法事故发生后及时进行操作,这可能会很遥远来自琐碎。

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

注意:由于我感兴趣,我已将该问题标记为特定于C++特别适用于它的解决方案和特质;尽管如此,我知道与其他语言/环境。

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

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

  • 轻松调查(尽可能)
  • 确保恢复

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


确保恢复

try/catch的C++/Java风格的主要问题是,它极易损坏您的环境,因为trycatch可能会在其自身范围之外发生变化注意:与Rust和Go相比,在Rust和Go中,一个任务不应该与其他任务共享可变数据,fail会杀死整个任务,而没有恢复的希望

因此,有3种恢复情况:

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

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

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

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


简化调查

注意:我将借此机会推广Mozilla的一个名为rr的项目,该项目一旦成熟,将非常非常有助于调查。查看本节末尾的快速注释

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

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

  • 连续日志记录,这样当发生异常时,您就可以拥有尽可能多的上下文
  • 异常日志记录,以便在发生异常时,尽可能多地进行日志记录

日志记录持续意味着性能开销和(当一切正常时)无用日志的泛滥。另一方面,异常日志意味着对系统有足够的信任,以便在出现异常时执行某些操作(在bad_alloc的情况下…哦)。

总的来说,我建议两者兼而有之。

连续记录

每个日志应包含:

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

什么值得记录?

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

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

异常记录

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

如果像我一样,vector是您选择的容器,并且因此在您的代码中大约有3640个位置可能会引发这种情况,那么这是非常无用的。

为了得到一个有用的例外,基本的是:

  • 一条精确的消息:"access to index 32 in vector of size 4"稍微有点帮助,不是吗
  • 调用堆栈:它需要特定于平台的代码来检索它,但可以自动插入您的基础异常构造函数中,所以请使用它

注意:一旦你的异常中有了调用堆栈,你会很快发现自己上瘾了,并将功能较弱的第三方软件包装到适配器层中,哪怕只是将它们的异常转换为你的;我们都做到了;)

除此之外,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%是没有意义的。


所有人

我可以为冗长的阅读道歉,但事实上,我可能只是略读了这个话题。错误恢复很难。我将感谢各位的评论和意见,以帮助改进这一答案。

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

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

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

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