近日,快手宣布开源KOOM,成为行业首个开源线上内存溢出(Out of Memory,以下简称OOM)问题解决方案的互联网企业。据介绍,KOOM是在客户端完成内存监控后,将解析报告上传到云端,传输文件大小仅为KB级,运行时用户无感知,对流量基本无影响,适合大规模普及应用,目前该方案已在快手全量业务中应用,OOM率降低了80%以上,效果显著。
OOM是当前Android开发中的常见疑难问题,尤其是线上发生的OOM问题极难定位。业界当前最知名的方案LeakCanary,通过监控Activity/Fragment泄漏优化Java OOM问题,多年来一直为广大app保驾护航,解决了OOM治理从0到1的问题。但面对行业不断复杂的业务环境和庞大用户流量,LeakCanary仍有优化空间:受限于性能,无法在线上大规模部署,仅支持线下使用;只能定位Activity&Fragment泄漏,无法定位大对象、频繁分配等问题;需要人工一一分析,无法对问题聚类量化……为了彻底解决OOM问题,行业尝试了多种解决方案,通常是基于LeakCanary做优化,但至今没有能完全解决监控过程中的性能问题,普遍解决方法是通过采样的办法牺牲一小部分用户的体验来定位问题。
快手OOM Killer沿用行业的研究思路,针对LeakCanary无法解决的难题进行自研改造,充分发挥LeakCanary原有优势的同时补足短板,打造了一套可以线上部署、兼顾线下、配置灵活、适用范围广泛、高度自动化,埋点、监控、解析、上报、分发、跟进、报警一站式服务的闭环监控系统,将绝大多数OOM问题拦截在灰度阶段,彻底解决了OOM问题。
KOOM框架
快手KOOM核心流程包括:配置下发决策、监控内存状态、采集内存镜像、解析镜像文件(以下简称hprof)生成报告并上传、问题聚合报警与分配跟进。
无主动触发GC不卡顿
之前行业的普遍做法是通过在Activity.onDestroy()后连续触发两次GC,并检查引用队列,判定Activity是否发生了泄漏,但频繁GC会造成用户可感知的卡顿,快手为实现无感触发设计了全新的监控模块,通过无性能损耗的内存阈值监控来触发镜像采集。将对象是否泄漏的判断延迟到了解析时,阈值监控只要在子线程定期获取关注的几个内存指标即可,性能损耗忽略不计。
内存监控流程图
高性能镜像DUMP
采集内存镜像传统方案会造成应用完全冻结长达几秒,期间用户完全不能操作,严重损害用户体验。快手利用系统内核COW(Copy-on-write,写时复制)机制,每次dump内存镜像前先暂停虚拟机,然后fork子进程来执行dump操作,父进程在fork成功后立刻恢复虚拟机运行,整个过程对于父进程来讲总耗时只有几毫秒,对用户完全没有影响。
暂停虚拟机需要调用虚拟机的art::Dbg::SuspendVM函数,谷歌从Android 7.0开始对调用系统库做了限制,快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制。
Fork dump hprof流程图
“不偷”用户流量的解决方案
传统方案得到的hprof文件通常比较大,占用用户大量磁盘空间,上传大文件浪费用户流量,且不利于问题聚类分析。快手采用了新的思路:采用边缘计算的思路,将内存镜像于闲时进行独立进程单线程本地分析,不过多占用系统运行时资源;分析完即删除,不占用磁盘空间;分析报告大小只有KB级别,不浪费用户流量。
分析报告生成流程总体分为三个环节,第一个环节扫描镜像构建索引,建立泄露查找分析的基础;第二个环节查找出泄露的对象,根据既有的framework知识以及人为设定的策略,执行对象泄露判定;第三个环节生成最终报告文件,将对象泄露路径、泄露数量、类统计、运行时信息添加至报告文件,辅助后续根据报告分析解决OOM问题。
解析镜像生成报告流程图
针对镜像回捞需求,对hprof进行运行时hook裁剪,只保留分析OOM必须的数据。裁剪还有数据脱敏的好处,只保留对分析问题有用的内存中类与对象的组织结构,并不上传真实的业务数据,充分保护用户隐私。
总结展望
快手KOOM计划做完整的客户端内存解决方案,开发者可以通过接入KOOM,解决自己项目中的OOM问题。此次一期开源暂时只包括Android Java OOM解决方案,后续还将开源Android线程/文件描述符监控、Android Native OOM监控、iOS OOM监控等,最终实现帮助开发者解决各种场景下OOM的愿景。
快手KOOM GitHub地址:https://github.com/KwaiAppTeam/KOOM