共计 9477 个字符,预计需要花费 24 分钟才能阅读完成。
本文作者:xxq
背景
客户端 APM 监控是发现和解决产品质量问题的重要伎俩,通常用于排查线上解体等问题,随着业务迭代,单纯的解体监控不能满足要求,特地是对于云音乐这样业务场景很简单的产品,滑动不晦涩、设施发热、UI 卡死、无端闪退 等异样问题对用户体验挫伤都很大,因而咱们自研了一套能力更欠缺的 APM 监控零碎并在云音乐上获得了不错的成果,本文是对于客户端监控局部的具体实现计划以及施行成果的一些总结。
行业调研
互联网大厂根本都有自研的 APM,其中有些甚至曾经开源,市面已有计划中有大厂将本人积攒多年的 APM 监控能力商业化(字节、阿里、手 Q),也有许多优良的开源我的项目或具体计划介绍(matrix、Wedjat、Sentry),这些 APM 我的项目中不乏品质较高的开源我的项目比方 matrix 的内存监控,也有原理和思路比拟全面比方 Wedjat 以及一些技术分享文章。
但对于云音乐这样比较复杂且独立的大型项目来讲,亟需一款技术可控且合乎本身业务特点的 APM,因而咱们不仅吸纳了市面上优良计划的实践经验,同时联合业务场景做了深度的优化与改良,咱们的计划次要有如下特点:
- 场景丰盛全面:笼罩了 OOM、ANR、Jank 卡顿、CPU 发热、UI 假死 等场景;
- 异样精密管控:设计了一套异样问题分级规范,对不同级别的问题采纳不同的监控和治理策略;
-
堆栈精准高效:
- 通过 聚合型堆栈 构造晋升问题堆栈的准确率;
- 通过过滤无用堆栈缩小烦扰信息;
- 上报堆栈的线程名以便于过滤特定问题堆栈;
-
调试能力丰盛:调试工具能够无效晋升问题排查效率
- 监控台实时展示 CPU/GPU/FPS 等信息;
- 反对各类异样场景的模仿;
- 反对本地符号化堆栈信息;
- 反对函数耗时统计。
计划介绍
一、堆栈
指标
一款 APM 我的项目的外围指标是帮忙业务提前发现和疾速定位性能问题,在大家熟知的解体监控中 解体堆栈 是其最为外围的信息,在大部分场景能间接定位到呈现解体问题的代码行,在本文提到的各类异样监控中亦是如此,本我的项目中绝大部分异样 Issue 都会将堆栈作为其外围信息上报,因而堆栈是 APM 我的项目中最根底也是最重要的模块。
但与此同时性能性能异样的堆栈和解体型堆栈也存在很大区别,解体堆栈是在问题产生时抓取全线程堆栈,而性能异样的监控很多时候不能精确抓取到过后的调用栈,须要利用统计学伎俩去 猜问题场景最有可能的堆栈,所以咱们设计了一套 聚合型堆栈 计划,本文也先从这里开始论述。
堆栈聚合
Apple 的 ips 堆栈
堆栈格局参考自苹果 ips 文件,它将多组堆栈聚合到一起展现,通过缩进来示意堆栈的深度,这样即节俭了堆栈的存储空间,也便于直观展现多组堆栈信息,还能依据堆栈的命中次数提取出命中率最高的 要害堆栈,这对 Issue 的聚合有很大的帮忙。
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25331392763/770c/f9e6/cc4a/e1ddc94657c34898dd02be45448e0fd8.png” alt=”image.png” width=”600″ />
云音乐的聚合型堆栈
存储构造:这种聚合型堆栈实现办法比较简单,通过二叉树存储堆栈数据,打印后果时只需遍历二叉树,其中二叉树生成的算法如下:
- 传入堆栈数组以及以后遍历的深度,如果深度曾经超过数组大小,则退出递归;否则执行
> 步骤 2
;- 从栈底开始匹配以后二叉树节点,如果雷同,则跳转至
步骤 3
;不雷同则跳转至步骤 > 4
;- 挪动到下一个深度并交给
right
节点解决,right
为 nil 时创立节点,递归跳转至> 步骤 1
;- 不挪动深度并交给
left
解决,left
为 nil 时创立节点,递归跳转至步骤 1
。
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25612599000/4cb5/a6e8/ec21/3dea7f2e4b76c5aec56c1cdefa1480a5.jpeg” alt=”image.png” width=”100%” />
打印堆栈则是通过 DFS 后续遍历二叉树,再格式化输入每一栈帧的信息即可,须要依据树深度来输入正确的缩进,同时将堆栈的命中次数 / 占比打印在后面,后文有聚合型堆栈的展现成果,此处不赘述。
压缩原理:函数调用栈有一个特点,栈底的调用变动远远小于栈顶,这很好了解,一个调用树必定是越往树枝末端分叉越多,这也使得从栈底向上聚合时能压缩大量的存储空间,粗略统计相比不必聚合型堆栈的数据,能够节俭 50% 以上的存储空间。
下图中演示了 3 组堆栈聚合的过程,其中堆栈数据通过二叉树来治理。
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328001727/a708/0584/8eb8/926dd268a917c652652ee775cfd8a8df.jpeg” alt=”image.png” width=”100%” />
要害堆栈
每次传入堆栈更新 / 构建二叉树时,将以后节点的计数 +1,示意以后节点匹配的次数,次数最高的权重也就最高,权重最高的为要害堆栈。
因而获取要害堆栈的过程也是搜寻权重最大的二叉树门路,实现比较简单此处不再赘述。
有效堆栈
为什么要过滤?
在实际上报的堆栈里,咱们发现大量堆栈如下,都是一些纯零碎调用。
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328075712/055b/b2c6/4eb0/fdc415daef7c8d58eb450582c9613751.png” alt=”image.png” width=”33%” />
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328075713/5881/9c58/fbec/ce07e917a9e90359beca1ecf464a0a7e.png” alt=”image.png” width=”33%” />
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328075714/9ec1/2a0d/9b4b/7fed15c6e7d6c0059094fa05f74e36b2.png” alt=”image.png” width=”30%” />
这类堆栈对咱们排查问题简直没有什么帮忙,因而咱们默认剔除这类堆栈,最大水平缩小烦扰。
一个堆栈是由一组调用帧组成,每个调用帧由 image
addr
offset
或与之等价的信息形成,咱们只需判断 image 是不是 app 本人即可晓得当次调用是否来自咱们利用本身的代码。须要留神的是 APP 本身引入的动静库也要纳入外部调用,因而判断 image
是否来自 app 本身时,文件门路要去掉 *.app/*
这部分的匹配。
判断 main
函数地址
下面的三个图中,第一个图里有 main
函数,不管何时抓取主线程简直必然有这个调用,因为 APP 是由它启动的。然而 main 函数的 image 就是利用本身,如何独自排除掉这个非凡状况?能够通过 main 函数地址进行判断,首先获取到 main 函数地址,而后判断调用帧的 addr
是否来自 main 函数。
main 函数地址存在 mach-o 文件信息 LC_MAIN
CMD 中
// 获取 main 函数地址
struct uuid_command * cmd = (struct uuid_command *)macho_search_command(image, LC_MAIN);
if (cmd != NULL) {struct entry_point_command * entry_pt = (struct entry_point_command *)cmd;
Dl_info info = {0};
dladdr((const void *)header, &info);
main_func_addr = (void *)(info.dli_saddr + entry_pt->entryoff);
}
须要留神的是,获取到的函数地址与 frame 的
addr
会存在一个固定差值,判断时须要解决一下。
二、监控
指标
有了新的堆栈能力后,接下来咱们须要针对不同的异样场景设计相应的监控计划,个别比拟常见的性能异样场景和归因如下:
场景 | 归因 |
---|---|
设施发热、耗电快 | CPU 长时间高占用、频繁磁盘 IO |
卡顿 | 主线程执行或同步期待耗时工作,比方磁盘 IO、文件加解密计算、图片提前解压等 |
界面不响应 | 主队列不响应工作,比方主线程死锁、死循环占用等 |
异样闪退 | 内存占用过高 OOM、界面卡死、磁盘空间有余、CPU 继续过低等 |
咱们须要利用设备的零碎信息对不同的场景施行与之相应的监控计划,其中零碎信息与异样场景之间能够简略依照上面的映射进行关联:
- CPU => 设施发热问题
- Runloop 耗时 => 卡顿问题
- main queue => 界面不响应
- 内存占用 => OOM
理论中会略微简单一些,接下来本文会围绕一些典型场景讲述其监控原理。
CPU 高耗费
原理
窗口统计机制
CPU 过高的占用会带来设施发热、耗电快、后盾过程被零碎强杀等问题,重大影响用户体验,但失常应用下,比方滚动列表视图,通常会因为频繁 I / O 以及 UI 高频刷新,而以致 CPU 很容易达到 100% 占用率,但短时间的 CPU 高占用并不能掂量 APP 的衰弱度,甚至很多时候是失常景象,咱们更关注的那些 长时间占用 CPU 的问题线程,像 Xcode 自带的耗电监控也是相似的逻辑,因而咱们应用 窗口扫描机制 策略来发现这类异样问题。
Apple Xcode
自带的耗电监控异样日志<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328116852/7775/6b6c/6b20/0645d1a7a64722d3d69fa14722607027.png” alt=”image.png” width=”200″ />
实际中咱们发现大部分 CPU 异样场景会集中在单个线程,因而监控更偏重线程维度的表白,异样 Issue 与线程一对一的关系,同时将线程名称一并上报。
此外 CPU 异样最要害的信息是 堆栈,对于堆栈的格局、抓取策略、关键帧提取等内容,后面曾经具体论述,总的来说计划有如下几个关键点:
- 通过窗口扫描机制,聚焦 长时间占用 CPU 的异常情况
- 将异样问题依据均匀 CPU 占用率划分 info/warn/error 三种级别
- 一个 Issue 对应一个线程,Issue 中蕴含线程名信息
- 默认状况下,过滤齐全没有 APP 外部调用的堆栈数据
窗口扫描机制
固定的统计窗口内 CPU 超过限度的次数超过肯定次数时,抓取以后线程堆栈,当抓取线程堆栈数量超过设定阈值时,将采集到的堆栈聚合、排序并上报。
解释阐明:
- CPU usage 范畴是 0~1000,即 usage 为
100
示意占用率为10%
- 图中窗口为 5/8,即窗口 8 次中有 5 次超限(超过 80 阈值),抓取堆栈
- 窗口 1 中只有 120、100、100,共计 3 次超限
- 窗口 2 中有 120、100、100、100,共计 4 次超限
- 窗口 3 中有 120、100、100、100、100,共计 5 次超限,满足 5 / 8 窗口,
抓取堆栈
- …
成果
通过 CPU 监控定位了一处后盾线程高占用从而导致云音乐后盾听歌被强杀的线上问题。
某个线程 CPU 高占用上报量突增,解决后上报量升高到个位数
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328584190/4f2b/7f84/3229/8f49a5b73aa95a6eb1cd744814b7b65a.png” alt=”image.png” width=”90%” />
上报堆栈显示主线程某个动画模块继续高 CPU 占用
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25332319498/466f/e37d/4cc3/cc93e7d075d8a448374084af7d396932.png” alt=”image.png” width=”90%” />
Jank 卡顿
原理
后盾线程监控
业内对于卡顿监控的计划根本大同小异,通过一个独自的线程一直轮训检测 Main Runloop 的耗时状况,超时则认为产生卡顿,咱们定义超时工夫为 3 帧即 50ms
。同时咱们还管制了堆栈抓取的频次以及页面采集频次,因为卡顿事件切实是太多了😹。
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25328975870/056e/8de4/7ab2/4440c71c0b4a10c4e9e8baa28fcbbc6d.png” alt=”image.png” width=”90%” />
示例代码
// 监控线程
dispatch_async(self.monitorQueue, ^{
// 子线程开启一个继续的 loop 用来进行监控
while (YES) {NSTimeInterval tsBeforeWaiting = GetTimestamp();
long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, s_jank_monitor_runloop_timeout * NSEC_PER_MSEC));
CFRunLoopActivity runloopActivity = atomic_load_explicit(&self->_runLoopActivity, memory_order_acquire);
NSTimeInterval currentTime = GetTimestamp();
NSTimeInterval tsInterval = currentTime - tsBeforeWaiting;
if (semaphoreWait != 0) {
// 信号量超时,认为产生卡顿
...
}
}
}
...
// 主线程 runloop 回调
static void RunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {APMJankRunloopMonitor *jankMonitor = (__bridge APMJankRunloopMonitor *)info;
atomic_store_explicit(&jankMonitor->_runLoopActivity, activity, memory_order_release);
dispatch_semaphore_t semaphore = jankMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
频控
每个页面每日只统计 1 次,除此之外,为了防止过于密集地抓取堆栈以及扩充堆栈采集的时间跨度,并不是每次卡顿事件产生时都抓取堆栈,约定在第 1、3、5、10、15、20…5n
次卡登时抓取主线程堆栈,当抓取到的堆栈数量超过一个阈值时上报数据。
成果
从上线后成果来看,聚合的准确度还不错,通过几个头部卡顿 Issue 能够看到,页面卡顿的典型场景集中在磁盘 IO 方面,与理论的后果是相符的。
主线程操作 FMDB
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25329044523/6483/91d0/01df/c13ee886ef5f6525693abe57325fed99.png” alt=”image.png” height=”80″ />
主线程 md5 计算
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25329044525/dec4/2b09/fbfd/8db8f61c2bde5851ca434af1a3cb350f.png” alt=”image.png” height=”80″ />
主线程下载文件
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25329044524/c9d5/36b8/c0a5/85e9a00e8794960a09aae5c48ac6f66e.png” alt=”image.png” height=”80″ />
ANR 卡死
原理
ping 机制
ANR 是指 UI 线程无响应的状况,此时 UI 线程因为某种原因被阻塞,不执行任何新提交的主线程队列工作,基于这个特点,监控原理则是通过定时向 main_queue
中发送工作批改 ack
值,每次轮训检测 ack
的值是否产生批改来判断主线程是否产生了ANR。
检测流程示意
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25556588997/078b/aeeb/f1b6/426d48ec7b6ec3d71e4eacf62803fd3c.jpg” alt=”image.png” width=”100%” />
示意代码
// ack: recv success
if (atomic_load_explicit(&s_ack, memory_order_acquire)) {
// ack 胜利,值被批改
// 状态复原,ANR 完结 / 未产生
// ...
// ANR 计数清零
atomic_store_explicit(&s_anr_count, 0u, memory_order_release);
} else {
// 无应答,ANR 计数 +1
unsigned long anr_count = atomic_fetch_add_explicit(&s_anr_count, 1u, memory_order_acq_rel);
anr_count ++;
// 产生 ANR 事件
// ...
}
// ack: send
atomic_store_explicit(&s_ack, false, memory_order_release);
dispatch_async(dispatch_get_main_queue(), ^{
// ack: recv
atomic_store_explicit(&s_ack, true, memory_order_release);
});
每次产生 ANR 时抓取堆栈,抓取规定如下
- ANR 的第 4、8、16 秒时,抓取全线程堆栈并聚合
- ANR 的第 2、3、4、5、6…n 秒时,抓取主线程堆栈并聚合
实时将抓取到的堆栈数据存储到本地,如果程序从 ANR 状态复原执行,则删除本地 ANR 数据;
每次启动时查看本地是否存在 ANR 数据,如果有数据则上报 ANR 异样,上报后删除这份数据。
成果
常见的 ANR 场景有死锁(CPU 占用低)、死循环(CPU 占用高)、大工作等,上面展现了几种典型的 ANR 异样堆栈。
死锁问题
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25744281243/ce29/fea9/778a/d1c4bd1938a5d61a29c5d85e60f7dc4a.png” alt=”image.png” height=”200″ />
h5 页面死锁
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25332455565/d658/b3f7/e792/5b8002312c7372dfd500b9a60aca7960.png” alt=”image.png” height=”200″ />
IO 操作超时
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25332457068/9d01/34ec/8ac9/e6c93e3e653e3eebc8f3e314ef21ad19.png” alt=”image.png” height=”200″ />
内存异样
原理
内存异样次要蕴含 OOM、 大内存对象 和巨量小内存对象 三类异样,其中 OOM 属于解体型异样,而后两者属于运行时异样内存调配,比方某个对象创立了是百万次,或者一次申请了 10M 大小的内存对象。
计划原理在肯定水平参考了 matrix
的计划,通过零碎的 malloc_logger
回调时抓取内存申请的堆栈,依据内存大小维度聚合内存对象,记录内存的申请数量、内存大小以及堆栈等信息,在上报时 dump 出堆栈数据并上报,堆栈格局和后面一样都是聚合型堆栈。
须要留神的是,Dump 内存信息是比拟耗性能的工作,监控只在 APP 内存占用超过 500M 时触发 dump,同时在 >500M 的前提下,每次内存增长 300M 会再次触发 dump 工作,下图展现了内存稳定与 dump 机会的场景。
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25545743594/6d72/80b6/f40d/0978cac0dbfdc6b76c4664df0889875e.png” alt=”image.png” width=”100%” />
成果
目前 OOM 监控已在线上启用 3 个月以上,没有对用户体验产生显著劣化,咱们甚至尝试过在 main 函数前就启动 OOM 监控,帮忙业务侧定位到一个极难排查的 启动 OOM 问题。
程序刚启动便产生重大的 OOM,零碎的 ips 以及 xcode instrument 等官网工具,对这个场景简直都大刀阔斧。
<img src=”https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25329204947/7bcc/0840/5e61/86903343bf423408160da6824f5f6d21.png” alt=”image.png” width=”100%” />
下图展现了某个 240 字节的内存对象申请了 6535 次,共占用 485Mb 内存大小
<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/25332319498/466f/e37d/4cc3/cc93e7d075d8a448374084af7d396932.png” alt=”image.png” width=”100%” />
后记
限于篇幅有很多能力没有开展讲述,APM 上线半年以来,帮忙云音乐发现和定位不少线上问题,现在面对客诉反馈时也不再两眼一抹黑,大大提高了问题的解决效率,APM 在将来还会围绕上面几个方向继续欠缺,它也将继续为云音乐线上品质保驾护航。
对于 APM 将来的布局
- 链路自动化:异样 Issue 主动指派
- 场景精细化:网络大图内存异样监控
- 更全面的工具:监控日志定向回捞、采样数据可视化展示
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!