声明:和提问者一样,我也是为服务器编写代码的,因此本篇答案只关注此特定使用场景。嵌入式软件或部署应用程序的策略可能大相径庭,不了解。
首先,这个问题有两个重要(且非常不同)的方面:
让我们分别处理它们,因为分而治之更好。让我们从更困难的部分开始。
确保恢复
C++ / Java 风格的 try
/catch
的主要问题在于,它极易破坏您的环境,因为 try
和 catch
可以改变其自身范围之外的内容。注意:与 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%。
就这些了
我可以为篇幅冗长的阅读道歉,但事实上,我可能只是浏览了主题。错误恢复很难。我将感谢评论和备注,以帮助改进此答案。