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

sentry-supports

同时 Sentry 的大部分服务以及 SDK 都进行了开源,我们可以从其文档和具体的代码中看到其相应的设计和实现。

下面为官方提供的系统架构:

sentry-arch

以一个 crash 为例来看下其数据流转的过程:

  1. 客户端 SDK 上报 crash 日志
  2. 通过 nginx 将其传递到 relay 再传递到 kafka 服务中
  3. Ingest consumer 会首先消费这个事件,将其进行符号化并做一些其他的处理
  4. 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 定义:

SortConfig

方法执行时会遍历传入的文件路径,并创建 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 符号都被处理并保存,最终会统计个数并保存一些元数据。

sort-files-last

符号化过程

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

route

最终这个 handler 会去调用 parse_apple_crash_report 来创建符号化 request,再通过 do_symbolicate 方法来执行。

parse_apple_crash_report

这个方法会把请求数据进行序列化为指定的结构体 AppleCrashReport

apple-crash-report

主要会对其中的各种信息进行读取和转换,比如 arch, 涉及到的 modules,system info, stacktrace 中的每条栈帧等等。

然后会对资源进行分配并借助 tokio 来创建 parse 请求对应的 future 任务以及相应的 measurement 并执行。

do_symbolicate

这个方法最外层也如 parse_apple_crash_report 一样会借助 tokio 对异步任务进行封装,实际处理符号化的过程在 do_symbolicate_impl 方法中。

do_symbolicate_impl 首先会调用 ModuleLookupfetch_symcachesfetch_sources 来执行关于缓存的逻辑,其中它们会遍历每个 thread 的 stacktrace 来查找每条栈帧里涉及到的 module 通过 instruction address 以及 address mode (绝对地址或者相对地址)

get-module-by-addr

这些模块索引 (id) 会被存储到一个 hashset 中,然后对当前缓存里的所有模块进行遍历,该模块的索引没出现在这个 hashset,则会对该模块 debug_status 进行标注为 Unused。

fetch_symcaches 为例,如果命中则会创建 FetchSymCache 再通过 SymCacheActor 来执行这个操作。SymCacheActor 会获取匹配对象并返回最合适对象的元数据。这会从源中请求可用的匹配对象,然后查找元数据缓存中每个匹配对象的对象元数据。从数据缓存中请求对象元数据也会触发每个对象的下载,然后将其缓存。元数据本身被缓存在元数据缓存中,通常寿命更长。

fetch-sym-cache

fetch_sources 则会去相应的对象文件找到对象并进行缓存。

在两个方法确保将所有涉及到的对象缓存到内存中后会调用 symbolicate_stacktrace 来进行符号化,这个方法会直接以查表方式从缓存中还原符号,如果失败也会有 demangle 的处理逻辑。最终会把结果拼接成最终符号化后的 report。

总结

通常 iOS 这边的符号化会使用苹果提供的符号化工具 symbolicatecrash,或者一些基于 atos 的缓存方案,这些过于依赖 mac 系统而且官方工具符号化的性能较低,使得很多公司或者平台通常会使用非实时符号化的方案(批量处理或按需处理)。

我们可以通过使用 Sentry 的 Symbolicator 方案来摆脱上述限制,并且其也提供了对于内嵌 DWARF 的 dsym 文件的处理方式,符号缓存管理等等。通常我们可以在几 ms 内完成 crash 日志的符号化,这大大提升了符号化的效率,对于线上问题的实时归因和报警也给与了很大的帮助。