免责声明:很像服务器的 OP I 代码,因此整个答案都集中在这个特定的用例上。嵌入式软件或已部署应用程序的策略可能应该大不相同,不知道。
首先,这个问题有两个重要(而且相当不同)的方面:
让我们分别对待两者,因为分裂就是征服。让我们从更艰难的开始。
确保恢复
try/catch 的 C++/Java 风格的主要问题是它非常容易破坏您的环境,因为 try 和 catch 可以改变它们自己范围之外的内容。 注意:与 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 和评论,以帮助改进这个答案。