本文作者: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 的聚合有很大的协助。
云音乐的聚合型仓库
存储结构:这种聚合型仓库完成办法比较简略,经过二叉树存储仓库数据,打印结果时只需遍历二叉树,其间二叉树生成的算法如下:
- 传入仓库数组以及当时遍历的深度,假如深度现已超越数组巨细,则退出递归;否则履行
> 进程2
;- 从栈底开端匹配当时二叉树节点,假如相同,则跳转至
进程3
;不相同则跳转至进程> 4
;- 移动到下一个深度并交给
right
节点处理,right
为nil时创立节点,递归跳转至> 进程1
;- 不移动深度并交给
left
处理,left
为nil时创立节点,递归跳转至进程1
。
打印仓库则是经过 DFS 后续遍历二叉树,再格局化输出每一栈帧的信息即可,需求依据树深度来输出正确的缩进,一同将仓库的射中次数/占比打印在前面,后文有聚合型仓库的展现作用,此处不赘述。
紧缩原理:函数调用栈有一个特色,栈底的调用变化远远小于栈顶,这很好理解,一个调用树肯定是越往树枝末端分叉越多,这也使得从栈底向上聚合时能紧缩大量的存储空间,大略核算比较不必聚合型仓库的数据,能够节省50%以上的存储空间。
下图中演示了3组仓库聚合的进程,其间仓库数据经过二叉树来办理。
要害仓库
每次传入仓库更新/构建二叉树时,将当时节点的计数+1,表明当时节点匹配的次数,次数最高的权重也就最高,权重最高的为要害仓库。
因而获取要害仓库的进程也是查找权重最大的二叉树途径,完成比较简略此处不再赘述。
无效仓库
为什么要过滤?
在实践上报的仓库里,咱们发现大量仓库如下,都是一些纯体系调用。
这类仓库对咱们排查问题简直没有什么协助,因而咱们默许剔除这类仓库,最大程度削减搅扰。
一个仓库是由一组调用帧组成,每个调用帧由 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
自带的耗电监控反常日志
实践中咱们发现大部分CPU反常场景会会集在单个线程,因而监控更偏重线程维度的表达,反常Issue与线程1对1的联系,一同将线程称号一并上报。
此外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高占用上报量突增,处理后上报量降低到个位数
上报仓库显示主线程某个动画模块继续高CPU占用
Jank 卡顿
原理
后台线程监控
业界关于卡顿监控的计划根本迥然不同,经过一个独自的线程不断轮训检测 Main Runloop 的耗时状况,超时则以为产生卡顿,咱们界说超时时间为3帧即 50ms
。一同咱们还控制了仓库抓取的频次以及页面收集频次,因为卡顿事情实在是太多了。
示例代码
// 监控线程
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
主线程 md5 核算
主线程下载文件
ANR 卡死
原理
ping机制
ANR 是指UI线程无响应的状况,此时UI线程因为某种原因被阻塞,不履行任何新提交的主线程行列使命,根据这个特色,监控原理则是经过守时向 main_queue
中发送使命修改 ack
值,每次轮训检测 ack
的值是否产生修改来判别主线程是否产生了ANR。
检测流程示意
示意代码
// 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反常仓库。
死锁问题
h5 页面死锁
IO 操作超时
内存反常
原理
内存反常主要包含OOM、大内存目标和巨量小内存目标三类反常,其间 OOM 归于溃散型反常,而后两者归于运行时反常内存分配,比方某个目标创立了是百万次,或许一次申请了10M巨细的内存目标。
计划原理在必定程度参考了 matrix
的计划,经过体系的 malloc_logger
回调时抓取内存申请的仓库,依据内存巨细维度聚合内存目标,记录内存的申请数量、内存巨细以及仓库等信息,在上报时dump出仓库数据并上报,仓库格局和前面相同都是聚合型仓库。
需求留意的是,Dump 内存信息是比较耗功能的使命,监控只在APP内存占用超越500M时触发 dump,一同在 >500M 的前提下,每次内存增长300M会再次触发 dump 使命,下图展现了内存动摇与 dump 机遇的场景。
作用
目前OOM监控已在线上启用3个月以上,没有对用户体会产生显着劣化,咱们乃至尝试过在 main 函数前就发动 OOM 监控,协助事务侧定位到一个极难排查的发动 OOM 问题。
程序刚发动便产生严重的 OOM,体系的 ips 以及 xcode instrument 等官方东西,对这个场景简直都束手无策。
下图展现了某个 240 字节的内存目标申请了6535次,共占用485Mb内存巨细
后记
限于篇幅有许多才能没有展开叙述,APM 上线半年以来,协助云音乐发现和定位不少线上问题,现在面临客诉反应时也不再两眼一抹黑,大大提高了问题的处理效率,APM 在未来还会环绕下面几个方向继续完善,它也将继续为云音乐线上质量保驾护航。
关于 APM 未来的规划
- 链路主动化:反常 Issue 主动指派
- 场景精细化:网络大图内存反常监控
- 更全面的东西:监控日志定向回捞、采样数据可视化展现
本文发布自网易云音乐技能团队,文章未经授权禁止任何形式的转载。咱们常年接收各类技能岗位,假如你预备换作业,又恰好喜欢云音乐,那就参加咱们 grp.music-fe(at)corp.netease.com!