Sentry 是一个问题追踪和性能监控的三方平台,很多国外的公司都在使用它来监控线上问题,比如 Github, Disney, Slack 等等。它支持各种语言,框架以及集成服务

同时 Sentry 的大部分服务以及 SDK 都进行了开源,我们可以从其文档和具体的代码中看到其相应的设计和实现。
下面为官方提供的系统架构:

以一个 crash 为例来看下其数据流转的过程:
- 客户端 SDK 上报 crash 日志
- 通过 nginx 将其传递到 relay 再传递到 kafka 服务中
- Ingest consumer 会首先消费这个事件,将其进行符号化并做一些其他的处理
- Snuba (Sentry 的搜索服务) 也有相应的 consumer 来对其进行消费并最终将其存储到 clickhouse 中方便之后的查询
这篇文章我们会以 iOS crash 处理的相关流程来简析 Symbolicator 服务,了解 Sentry 符号化服务的一些设计以及实现细节。
Symbolicator
Symbolicator 是一个独立的服务,解决了函数名,文件位置和源上下文堆栈跟踪。它可以处理 Minidumps 和苹果崩溃报告。此外,Symbolicator 可以作为代理服务器支持多种格式的服务,如微软的符号服务器或Breakpad 符号仓库。
Symbolicator 使用 Rust 编写,其内部依赖了 symbolic.
Symbolic 支持:
- 解析和生成一些符号缓存文件
- Demangle C++/OC/OC++/Rust/Swift
- 通过 sourcemap 展开 Javascript
- 反混淆
- Minidump / Breakpad 处理
- 通过 FFI 支持 C,Python 调用
- 还能处理 UE4 崩溃日志
通常我们会上传一些系统以及我们应用的符号表文件,Sentry 支持多种数据源 (比如 Sentry 自己的文件服务器或者 AWS S3) 的配置,之后它会去相应的服务下载这些符号文件并通过 symbolic 解析之后做统一的处理,生成一些缓存文件以及元数据方便之后符号化使用。
符号文件的处理
Symbolicator 服务为了抹平不同平台差异并且解决符号文件过大的问题,创建了 SymCaches。这是一种自定义格式的二进制文件,会以统一的格式存储原始调试信息的子集。而且这种文件格式是支持磁盘内存映射和方便快速寻址的。
Symbolicator 的 symsorter 主要负责这些文件的处理及排序,其支持代码调用及 CLI command 执行。
sort_files
函数支持传入配置和相应的文件路径列表来进行处理,下面是 config 定义:

方法执行时会遍历传入的文件路径,并创建 ByteView (通过 mmapping 从磁盘映射到内存) 来读取相应的文件,然后会根据其文件格式做不同方式的处理,如果是 zip,会读取其中数据并调用 process_file
来进行处理,这个方法会主要进行下面的工作:
- 解析符号对象 objects
- 遍历所有的 objects 并根据配置为其创建相应的目录或文件,保存元数据文件
- 根据压缩等级对数据进行相应的压缩和写入
- 最终将所有的 object id 和类型以数组形式返回出去
如果不是 zip 直接是 object 文件的话,也会调用 process_file
处理并解构返回的结果,根据其返回的对象类型来插入到一个 source_candidates 的 hashmap 中。同时对于上面处理的所有 id 会存储到一个 debug_ids 的数组中。
接下来如根据 with_sources 的配置来继续对 source_candidate 进行处理,将上面存入 map 的文件路径形式的 value 再进行 process_file
处理,确保所有的 debug 符号都被处理并保存,最终会统计个数并保存一些元数据。

符号化过程
这个服务基于 axum 对外暴露了一些 api,其中 ‘/applecrashreport’ 接口就是用于处理 iOS crash report。

最终这个 handler 会去调用 parse_apple_crash_report
来创建符号化 request,再通过 do_symbolicate
方法来执行。
parse_apple_crash_report
这个方法会把请求数据进行序列化为指定的结构体 AppleCrashReport:

主要会对其中的各种信息进行读取和转换,比如 arch, 涉及到的 modules,system info, stacktrace 中的每条栈帧等等。
然后会对资源进行分配并借助 tokio 来创建 parse 请求对应的 future 任务以及相应的 measurement 并执行。
do_symbolicate
这个方法最外层也如 parse_apple_crash_report
一样会借助 tokio 对异步任务进行封装,实际处理符号化的过程在 do_symbolicate_impl
方法中。
do_symbolicate_impl
首先会调用 ModuleLookup 的 fetch_symcaches
和 fetch_sources
来执行关于缓存的逻辑,其中它们会遍历每个 thread 的 stacktrace 来查找每条栈帧里涉及到的 module 通过 instruction address 以及 address mode (绝对地址或者相对地址)

这些模块索引 (id) 会被存储到一个 hashset 中,然后对当前缓存里的所有模块进行遍历,该模块的索引没出现在这个 hashset,则会对该模块 debug_status 进行标注为 Unused。
以 fetch_symcaches
为例,如果命中则会创建 FetchSymCache 再通过 SymCacheActor 来执行这个操作。SymCacheActor 会获取匹配对象并返回最合适对象的元数据。这会从源中请求可用的匹配对象,然后查找元数据缓存中每个匹配对象的对象元数据。从数据缓存中请求对象元数据也会触发每个对象的下载,然后将其缓存。元数据本身被缓存在元数据缓存中,通常寿命更长。

fetch_sources
则会去相应的对象文件找到对象并进行缓存。
在两个方法确保将所有涉及到的对象缓存到内存中后会调用 symbolicate_stacktrace
来进行符号化,这个方法会直接以查表方式从缓存中还原符号,如果失败也会有 demangle 的处理逻辑。最终会把结果拼接成最终符号化后的 report。
总结
通常 iOS 这边的符号化会使用苹果提供的符号化工具 symbolicatecrash,或者一些基于 atos 的缓存方案,这些过于依赖 mac 系统而且官方工具符号化的性能较低,使得很多公司或者平台通常会使用非实时符号化的方案(批量处理或按需处理)。
我们可以通过使用 Sentry 的 Symbolicator 方案来摆脱上述限制,并且其也提供了对于内嵌 DWARF 的 dsym 文件的处理方式,符号缓存管理等等。通常我们可以在几 ms 内完成 crash 日志的符号化,这大大提升了符号化的效率,对于线上问题的实时归因和报警也给与了很大的帮助。