Appearance
Crash 分类治理体系
面对 600 版本爆发的 8844 例线上 crash,先把「一堆堆栈」收敛成三类根因,再为没有堆栈的「自杀」类建起三级 watchdog 和自动抓 trace 工具,最后把整套排查经验沉淀成方法论——覆盖 8+ 车型,单版本闭环 200+ 条。
单版本 crash 量8844 例
单版本问题闭环200+ 条
覆盖车型8+
黑屏自愈2 分钟
背景
小鹏 600 版本线上 crash 大量爆发——8844 例。没有分类、没有优先级,团队面对一堆堆栈无从下手。更麻烦的是,问题来自多条渠道:crash 监控平台的自动上报、市场反馈的车主投诉、实验室稳定性测试,车型横跨 E29、F01、H93、G01、E38、F30、D01、F57 等多个在售和在研型号,每条都要登记 VIN、版本、状态和负责人。
问题
- 量级压垮人肉排查:单版本 200+ 条 crash/黑屏/卡死记录,逐条看堆栈不可行。
- 最致命的一类没有堆栈:Unity「自杀」类问题进程直接退出,监控上只有一个退出事件,没有异常栈,传统 crash 分析手段全部失效。
- 现象和根因分离:黑屏、卡顿、重启看起来是三个问题,实际可能是一条链(详见 P0 SR 黑屏根因复盘)。
方案
第一步:先分类,再动手
mermaid
flowchart TD
A["收到 crash 记录"] --> B{"有异常堆栈?"}
B -->|"无, 进程直接退出"| C["Unity 自杀类<br>查 watchdog / fd / 心跳 trace"]
B -->|"有"| D{"堆栈落在哪一层?"}
D -->|"SRP 渲染回调"| E["渲染类<br>ASan 重编 native 定位"]
D -->|"字符串拼接"| F["XStringBuilder 溢出<br>改动态扩容"]
D -->|"第三方 SDK"| G["转第三方<br>附最小复现"]
C --> H["修复 → 验证 → 合入 → 关闭"]
E --> H
F --> H
G --> H优先级很朴素:自杀类影响面最大且没有现成线索,先打;有堆栈的按模块分发;确认是第三方的立刻转出,不恋战。
第二步:给「自杀」装上监控——三级自杀恢复
自杀的本质是 Unity 进程卡死或异常后被保护机制杀掉。既然系统要杀,那就让监控先于系统拿到现场。三级策略按触发条件分级:
| 级别 | 触发条件 | 动作 |
|---|---|---|
| 启动自杀 | 100 秒内未收到 UnityReady 信号 | 判定启动失败,触发自愈 |
| 链路卡顿自杀 | Android 发往 Unity 的信号超 10 秒无消费 | 判定链路卡死 |
| 心跳卡顿自杀 | UnityMain 主线程 20 秒无心跳 | 超 8s/12s 各 dump 一次 trace,20s 自杀 |
心跳这一级最关键:UnityMain 正常每秒发一个心跳包,8 秒没心跳先生成一个 trace 文件,12 秒再生成一个,20 秒才自杀——自杀前已经留下两份不同时间点的 UnityMain 堆栈(trace 落在 /data/Log/log0/subreality),根因定位从「猜」变成「对比两份堆栈看卡在哪」。
再叠加首帧健康检查(FNV-1a 哈希采样校验首帧数据完整性),整套监控实现了黑屏 2 分钟无人值守自愈:检测到异常 → 自杀 → 拉起重载 → 首帧校验通过,全程不需要人介入。
第三步:无限抓 trace 工具
对不能稳定复现的 crash,做了一个后台工具,crash 发生时自动抓取完整堆栈、寄存器状态、内存映射和最近 N 条日志,自动分类并上报飞书。思路是「不追复现,等它自己来」——台架和线上挂着工具跑,抓到即分析。
关键实现
crash 三分类的根因细节
类别 1:Unity 自杀(含 fd 泄露)。进程直接退出无堆栈。典型根因是 fd 泄露到系统上限后 Unity 内部主动退出。排查手段是查 fd 数量,系统侧一个经验阈值:SurfaceFlinger 的 fd 到 20000 以上就是泄露。修复上所有文件操作加 try-finally 保证 close。
类别 2:渲染 SRP 崩溃。在 ScriptableRenderPipeline 回调里 SIGSEGV,根因是不安全的 native 指针访问。用 AddressSanitizer 重编 native 模块定位 use-after-free。
类别 3:XStringBuilder 字符串溢出。日志拼接时固定缓冲区溢出破坏栈。修复:改动态扩容。
系统级异常排查关键字
治理过程中沉淀了一份「症状 → 日志关键字」对照,把系统层问题也纳入同一套方法论:
| 异常类型 | 排查关键字 / 手段 |
|---|---|
| Binder 通讯卡死 | kernel 日志搜 hwbinder|undelivered transaction_complete |
| 系统 hw binder 拥堵 | binder buffer 分配统计 |
| SurfaceFlinger fd 泄露 | 搜 DFX-SystemMonitor: /system/bin/surfaceflinger,fd > 20000 |
| 渲染线程卡死 | 大屏日志搜 OpenGLRenderer|Choreographer: Skipped 看帧延迟 |
| 低内存被杀 | lowmemorykiller 记录,对比自身与他应用内存 |
| onResume 超时被杀 | ScreenRootContainer: can not find surface, killing pid(resume 后 5s 内 surface 未创建即 kill) |
| Unity GC 死锁 | 多线程 block 在 GC_lock,Unity 引擎层问题 |
| GPU 硬件异常 | surfaceflinger 报 gpu_page_fault,kgsl 问题,换件解决 |
指标
- 8844 例 crash → 全量分析 + 分类完成,收敛为三类根因 + 第三方。
- 后续 8853 新一批 crash 快速收敛——按分类流程跑,大部分直接识别为第三方(百度)问题转出。
- 单版本闭环 200+ 条 crash/黑屏/卡死记录(crash 监控、市场反馈、实验室稳定性实验三渠道),覆盖 8+ 在售车型。
- 三级自杀恢复 + 首帧健康检查上线后,黑屏类问题 2 分钟内无人值守自愈。
- 沉淀「Unity 自杀 crash」分析 Skill,新人按 SOP 可独立排查。
复盘
- 治理的第一步永远是分类,不是修 bug。8844 例不分级就是 8844 个任务,分完类发现真正要自己修的根因只有三类。
- 没有堆栈的问题,要用监控换线索。自杀类 crash 的突破口不是更聪明的分析,而是 8s/12s 两份 trace 的设计——让现场自己留下来。
- 修复只是闭环的一半。每条记录有状态、有负责人、有版本归属,关闭前必须验证合入版本,否则「已解」只是表格里的自欺欺人。
- 第三方问题要快出证据。8853 那批能快速收敛,靠的是分类时顺手把「非我方」的判定证据(堆栈帧、最小复现)一起归档,转出去就回不来扯皮。