得物之前对于线上的ANR问题都是采用接入的Bugly平台进行问题收集和平台展示的,在体验中发现,无论是提供的信息对于解决问题的有效度、或者是对ANR日志聚合,都不太符合得物实际场景的需要。因此得物在参考业界各大厂商分享的ANR监控及治理方案后,我们开始着手建设自己内部的ANR监控平台,本文主要介绍 ANR监控 平台部分的实现,端上相关信息的采集实现不做阐述。
ANR 信息的处理
在进行ANR日志的数据处理之前,首先整理下得物目前对于ANR收集、上报的信息内容,主要包括以下几点信息
- ProcessErrorStateInfo
- tomstone信息
- 主线程的堆栈采样信息
- 主线程 Looper Message 历史调度的Message信息、当前处理的Message、未处理的Message队列
App状态信息
- 应用前后台状态
- 应用运行时长
- ...
以下针对上述几个关键信息的处理,进行说明。
ProcessErrorStateInfo解析
ProcessErrorStateInfo 是通过ActivityManager的getProcessesInErrorState()函数获取的,主要包含以下信息
- processName
- pid
- uid
- tag (如果ANR发生在 Activity、Service等系统组件上,则tag会被设置为对应的组件名)
- shortMsg (ANR 信息的简短描述)
- longMsg (ANR 的详细信息)
这里主要关注的时longMsg的信息, 通过正则匹配的方式,从 longMsg中可以解析出以下关键信息
- ANR 发生的组件
- ANR 触发的原因
tomstone 信息解析
关于 Android tombstone文件包含的信息可以参考 debuggerd中 tombstone的相关代码 ,得物目前在线上采用的是iQiyi开源的xcrash进行ANR tomsbtone信息的采集。 采集的tombstone文件中主要包含以下信息
Crash相关元信息
- Crash类型 (anr、java 、native)
- 应用启动的时间
- Crash 的时间
- 应用版本号
- 系统版本
- ABI
- pid、tid、crash 线程名
- 虚拟机GC相关信息
ANR时各线程的现场信息
- 线程当前调用栈
- 线程优先级 prio
- 线程状态
- 持有、或者等待的锁信息
- 线程 nice值、utm、stm等信息
- logcat信息
- open files
- memory info
- ....
对xcrash的 tombstone信息的解析,也直接采用的是 xcrash中内置的代码实现。
Looper Message回溯信息的解析
在LooperMessage回溯 是指对系统主线程Looper 对消息记录的历史(也包括未处理的消息记录),从Message的处理状态上分类,包含三类Message,1. 已处理的Message 2.当前正在处理的Message 3.未处理的Message;
对于已处理的Message,采集了Message执行的耗时时间(wallTime),以及cpu分配执行的时间(cpuTime),根据这两个时间的可以大致判断,主线程cpu的调度情况,如果两者之间的差值较大 (cpuTime 只占 wallTime的30%以下) ,考虑可能存在比较严重的线程调度抢占问题。
对于未处理的Message,主要是可以用来观测当前Message的调度延迟情况、是否存在消息积压过多等case。
堆栈采样信息(method trace)解析
得物会在App运行时,每隔一定的时间间隔(50ms)采集主线程当前的执行堆栈,总计会保存10S~20S 的线程调用栈,当发生ANR时,采集的堆栈信息会随ANR信息一起上报。
通过在后台对采集的堆栈进行处理,可以预先分析出其中较为耗时的函数,在这里简单描述下对函数采样数据的分析处理方式
- 首先将采集的堆栈遍历解析,并按照函数的调用关系转成NodeTree的形式,遍历过程中,记录每个函数的耗时
- 从根节点开始,层级遍历 MethodNodeTree,找出所有耗时大于一定阈值的函数
- 过滤掉一些白名单函数,比如 com.android.internal.os.ZygoteInit.main 、android.app.ActivityThread.main
部分代码
public class MethodNode {
private List children = new ArrayList<>();
//method cost time
private int cost;
private String fullMethodName;
private JavaStackFrameSnapshot javaStackFrameSnapshot;
}
ANR 日志的聚合
在APM平台中,对于issue 类型的事件,一个很重要的处理环节 就是对数据的聚合过程。 对于端上上报的 ANR 日志同样也需要进行聚合处理,这样可以更好的对ANR问题进行分类,评估不同问题在线上发生ANR的严重程度,根据严重程度进行集中治理。
聚合策略
在生产环境中,设备发生ANR的原因多种多样,简单归类的话可以分为以下几种情况
主线程慢函数执行导致
- 比如主线程执行繁重的代码,比如对大数据量的排序
- 主线程上执行 IO操作
- Looper 消息调度异常
- 锁等待、死锁
- 系统负载较高、当前进程的CPU调度受到影响
- 系统binder 服务异常
- ....其他异常情况
针对可能因为慢函数导致的ANR问题,通过上节的 “堆栈信息解析”环节已经解析出所有耗时大于一定阈值的应用内函数,在符合一定条件的情况下(主线程cpu调度正常) 可以按照 耗时函数进行聚合。
对于其他的case,我们先简单的按照 发生ANR的组件+ANR触发的原因 两个关键信息进行聚合,先按照这种比较粗的粒度进行聚合,再进行分析,如果发现一定的规律后,可以在平台支持一些 特定规则的自定义聚合能力进行数据聚合。
平台体验
这个章节分享的是,得物针对ANR问题在平台功能上的体验设计,重点主要是说明如何展现现有的这些数据信息,帮助开发者更好的发现及定位问题。
对于每一个上报的ANR日志来说,除了一些通用的埋点信息外(如userId、sessionId、deviceOs等),重点是对于ANR该问题相关的上下文信息的展示。
问题列表
首先ANR日志在经过聚合流程后,被分成一个个ANR ISSUE,每个ISSUE都有单独的处理状态及趋势 ,以便进行问题治理及问题趋势的跟进
在应用整体上,也提供了相应维度的趋势图可以观测目前线上ANR的情况。
问题详情
在问题列表页,可以大概概念当前ANR 有多少个问题,每个问题的趋势及处理状态,当点击每个问题时,会跳转到该“聚合问题”的详情页,在聚合算法实现较为完备的情况下(在实际场景中,以ANR目前原因的多样性,较难达到),每个问题详情都是一个特定原因导致的ANR、或者是发生在某一个特定的应用组件上的ANR。
在问题详情页,同样可以展示该特定问题的发生趋势,以及对问题处理状态等,但更为重要的是 这个问题,平台提供了哪些关键元素信息来帮助开发者定位ANR问题。
日志详情
在每个ANR ISSUE详情页中,每个ISSUE 都是一堆ANR上报的日志集合,即ISSUE 跟 Log,是一个一对多的关系,在问题详情页,可以查看该问题下面上报的所有异常日志,点击每个异常日志时,可以查看该日志的详细信息。在日志详情部分,ANR平台将上个小节 解析的所以信息,包括主线程 Message回溯信息、method trace信息,各进程CPU使用率等,以更友好的可视化方式进行展示。
ANR日志 基础信息的展示
- ANR 触发的组件
- ANR触发的原因
- ANR 发生时的应用的前后台状态
- 应用运行时长
- ....
ANR后一段时间各进程CPU的使用率信息
这里主要是展示端上收集上来的 各进程的cpu使用率信息,使用率上分为用户态和内核态。
在这里主要查看两个方面的信息,当前进程的cpu使用率有无异常,以及在用户态和 内核态的分布情况。另外一个方面是查看其它进程的cpu使用率,考虑是否可能出现由于其它进程cpu使用率过高,而影响当前进程的情况。
ANR 时各线程现场信息展示
线程现场信息,主要是查看 主线程是否有可能发生死锁、以及其它线程的运行情况,是否有可能出现其它工作现场过多,导致当前线程调度受影响的情况等。
Looper消息回溯信息的展示
Looper消息的回溯是可以从主线程处理Message的视角看消息的分发、处理情况。ANR发生的原因除了耗时函数外,也可能是由于主线程CPU的调度受到了瓶颈、或者是向Looper发送了过多消息导致等诸多原因。
logcat 信息
从logcat信息中,可以辅助验证、获取ANR发生前后的一些信息,有时候从logcat中的异常日志中也能定位出问题。
函数执行火焰图(Flame Chart)的展示
火焰图可以以全局的视角来看应用函数执行的情况,更直观的分析出可能导致出现anr的函数。
使用ANR监控平台排查问题的思路
简单来说,通过现有的平台,可以从以下几个方面来分析ANR问题
- 从ANR的前后台状态、应用运行时长、进程cpu使用率、Message Trace 中Message 的cpuTime、wallTime的差值 大概对当前应用的cpu调度能力有一个评估。
- 从火焰图中 分析是否有明显耗时的业务代码
- 从 MessageTrace中看是否有明显异常的消息处理(单个消息处理过长、Message数过多 等情况)
从CPU使用率上
- 看 应用进程user和kernel的使用占比有无异常,如果kernel使用较高,则分析是否发生了大量的系统调用, 如果user层的使用率较高,则说明在业务侧有较多繁重的任务执行。
- 查看 kswapd0 cpu使用率是否较高,如果cpu使用率比较高,则说明系统内存资源不足,此时系统正在进行频繁的内存交换 (内存空间和磁盘空间的交换),占用系统的cpu及io资源,因此会影响整机性能。
- 查看 systemServer的cpu使用率,是否是由于系统服务的cpu使用率过高,导致当前进程无法分配到足够的时间片
从logcat排查问题
- 从logcat中观察 gc相关的信息,是否有可能是由于内存严重不足,执行blocking gc导致的ANR,这里也可以结合线上的内存采集信息,将ANR发生前后的内存使用信息一同上报
- 查看 kernerl 相关的日志,是否 有lomemorykiller iowait等系统相关资源不足的情况
部分问题治理分享
本节是分享在ANR平台上线后,利用ANR平台定位到的一些ANR问题。
调用Kotlin Function 对象的 toString 函数调用导致ANR
在排查ANR日志时,发现有多个ANR 日志中,Kotlin 的KfunctionImpl.toString都比较耗时,火焰图case如下
于是在线下对Kotlin的该函数进行了测试,在demo中参照火焰图中的业务代码进行了场景还原
结果发现,一个简单的toString调用,在demo中耗时达到了 118ms。经过一系列的变量测试发现,在引入kotlin后,并且 引入了 kotlin-reflection库的情况下, 调用 Kotlin Function对象的toString函数时 耗时非常严重。因此在业务代码中要注意,不要对kotlin的 函数对象 直接或者间接的触发toString操作。
主线程执行View.getDrawingCache进行bitmap转换
从队列中,发现大量的消息已被阻塞,并且在前序的Mesage处理记录中,发现有耗时900ms的消息处理。
转到对应的 method trace中发现,卡顿主要是由于在主线程对Webview获取drawingCache导致的。
在线下进行测试时,可能在正常情况下该函数的耗时并没有这么耗时,因此暂不能确定该问题是如何引起的,但是从代码实现的角度,也不应该在主线程执行类似的耗时操作,因此可以将该操作移入异步线程执行。
SP 等待任务造成的 ANR 问题
在ANR平台上线后,发现目前收集的用例中 有大量数据是由于 系 统组件在 onPause、onStop等相关流程需要等待所有异步的 SP写入操作完成造成的
得物目前的业务都已使用 MMKV 替换SP,主要使用SP的场景都是接入的一些三方SDK,而这些sdk一般不需要对这部分数据在组件跳转间 对“异步任务数据写入完成”的强保证,因此得物对这部分系统的机制进行了HOOK,在这部分HOOK功能上线后,ANR率有了明显下降。
后续优化迭代方向
从目前ANR平台上线后 问题的治理情况上来看,大部分容易解决的问题都是从 火焰图上分析出明显耗时的业务代码从而解析的,在实际的线上场景中,目前还是发现有一些特定的case以现有上报的日志信息无法明确给出发生ANR的根本原因。后面会根据每种case,加强其他方面的监控能力,来协助定位问题。
端侧
其他上下文信息收集的增强
比如在线上发现,某些ANR问题 耗时函数卡在一个简单的 File.exist函数调用上,针对此类问题,后续可能需要结合native层 对file io操作的一些监控、hook来协助定位问题。
端上 Method Trace 性能优化
目前得物APP在端上进行Method Trace的方案是 在子线程定时通过主线程的Thread对象 调用getStackTrace进行采集,虽然该函数的执行是在异步线程,但是在虚拟机底层在非当前线程获取某个线程的堆栈时,会先suspend对应的线程,获取完成后再resume。 因此如果对主线程抓起堆栈的频率较为频繁的话,势必会影响主线程正常函数的执行。
我们的MethodTrace的要求是应该尽量保证其只产生极小的运行时开销,避免为了监控反而影响应用性能,虽然目前的这套采样方案,控制的最小采样间隔为50s,对性能影响不大。在facebook 的profilo开源库中有一套 异步非阻塞获取线程堆栈的方案,不过其目前只开源了适配了 Android 9及以下的版本。字节跳动的西瓜视频 分享了另外一种 Java Method Trace ,不过该方案最终也是需要suspend thread, 对性能也是有一定影响,不过在实现上也是一个可以参考的方向。
平台测
MessageTrace 体验优化
目前Looper Message Trace 功能和 Method Trace(函数执行火焰图)功能是两个比较独立的功能,当我们在Message Trace 功能 中发现某个消息比较耗时,正常情况下是希望能够直接看到该消息具体做了什么操作,因此在平台测可以做的一个优化是, 当点击Message时,根据这个Message的处理的开始结束时间,自动截取对应时间的函数堆栈片段,进行信息展示。
Anr 时间的区分
端上收集上来的 Message信息及 函数采样信息,其实是包含了anr之后的,因为从anr触发到应用程序捕获再到收集信息 存在一定的时间差,因此从问题分析的角度上,这个时间点需要做一个明显的区分。
参考资料
- https://www.ssw.uni-linz.ac.a...
- https://mp.weixin.qq.com/s?__...
- https://mp.weixin.qq.com/s?__...
- https://mp.weixin.qq.com/s?__...
- https://androidperformance.co...
文/Knight-ZXW
关注得物技术,做最潮技术人!