1. 崩溃上报整体架构与数据流
1.1 架构概览:四层职责分离
崩溃上报系统并非简单的“捕获→发送”两步走,而是一条包含客户端初始化、采集队列、上报服务、存储链路四个核心段的完整流水线。每段承担独立职责,并通过明确定义的接口或协议衔接,便于隔离故障、横向扩展和独立演进。
设计原则: 每层只处理本层数据;层间通过队列或 RPC 通信,禁止跨层直调。
1.2 四层核心组件与数据流
| 层次 | 职责 | 关键组件 | 输出数据形态 |
|---|---|---|---|
| ① 客户端初始化 | 拉起 SDK、加载配置、挂载信号/异常处理器 | SignalHandler、CrashConfig | 处理器上下文 + 配置对象 |
| ② 采集队列 | 接收原生崩溃数据,序列化、压缩、保存到本地队列 | CrashCollector、RingBuffer、LocalStorage | 序列化后的二进制或 JSON 包 |
| ③ 上报服务 | 从队列拉取数据,执行重试/退避策略,发送至远端 | UploadService、RetryPolicy、HttpClient | HTTP(S) 请求体 + 元数据 |
| ④ 存储链路 | 服务端接收、校验、解析、持久化,并触发告警 | CrashServer、Parser、DB (如 ClickHouse) | 结构化事件记录 |
1.3 客户端初始化流程
- SDK 加载: 应用启动时通过
dlopen或静态链接加载崩溃采集动态库。 - 配置读取: 读取
crash_config.json(路径由环境变量CRASH_CFG_PATH指定),包含队列大小、超时、最大重试次数。 - 信号处理挂载: 调用
sigaction接管SIGSEGV、SIGABRT、SIGBUS等信号。 - 线程空间初始化: 分配独立栈(
sigaltstack)用于崩溃时安全调用。 - 回执测试: 通过模拟崩溃回执确认链路可用(可配置、生产环境默认关闭)。
初始化 SDK 示例(伪代码):
crash_handle_t handle = crash_init("crash_config.json");
crash_set_callback(handle, on_crash_callback);
crash_install_signal_handlers(handle, SIGSEGV | SIGABRT);
1.4 采集队列工作机制
崩溃发生后,信号处理函数会在独占栈上运行。由于此时堆内存可能已损坏,队列采用预分配环形缓冲区(RingBuffer)存储原始数据,避免二次崩坏。
- 写入阶段: 崩溃上下文(寄存器、堆栈指针、信号信息)被压入固定大小的内存块。
- 转储阶段: 应用重启后,启动器扫描本地临时目录,将 RingBuffer 内容序列化为 JSON 协议包,写入磁盘队列。
- 队列结构:
| 字段 | 类型 | 说明 |
|---|---|---|
| crash_id | uint64 | 崩溃唯一标识(时间戳 + 随机数) |
| timestamp | uint64 | 崩溃发生时刻(毫秒) |
| signal_num | int32 | 信号编号 |
| raw_payload | bytes | 经过 LZ4 压缩的堆栈二进制帧 |
| upload_status | enum | pending | uploading | uploaded | failed |
1.5 上报服务与重试角色
上报服务(UploadService)是状态机最密集的一层。每次从队列取出一个 crash 记录后,进入重试轮询:
- 构造 HTTP POST 请求,Body 为 JSON 协议包。
- 发送至服务端点:
POST /v1/crash/report。 - 接收响应。若 HTTP 状态码为 2xx,标记为
uploaded并从磁盘队列删除。 - 若状态码为 4xx 或 5xx,根据退避策略等待后重新入队。
- 达到最大重试次数(默认 5)后,标记为
failed并归档。
重要约束: 上报服务不得修改已经序列化的
raw_payload,只负责传输和状态转换。重试间隔使用指数退避(Exponential Backoff)+ 随机抖动。退避配置参见 retry_policy.yaml。
1.6 存储链路(服务端)
存储链路是上报服务的消费者,包含以下处理环节:
- 接入层: 通过 Nginx/OpenResty 进行验签、限流(每秒每客户端 10 次)。
- 解析层: 解析 JSON 协议包,执行堆栈符号化(调用
addr2line或读取 .debug 文件)。 - 持久化: 写入 ClickHouse 表
crash_reports,按timestamp分区。 - 告警: 若同
crash_id出现频次超过阈值(如 100 次/分钟),触发 P0 级钉钉/邮件告警。
存储表结构(核心列):
CREATE TABLE crash_reports (
crash_id UInt64,
app_ver String,
signal_num Int32,
raw_payload String,
city String,
reported_at DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(reported_at)
ORDER BY (reported_at, crash_id);
此表设计支持每秒数万条写入,实际生产可配合 Kafka 做削峰填谷。
1.7 性能与稳定性关注点
- 信号安全: 仅使用 Async-Signal-Safe 函数(
write、sigaction、snprintf),禁用malloc、printf、锁。 - 队列写放大: 崩溃上下文体积通常 < 4KB,但符号化信息可达 50KB。采集队列阶段只保留压缩后的托管栈,避免 IO 溢出。
- 上报并发: 每个客户端启动一个独立线程池(默认 2 个线程)执行上传,防止阻塞主业务线程。