Bugsnag 最近增加了对处理小型转储的支持,以便客户可以跟踪Electron上的本地崩溃或使用Breakpad或Crashpad时产生的崩溃。
添加 minidump 支持带来了许多技术挑战,我们必须解决这些挑战,以确保我们能够以高效、可扩展的方式处理文件,而不会影响我们正常的错误事件处理吞吐量。
minidump 是一些进程在崩溃时生成的文件。它们比核心转储小,只提供执行基本调试操作所需的数据。
当操作系统遇到意外错误时,发明了 minidump 文件格式以在 Windows 中使用。随后,Google 的 Breakpad 和 Crashpad 等工具采用相同的格式在多个平台上生成应用程序崩溃转储。使用 Electron 构建的应用程序还会为本地崩溃生成 minidump 文件(因为 Electron 使用 Crashpad)。
minidump 文件包含有关崩溃时进程的信息,并包含以下详细信息:
在 minidump 中,每个活动线程(发生错误时)都有一个运行时堆栈以及这些线程的寄存器值。
这些细节可用于遍历堆栈并为每个线程生成堆栈跟踪。在某些情况下,单独使用此方法可以获得有效(尽管未符号化)堆栈跟踪,但在其他情况下,我们需要在调试文件中提供额外的调用帧信息 (CFI)。
调用帧信息 (CFI) 记录描述了如何恢复特定机器地址处的寄存器值。此数据通常在调试文件中提供,并允许在遍历堆栈时生成更可靠的堆栈跟踪。
如果没有 CFI 信息,堆栈遍历器可能需要扫描堆栈以尝试找到调用帧,但这可能不太可靠并且可能会产生无效的堆栈跟踪。
一旦获得堆栈跟踪,它将只包含每个帧的地址。为了产生有意义的堆栈跟踪(每个帧的函数名、源文件和源行号),我们需要对其进行“符号化”,即将调试符号应用于它们。
来自编译器的相关调试文件(例如 macOS 的 dSYMs)可以用于此,但 Breakpad 已经定义了自己的符号文件格式,可以代替使用。
Breakpad 符号文件比大多数编译器生成的调试文件更简单(它不包含诸如抽象语法树之类的详细信息),但以可读的格式提供了所需的详细信息,以符号化堆栈跟踪。
Breakpad 提供了许多工具来帮助手动处理小型转储或将它们上传到专用服务器/服务进行处理:
minidump_stackwalk接收一个 minidump 文件和可选的 Breakpad 符号文件,并输出每个线程的堆栈跟踪以及其他信息(例如崩溃的原因、每帧的寄存器值、操作系统的详细信息等)。这是解析小型转储并从中获取有意义的堆栈跟踪的有用工具。
minidump_dump提供有关 minidump 的更多详细信息(例如 minidump 中每个流的详细信息)。
minidump_upload将 minidump 文件上传到专用服务器进行处理(例如 Bugsnag)。
dump_syms从应用程序二进制文件和调试文件生成 Breakpad 符号文件。
symupload 将Breakpad 符号文件上传到专用服务器进行处理(例如 Bugsnag)。
带有相关符号文件的 minidump_stackwalk 工具的输出示例如下所示:
-- CODE language-plaintext -- 操作系统: Windows NT 10.0.19041 1151CPU: amd64 family 23 model 24 stepping 1 8 CPUs
GPU:未知
崩溃原因:未处理的 C++ ExceptionCrash 地址:0x7ff887f54ed9进程正常运行时间:4 秒
线程 0(崩溃)0 KERNELBASE.dll + 0x34ed9
rax = 0x655c67616e736775 rdx = 0x6f6d2d6576697461 rcx = 0x6e2d7070635c656d rbx = 0x00007ff87f731600 rsi = 0x000000b80f3fcb70 rdi = 0x000000b80f3fca40 rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc8d0 r8 = 0xaaaaaaaa0065646f r9 = 0xaaaaaaaaaaaaaaaa r10 = 0xaaaaaaaaaaaaaaaa r11 = 0xaaaaaaaaaaaaaaaa r12 = 0x00007ff87f6f1ba0 r13 = 0x000010ff084af60d r14 = 0xffffffff00000000 r15 = 0x0000000000000420 rip = 0x00007ff887f54ed9 Found by: given as instruction pointer in context 1 KERNELBASE.dll + 0x34ed9 rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc908 rip = 0x00007ff887f54ed9 Found by: stack scanning 2 ntdll.dll + 0x34a5f rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc960 rip = 0x00007ff88a6a4a5f Found by: stack scanning 3 my_example.node!CxxThrowException [throw.cpp : 131 + 0x14] rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc9b0 rip = 0x00007ff87f6fab75 发现者:堆栈扫描 4 my_example.node!RunExample(Nan::FunctionCallbackI nfo v8::Value const &) [my_example.cpp : 26 + 0x22] rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fca20 rip = 0x00007ff87f6f1ec2 发现者:调用帧信息..
在向 Bugsnag 添加 minidump 支持时,我们面临一些技术挑战。
Minidump 与我们通常的错误事件 JSON 有效负载有很大不同,因此我们必须确保我们能够以高效、弹性和可扩展的方式处理它们。
Minidump 文件可能比我们收到的正常有效载荷大得多;我们正常的有效载荷平均约为 20KB,而小型转储的大小通常为数百 KB。此外,如果有很多活动线程或具有大堆栈的线程,小型转储可能会变得非常大(数十兆字节)。
通常,当我们收到错误事件有效负载时,我们会将它们添加到 Kafka 队列以进行异步处理,以便我们能够处理任何上传的积压。
如果我们要排队更大的小型转储文件,我们需要确保排队机制是可靠的。 minidump 文件压缩得很好(通常压缩到其原始大小的 10% 左右),但仍然存在压缩文件太大的风险。
我们在内部 Kafka 实例上对各种大小的消息进行了一些负载测试,发现:
数据吞吐量和复制延迟并没有真正受到文件大小的影响。
随着平均文件大小的增加,每秒可处理的消息数减少。
这种降低的消息吞吐量仅在同时排队大量特别大的文件时才显着,但我们预计这些应该非常罕见,因此 Kafka 将适合此目的。
当使用 Breakpad 的 minidump_stackwalk 工具对 minidump 进行符号化时,如果提供了 Breakpad 符号文件,处理 minidump 可能需要更长的时间(由于加载符号文件、解析它们并查找相关符号数据需要时间)。
在一个示例电子小型转储上,没有 Breakpad 符号文件需要 20 毫秒,而使用它们需要 14 秒!
在手动符号化单个小型转储时,较慢的处理时间不是太大问题,但我们需要确保我们可以尽可能高效地处理和符号化小型转储,以便我们可以保持小型转储处理的高吞吐量。
为了实现这一点,我们实现了自己的符号逻辑。 Breakpad 符号文件格式简单且有据可查,这意味着我们可以解析文件以生成定制的映射文件,以便轻松查找地址。
定制文件比 Breakpad 符号文件大很多,但执行查找的效率也更高。预先对 Breakpad 符号文件进行这种预处理意味着处理小型转储所需的时间显着减少(以增加符号文件的存储需求为代价)。
在 minidump 处理的初始设计中,我们在遍历堆栈时完全省略了 Breakpad 符号文件的使用(以提高效率),但我们很快意识到,由于缺少调用帧信息数据,这有时会导致无效的堆栈跟踪。
我们知道,如果我们为堆栈遍历传入完整的 Breakpad 符号文件,它会很慢(因为它也试图符号化堆栈跟踪),因此我们选择生成只包含文件的精简版本遍历堆栈所需的信息。
这大大减少了 Breakpad 符号文件的大小以及处理 minidump 所需的时间,但它仍然不是很有效(例如 Electron minidump 需要 1.5 秒)。
因此,我们探索了序列化修剪后的 Breakpad 符号文件的选项,以便可以更有效地读取它(而不是每次都解析文件)。使用文件的序列化版本将处理时间从 1.5 秒减少到 200 毫秒。
这种性能改进意味着我们应该能够支持每个服务实例更高的小型转储吞吐量,这意味着我们可以降低基础设施成本。
随着新功能采用的增加,我们将密切监控基础设施的使用情况,以确保我们继续以有效的方式处理小型转储,并查看是否有任何其他性能改进。
Bugsnag 可帮助您优先考虑和修复软件错误,同时提高应用程序的稳定性。