背景介绍
直播 OOM 问题比较棘手难以定位,主要体现在涉及的业务很多,从定位到解决花费时间比较久。为了提前触达问题,提高定位的效率,也是对现有工具的补充,提出直播内存抖动解决方案- MemoryThrashing。
为什么要提出这个方案 ?
- 现有的 “MemoryGraph” 工具可以通过抓取的“MemoryGraph” 文件分析 OOM 成因,比如内存泄漏、内存占用过高导致的 OOM 问题,但因为性能开销很大,所以是采样上报且采样率很低,不容易触达问题, 只能定向对已知用户开启才行。期望自研一个工具,在内存增长时可以发现问题,也能用于 OOM 发生后的分析,同时具备性能开销小、全采样的能力;
- “MemoryGraph” 生成时可能不是内存高位,比如设备内存是 4G,生成的“MemoryGraph” 可能是 1G,会影响 OOM 分析;
什么是 Thrashing(抖动) ?
维基百科中 thrashing 定义:
- In computer science, thrashing occurs when a computer's virtual memory resources are overused, leading to a constant state of paging and page faults, inhibiting most application-level processing.([1]) This causes the performance of the computer to degrade or collapse.
从业务视角定义内存 thrashing:
通俗讲就是性能数据有大的波动,拿内存来讲,当内存短时间从 600M 涨到 800M 叫作一个抖动。希望通过自研工具找出这 200 M 内存增长来自于哪里,在实际的 OOM 案例中因内存突增导致的 OOM 是比较常见的,具体现象如下:
- 内存未回落:内存突增一般发生在一两分钟内,内存从 1G 涨到 3G,这部分内存会一直滞留在内存中不会被释放掉或者没有机会释放掉直接 OOM,同时助高了内存水位很容易发生 OOM 问题;
- 内存回落:内存突增到一定水位开始回落未形成 OOM,这种现象通常是内存问题不够劣化,或者机器本身内存足够大不容易 OOM,虽然没有造成 OOM 但也是一个潜在的问题;
以临时对象、内存堆积为例来阐述如何定位该类问题,通过“AllocTime Summary” 描述临时对象分配次数,通过 “Memory Summary” 描述内存堆积。
临时对象
临时对象:短时间分配大量对象,导致直播稳定性波动较大,可能使内存、CPU 负载变高。这类问题通常表现为短时间内存冲高或者直接 OOM,或之后开始迅速回落到正常水位,这类对象不会驻留内存过久,通过监控 “临时对象” 可以提前发现这类问题。
以上是按分配次数(AllocTime Summary)统计的 TOP 临时对象,“AllocTime Summary 1” 代表第一次采样 Class 的分配次数其它依次类推。举例:通过 diff “AllocTime Summary 2” 与 “AllocTime Summary 1” 差值可知 “LivexxxA” 在采样周期分配了 7803 次,由于未采集到 “Memory Summary” 信息,可认为未有内存驻留。
内存堆积
内存堆积:内存驻留了大量对象,而且这类对象短时间不会释放掉,导致内存水位居高不下,很容易触发 OOM 问题。
以上是按内存驻留统计的 TOP 实例,“Memory Summary 1” 代表第一次采样实例数量的内存驻留信息其他依次类推。举例:通过 diff “Memory Summary 2” 与 “Memory Summary 1” 可知 “LivexxxA” 在采样周期内增长了 56791 个,根据最后一次采样可知内存驻留了总共 69904 个实例,通过采样可知“LivexxxA” 每次都是递增的。
MemoryThrashing 方案
方案调研
方案思路是做内存差值找出增长,通过采样多个时刻的内存信息(目前主要监控 Class 的实例个数), Diff 出内存信息找出 TOP 增长,达到归因的目的。
- 内存区:通过内存节点遍历统计 Class 实例个数;
- Runtime:通过 alloc、dealloc 计数实现统计实例存活数量;
内存区
通过内存节点遍历与已注册的 Class 比较统计实例个数,该方案的优点是可以监控整个 APP 的 OC 对象实例个数,面对直播业务场景需不需监控整个 APP 的对象,目前看暂时用不到,需求出发点是监控直播场景且满足一定条件。比如:直播观播一段时间后内存的大幅波动,场景比较聚焦。另一个考虑是如果当前内存比较大,遍历 zone 会比较耗时,如果不挂起线程会有潜在的崩溃问题、以及数据不准问题。
RunTime
通过 Hook 的方式,统计 Class 实例的分配、释放次数,达到记录实例存活个数的目的,可监控固定场景的 OC 实例增长情况,如直播间内的内存突增,范围比较小不需要统计过多的无用对象。该方案相对内存区遍历耗时小,且不会有野指针问题。但需要注意的是监控对象时对性能的影响,目前采用的是 RunTime 方案,从线下直播间测试情况看对主线程的影响忽略不计。
方案设计
在实际开发过程中发现对象的创建、释放处于复杂的多线程环境中,处理不当会对业务产生潜在的影响,影响到业务执行效率或者造成稳定性问题:
- 容器置于多线程下会有线程安全问题;
- 过度的使用锁会阻塞业务代码执行,也可能触发 Watchdog 机制导致 APP 被 kill;
经过优化采用多级缓存方案解决主线程的性能开销问题,达到主线程几乎零开销。
监控流程
在进入直播间一段时间后开启监控,通过监控内存值变化来区分是否开启采样功能,开启采样后会进入连续多次采样阶段,多次采样完成后进行数据上报,上报完成后会继续监控内存。
数据展示
在高热直播间多次采样的内存快照,采集 TOP 100 数据,以 “LivexxxA” 为例两次采样中第二次增长了 4125 个实例,可以简单归因 “LivexxxA” 相关业务导致 “MemoryThrashing”,可以从 “LivexxxA” 相关业务入手排查。
方案优缺点
方案 | 优点 | 缺点 |
“MemoryThrashing” | 可以多次采样,对比内存增长趋势;性能开销小,可线上全量;提前感知内存问题;上手简单,通过对象数量就可以排查问题; | 不支持多语言,只限于 oc 语言;不具备通过内存节点关系分析内存泄漏问题,只能找出堆积的对象;不具备分析多个内存区的能力;Hook 方式影响方法缓存; |
“MemoryGraph” | 问题发现能力强:可以通过内存节点关系分析内存泄漏导致的 OOM 问题;可以统计内存区的内存占用情况;适用多语言;上手复杂,需要梳理内存节点引用关系; | 线程挂起会影响业务执行,用户感知明显;内存使用越高,内存区遍历越耗时;只能少量采样; |
实践案例
目前 “MemoryThrashing” 已经部署了,可以监控测试环境,后续将部署到线上。通过线下看提前暴露了很多问题,相对以往方式只有问题发生了或者产生了明显影响才能感知到,需要 QA 反馈到 RD,通过“MemoryThrashing”大大提升了排查效率,很好的将劣化问题前置发现,以下抽取其中两个案例。
内存堆积
如下,多个采样周期内出现了大量对象的分配问题,且这些对象未释放,并且导致了内存明显上涨,采样周期 3 比采样周期 2 多分配了 234024 个对象,且最后内存驻留了 238800 个 “LivexxxBigDataRead” 对象,占用内存 10.9M。
临时对象
如下,是开播场景抓到的问题,在主播端开启弹幕狂欢时,过 Effect 认出人脸后,就会创建一个对应的轮廓模型给到中台去画轮廓,频率会很高,每 5 秒周期(实际时间更小)临时对象增量高峰可到 6w 个(后两次采样差值),由于未生成 “Memory Summary” 信息可认为未驻留内存 ,累计过百万次对象分配,对开播性能会产生直接影响:
未来规划
归因能力
只统计 OC 对象数据在某些情况下可能不够,比如公共基础对象异常增长,则没有办法追踪到具体成因,如果带有对象引用关系可以进一步锁定问题。当然这些都是对 “Memory Graph” 能力的补充,如果“Memory Graph” 已经抓到了数据,可以结合“Memory Graph” 锁定对象引用链路继而找到业务。
- “MemoryThrashing” 可以加上对象引用关系计算,从效率上讲没必要对所有的对象查找其引用关系,查找引用关系是比较耗时的。只需查找 TOP 增长点的关键对象引用关系,实测可能只需要查找几个对象的引用关系。
- 通过线程堆栈采样记录信息;
CPU 监控
根据以往案例如:OOM、ANR 有不少会伴随着高 CPU 使用率,比如某次案例由大量数据处理导致的 OOM 问题,经排查发现负责该业务处理的线程 CPU 使用率很高,所以通过监控线程 CPU 使用率,来补充监控显得很有必要,可以通过线程名字、堆栈, 锁定怀疑的业务。