图片来自:unsplash.com 本文作者: zgy
背景
跟着云音乐不断的对线上溃散管理,现在溃散率现已到达了行业界较低水平。但线上还存在很多 OOM 的溃散,这种溃散大多是因为编码不规范导致的内存反常问题(比方内存走漏、大目标、大图等不合理的内存运用)。内存问题难发现、难复现和难排查。这就需求咱们经过一些监控手段和一些东西去帮忙开发人员更好的排查此类问题。 接下来便是云音乐在内存监控方面的一些探索和实践,首要从以下几个方面介绍
内存走漏监控
谈到内存问题,咱们最先想到的应该便是内存走漏。简单来说内存走漏便是某些不再运用的目标被其他生命周期更长的 GC Root 直接或许直接以强引证的方法持有,导致内存不能及时释放,然后引发内存问题。
内存走漏简单添加运用内存峰值提高 OOM 的概率,归于过错型问题,一起也是比较照较简单监控的类型。可是关于事务同学一般开发使命比较重,开发过程中一般不太会主动去重视本地检测的走漏问题。这就需求咱们去树立一套主动化东西监控内存走漏,并主动生成使命单派发到对应开发,然后推进开发人员像处理溃散问题的流程相同处理 APP 中的走漏问题。
内存监控计划
首先说到内存走漏检测咱们肯定都能想到 LeakCanary,Leakcanary 是 Square 开源的 Java 内存走漏剖析东西,首要用于开发阶段检测 Android 运用中常见的内存走漏。
LeakCanary 的优势是能给出可读性很好的功能检测成果,并且能给出一些常见的处理计划,所以比较其他本地剖析东西( MAT 等)更加高效。 LeakCanary 的中心原理是首要经过 Android 生命周期的 api 来监听 activities 和 fragments 什么时分被毁掉,被毁掉的目标会被传递给一个 ObjectWatcher,它持有它们的弱引证,默许等待5秒后调查弱引证是否进入相关的引证队列,是则阐明未产生走漏,不然阐明可能产生走漏。
LeakCanary 的中心流程如下:
Leakcanary 在测验环境能根本满意咱们本地的走漏监控,可是由于 LeakCanary 本身检测会主动触发 GC 形成卡顿,并且默许直接运用的是 Debug.dumpHprofData()
,在 Dump 的过程中会有较长时刻的运用冻结时刻,不太适宜出产环境。
关于这点快手团队在开源框架 Koom 中提出了优化计划:它利用 Copy-on-write 机制 fork 子进程 dump Java Heap,处理了 dump 过程中 App 长时刻冻结的问题。Koom 的中心原理是周期性查询 Java 堆内存、线程数、文件描述符数等资源占用状况,当连续多次触发设定的阈值或许突发性连续快速打破高阈值时,触发镜像收集,镜像收集采用虚拟机 supend->fork 虚拟机进程 -> 虚拟机 resume->dump 内存镜像的策略,一起基于 shark 履行镜像解析离线内存走漏断定与引证链查找,并生成剖析报告。
Koom 的中心流程图如下:
经过对两个开源库的剖析比对,为了做到更全面的监控,咱们决议从线上线下两个维度来树立咱们的监控体系,再结合咱们的渠道对剖分出的内存走漏、大目标等问题依照引证链主动聚合归因,并且依照聚合后的问题排序,后续经过主动建单的方法推进事务侧开发去处理问题。
整体流程:
线上咱们树立一个相对严苛的条件(内存连续触顶、内存突增,线程数或许 FD 数连续几回到达阈值等条件,并且单个用户在必定周期内只会触发一次),当用户触发这些条件后,会 dump 内存生成 HPORF 文件,然后对 HPORF 文件进行剖析,剖分出内存走漏和大目标(大目标阈值经过线上装备可动态调整)等信息,一起剖析大图占用以及图片总占用等信息,最后将剖析的成果上签到后台服务。为了降低对线上用户的影响,前期咱们暂时先不上传 HPORF 文件,后期再依据需求依照采样的方法上报裁剪后的 HPORF 文件。关于 shark 对 HPORF 文件的剖析,网上都有较为详细的资料,这儿就不展开了。
线下咱们首要结合主动化测验以及在测验环境下,监控 Activity、Fragent 走漏数量到达必定阈值或许内存触顶等多种状况下触发 dump,并且会输出 HPORF 文件剖析成果,一起上签到后台服务。
渠道侧依据客户端上报的问题,将大数据问题完结聚合消费后,依照用户的走漏次数、影响用户数、均匀内存走漏率等维度进行排序,后续能够经过主动化建单的方法,分发给对应的开发,然后推进事务侧处理。
现在咱们首要支撑以下目标的走漏:
- 现已 destroyed 和 finished 的 activity
- fragment manager 现已为空的 fragment
- 现已 destroyed 的 window
- 超越阈值巨细的 bitmap
- 超越阈值巨细的根本类型数组
- 超越阈值巨细的目标个数的恣意 class
- 现已整理的 ViewModel 实例
- 现已从 window manager 移除的 RootView
大图监控
咱们都知道 Bitmap 一直是 Android App 总内存消耗占比最大的部分,在很多 java 或许 native 内存问题的背面都能看到不少很大的 Bitmap 的影子,所以大图管理是内存管理必不可少的一步,那么咱们做内存监控也必然少不了大图监控。
针对大图监控,咱们首要分为线上图片库加载的大图和本地资源大图的监控。
线上大图监控
现在咱们首要是对网络加载的图片做了统一的监控, 由于咱们事务加载图片都统一运用的是同一个图片框架,所以咱们只需求在加载图片时判别加载的图片是否超越必定的阈值或许超越 view 的巨细,超越则进行记载和上报。咱们改造了当时的图片库,新增图片信息的获取然后回调给监控 sdk,能够拿到加载图片的宽、高、文件巨细等信息,一起也获取当时 view 的巨细,然后咱们会比照当时 view 的图片巨细或许图片占用内存是否到达必定的阈值(这儿支撑线上装备),终究上签到咱们的监控渠道。为了方便剖析定位,并且削减功能消耗,咱们在线上不会抓取堆栈信息,只会获取当时 view 的层级信息,为了防止 view 层级过大,咱们只获取5层数据,现在来看当时的信息现已足够咱们定位到当时的 view。一起咱们也结合了自研的曙光埋点体系,算出当时 Oid 页面的大图率,这样也能够方便咱们监控一些 p0 级页面的大图率。
本地图片资源监控
除了线上的大图,咱们还会对本地的资源图片做一些把控,一起也能防止图片资源过大导致包体积快速增长的问题。详细实现是经过卡点流程去做一些本地资源的检测,经过插件在 mergeResources 使命后,遍历图片资源,收集超越阈值的图片资源,输出一个列表,然后上报后台服务,经过主动建单的方法,找到对应的开发,在发版前修正掉。
内存巨细监控
除了发现监控走漏的问题和大图的问题,咱们还需求树立一个内存大盘,以便咱们能更好的了解当时 App 线上的内存占用问题,方便咱们更好的监控 App 的内存运用状况。咱们的内存大盘首要分为运用发动内存(Pss)和运转中内存(Pss)、Java 内存、线程等。
发动内存、运转内存和 Java 内存监控
咱们发现在 App 发动的时分,假如遇到需求运用的内存过大,这时分在 App 侧会呈现较大的体会问题,体系不断的回收内存,一起 App 履行发动,在内存不足的状况下发动会更慢,因而咱们需求监控发动内存占用状况,来方便咱们后续的内存管理。Android 体系中,需求咱们重视两类内存的运用状况,物理内存和虚拟内存。通常咱们运用 Android Memory Profiler 的方法查看 APP 的内存运用状况。
咱们能够查看当时进程总内存占用、JavaHeap、NativeHeap,以及 Graphics、Stack、Code 等细分类型的内存分配状况。那么咱们需求线上运转时获取这些内存数据该怎么获取呢?这儿咱们首要经过获得一切进程的 Debug.MemoryInfo 数据(留意:这个接口在低端机型中可能耗时较久,不能在主线程中调用)。经过 Debug.MemoryInfo 的 getMemoryStat 方法(需求 23 版别及以上),咱们能够获得等价于 Memory Profiler 默许视图中的多项数据,然后不断获取发动完结以及运转过程中细分内存运用状况。
运用发动完结时内存的获取咱们结合了咱们之前发动监控的完结时刻节点来收集当时的内存状况。咱们在发动时就会发动多个进程,依据咱们之前的剖析,APP 发动的时分假如需求运用内存越多,越简单导致 APP 发动呈现问题。所以咱们会核算一切进程的数据,为后续的进程管理做好铺垫。
运转内存则是每隔一段时刻去异步获取当时内存的运用状况,一起也是获取整个运用一切进程的内存占用状况。咱们把一切收集的数据上签到渠道端,一切核算都在后台处理,这样能够做到灵活多变。后台能够核算出发动完结、运转中均匀 PSS 等目标,它们能够反映整个 APP 内存的大概状况。
此外,咱们还能够经过 RunTime 来获取 Java 内存。咱们经过收集的数据核算出 Java 内存触顶(默许内存占用超越 85% 算触顶)的状况,再依据咱们的渠道的汇总算出一个触顶率,能够很好的反映 App 的 Java 内存的运用状况。一般假如超越 85% 最大堆约束,GC 会变得更加频繁,简单形成 OOM 和卡顿。因而 Java 触顶率是咱们需求重视的一个很重要的目标。
在监控 Java 内存触顶的一起,咱们在收集数据时也加了 Java 内存不足的回调。关于体系函数 onLowMemory
等函数是针对整个体系的内存回调,关于单进程来说,Java 内存的运用没有回调函数供咱们及时释放内存。咱们在做触顶的时分,刚好能够实时监控进程的堆内存运用率,到达阈值即可通知相关模块进行内存释放,这样也能够在必定程度上降低 OOM 的概率。
线程监控
除了由内存走漏或许请求大量内存导致的常见的 OOM 问题。咱们也会遇到类似如下过错
java.lang.OutOfMemoryError: {CanCatch}{main} pthread_create (1040KB stack) failed: Out of memory
这儿的原因咱们应该都知道,根本原因是因为内存不足导致的,直接的原因是在创建线程时初始 stack size 的时分,分配不到内存导致的。这儿就不详细去剖析 pthread_create 的源码了。除了 vmsize 对最大线程数的约束外,在 linux 中对每个进程可创建的线程数也有必定的约束(/proc/pid/limits)而实践测验中,咱们也发现不同厂商对这个约束也有所不同,并且当超越体系进程线程数约束时,同样会抛出这个类型的 OOM。这儿特别指出的是华为的 emui 体系的某些机型,将最大线程数约束为 500 个。
为了了解咱们当时的线程的运用状况,咱们对云音乐的线程数进行了监控核算,线程数超越必定阈值时,将当时的线程信息上报渠道。这儿渠道也核算出了一个线程触顶率,经过这个触顶率能够衡量咱们整体的线程健康状况,也为咱们后续收敛运用线程做好铺垫。
除此之外,咱们还借鉴了 KOOM 对线程走漏做了监控,首要监控 native 线程的几个生命周期方法: pthread_create、 pthread_detach、 pthread_join、 pthread_exit。 hook 以上几个方法,用于记载线程生命周期和堆栈、称号等信息,当发现一个 joinable 的线程在没有 detach 或许 join 的状况下,履行了 pthread_exit,则记载下走漏线程信息,然后在适宜的机遇上报线程走漏。
总结
云音乐的内存监控比较业界起步较晚,所以能够站在伟人的肩膀上,结合云音乐现状做更适宜咱们当时场景下的监控和优化。内存监控是一个持续完善的课题,咱们并不能一步到位的做完一切事情。更重要的是咱们能持续发现问题,持续做精细化的监控,而不是一直对处于”对当时内存现状不了解,一边填坑又一边挖坑”的阶段。咱们的目标是树立合理的渠道为开发人员处理问题或许及时发现问题。当时云音乐内存监控还归于不断探索和不断完善的阶段,咱们还需求在未来的时刻里配合开发人员不断的优化和迭代。
参考资料
- github.com/square/leak…
- github.com/KwaiAppTeam…
- /post/713472…
- blog.yorek.xyz/android/pai…
本文发布自网易云音乐技能团队,文章未经授权禁止任何形式的转载。咱们常年招收各类技能岗位,假如你预备换作业,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!