调试在生产环境中崩溃

时间:2014-03-19 16:11:25

标签: c++ debugging production

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

我的问题与处理不可恢复的错误和/或相关 程序员错误---松散相当于“未经检查”的Java 例外,因为缺乏更好的并行。我特别感兴趣 在生产中处理这些条件的常见做法 的环境中。

特别是对于生产环境而言,有两个相互冲突的目标 出现上述类错误:易于调试 和可用性(在操作性能方面)。每个 这些反过来暗示了一个具体的策略:

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

  • 尽可能地崩溃;这使得人们可以进行验尸 分析通过核心导致错误的条件 倾倒。当然,必须为系统提供恢复的手段 崩溃后及时操作,这可能是远的 来自琐事。

所以我最终得到了两个半生不熟的解决方案;我想妥协一下 服务可用性和调试工具之间。我是什么 失踪?

注意:我已经将问题标记为C ++特定的,因为我感兴趣 解决方案和适用于它的特性; 尽管如此,我知道其他人会有很多重叠 语言/环境。

2 个答案:

答案 0 :(得分:1)

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

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

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

让我们分开对待,因为分裂是征服。让我们从更艰难的位置开始吧。


确保恢复

C ++ / Java风格try / catch的主要问题是,它很容易破坏您的环境,因为trycatch可以改变外面的内容他们自己的范围。 注意:与Rust和Go形成对比,其中任务不应与其他任务共享可变数据,而fail将无法恢复整个任务。

因此,有3种恢复情况:

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

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

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

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


缓解调查

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

毫不奇怪,为了调查您将需要数据。优选地,尽可能地,并且有序/标记。

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

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

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

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

连续记录

每个日志应包含:

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

什么值得记录?

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

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

例外记录

另一种选择是丰富异常。作为原始例外的示例:std::out_of_range从libstdc ++的向量中引出以下原因(来自what):vector::_M_range_check

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

获得有用例外的基础是:

  • 精确消息:"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次记录一次深度交易可以提供帮助!

这里的想法是减少一般写入的日志量,同时仍然有机会在野外详细观察错误痕迹。减少通常由日志记录基础结构限制(传输和写入所有这些字节的成本)或软件性能(格式化日志会降低软件速度)驱动。

概率测井的想法是“翻转硬币”#34;在每个会话/交易开始时,决定它是快速的还是慢的:)

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

关于rr

的快速说明

只有20%的开销,并且此开销仅适用于CPU处理,实际上值得系统地使用rr。但是,如果这不可行,那么在rr下启动N个服务器中的1个并且用来抓住很难找到错误是可行的。

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

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


那是所有人

我可以为冗长的阅读道歉,但事实上我可能只是略读了这个主题。错误恢复 hard 。我希望得到评论和评论,以帮助改进这个答案。

答案 1 :(得分:0)

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

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

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

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