Skip to content

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、版本、状态和负责人。

问题 ​

  1. 量级压垮人肉排查:单版本 200+ 条 crash/黑屏/卡死记录,逐条看堆栈不可行。
  2. 最致命的一类没有堆栈:Unity「自杀」类问题进程直接退出,监控上只有一个退出事件,没有异常栈,传统 crash 分析手段全部失效。
  3. 现象和根因分离:黑屏、卡顿、重启看起来是三个问题,实际可能是一条链(详见 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 可独立排查。

复盘 ​

  1. 治理的第一步永远是分类,不是修 bug。8844 例不分级就是 8844 个任务,分完类发现真正要自己修的根因只有三类。
  2. 没有堆栈的问题,要用监控换线索。自杀类 crash 的突破口不是更聪明的分析,而是 8s/12s 两份 trace 的设计——让现场自己留下来。
  3. 修复只是闭环的一半。每条记录有状态、有负责人、有版本归属,关闭前必须验证合入版本,否则「已解」只是表格里的自欺欺人。
  4. 第三方问题要快出证据。8853 那批能快速收敛,靠的是分类时顺手把「非我方」的判定证据(堆栈帧、最小复现)一起归档,转出去就回不来扯皮。

罗子初 · AI 座舱专家 · 8年端侧研发

Lv.1