一文看懂快手KOOM高性能监控方案

7,201 阅读7分钟

1.KOOM简介

2.KOOM基础使用流程

3.KOOM的dump触发时机

4.KOOM高性能的fork dump

5.线上采集性能对比


1.KOOM简介

2020年快手开源KOOM (OOM Killer),KOOM是首个开源的线上内存溢出OOM问题的解决方案。

KOOM已在快手全量业务中应用,OOM率降低了80%以上,效果显著。


客户端OOM问题是比较难处理的。一般的崩溃问题,只要获取崩溃瞬时的数据,比如异常类型、堆栈等就都比较容易解决,而 OOM 往往是多个因素累加在一起形成的,并且和用户操作的过程有很大关系。没有成熟的线上内存监控体系,一般我们只能采用线下复现的方式,效率很低下,不能满足需求。


主流的LeakCanary可以针对Activity或者Fragment的来优化OOM问题,但是受限于性能原因,线上无法大规模使用,一搬都是用于线下定位问题使用。还有其他的方案比如腾讯Matrix的ResouceCanary,也是基于LeakCanary为基础做优化,没有完全解决监控过程中的性能问题。


KOOM也是沿用行业思路,基于LeakCanary进行自研改造,补足原来的短板,打造了一套一站式服务的监控系统。



KOOM是在客户端完成内存监控后,自动或者手动将报告上传云端,文件经过优化处理大小在KB级别,运行时无感知,流量占用非常少,可以大规模应用。


2.KOOM基础使用流程

github地址:

github.com/KwaiAppTeam…

近2000 Star


最后生成的json报告形式,比如CommonUtils持有Activity时

{
  "analysisDone":true,
  "classInfos":[
    {
      "className":"android.app.Activity",
      "instanceCount":4,
      "leakInstanceCount":3
    },
    {
      "className":"android.app.Fragment",
      "instanceCount":4,
      "leakInstanceCount":3
    },
    {
      "className":"android.graphics.Bitmap",
      "instanceCount":115,
      "leakInstanceCount":0
    },
    {
      "className":"libcore.util.NativeAllocationRegistry",
      "instanceCount":1513,
      "leakInstanceCount":0
    },
    {
      "className":"android.view.Window",
      "instanceCount":4,
      "leakInstanceCount":0
    }
  ],
  "gcPaths":[
    {
      "gcRoot":"Local variable in native code",
      "instanceCount":1,
      "leakReason":"Activity Leak",
      "path":[
        {
          "declaredClass":"java.lang.Thread",
          "reference":"android.os.HandlerThread.contextClassLoader",
          "referenceType":"INSTANCE_FIELD"
        },
        {
          "declaredClass":"java.lang.ClassLoader",
          "reference":"dalvik.system.PathClassLoader.runtimeInternalObjects",
          "referenceType":"INSTANCE_FIELD"
        },
        {
          "declaredClass":"",
          "reference":"java.lang.Object[]",
          "referenceType":"ARRAY_ENTRY"
        },
        {
          "declaredClass":"com.kwai.koom.demo.CommonUtils",
          "reference":"com.kwai.koom.demo.CommonUtils.context",
          "referenceType":"STATIC_FIELD"
        },
        {
          "reference":"com.kwai.koom.demo.LeakActivity",
          "referenceType":"instance"
        }
      ],
      "signature":"569fc01daea06b6cc679bd61725affd163d022c3"
    }
  ],
  "runningInfo":{
    "analysisReason":"RIGHT_NOW",
    "appVersion":"1.0",
    "buildModel":"MI 9 Transparent Edition",
    "currentPage":"LeakActivity",
    "dumpReason":"MANUAL_TRIGGER",
    "jvmMax":512,
    "jvmUsed":2,
    "koomVersion":1,
    "manufacture":"Xiaomi",
    "nowTime":"2021-09-07_16-07-34",
    "pss":32,
    "rss":123,
    "sdkInt":29,
    "threadCount":17,
    "usageSeconds":40,
    "vss":5674
  }
}

主要是:类信息、gc引用路径、运行基本信息


2.1接入方式

image.png

2.2初始化

image.png

2.3 Java-OOM报告获取

    当内存异常时,采集内存镜像、分析之后会生成一份json文件报告

     手动获取

image.png

实时监听报告生成

image.png      设置Uploader

    

image.png

2.4 自定义需求

      配置KConfig设置需要的各项参数

image.png       默认heapRatio的设置,会根据最大内存来调整设置更合理的值

image.png    

  • 应用内存>512M 设置80%
  • 应用内存>256M 设置85%
  • 应用内存>128M 设置90%

3.KOOM的监控触发时机

LeakCanary 和 Matrix 都是在 Activity的onDestroy 时触发泄漏检测,而KOOM是使用阈值检测的方法来触发。


KOOM改用阈值检测,有什么好处?

传统的方案是onDestroy()后连续触发两次GC,并检查引用队列,判定Activity是否发生了泄漏,但频繁GC会造成用户可感知的卡顿,KOOM为实现无感触发设计了全新的监控模块,通过无性能损耗的内存阈值监控来触发镜像采集。

将对象是否泄漏的判断延迟到了解析时,阈值监控只要在子线程定期获取关注的几个内存指标即可,性能损耗忽略不计。

KOOM 1.1.0版本源码整体结构

image.png

大致了解一下有什么东西

analysis:内存镜像解析模块,对hprof文件的分析

dump:   内存镜像采集模块,dump出hprof文件

monitor:内存监控模块 

report:      文件报告模块

MonitorThread.java

image.png

  • moniter.isTrigger()是否触发采集
  • 进行轮询检测

isTrigger核心方法

image.png

触发的工作做完了,接下来就是做dump工作。

image.png

                                                            触发时机流程示意图

4.KOOM的高性能采集fork dump

像LeakCanary的 dump hprof是通过虚拟机提供的API dumpHprofData实现的,这个过程会“冻结”整个应用进程,导致几秒甚至10秒以上无法操作。

KOOM 相比较 LeakCanary 和 Matrix 来说有点不同,后俩者由于 dump 的整个过程会影响到主进程,所以基本应用与线下监控,而 KOOM 提出了 fork dump 的概念,能在 dump 分析内存泄漏的时候而不影响到主进程的应用运行,所以适合使用在线上监控。


KOOM高性能dump:

KOOM利用 Linux 的Copy-on-write机制(COW),fork子进程dump内存镜像,来解决这一问题,fork成功以后,父进程立刻恢复虚拟机运行,子进程dump内存镜像期间不会受到父进程数据变动的影响。

COW机制:

了解两个函数:fork()和exec(),exec是一组相关函数的统称,包括execl()、execlp()、execv()、execle()等。

fork()会创建一个子进程,子进程的是父进程的副本;

exec()重新装载程序,清空数据;

一般的fork()会直接将父进程的数据拷贝到子进程中,拷贝完之后,会执行exec(),父进程和子进程之间的数据段和堆栈是相互独立的。

image.png

Copy-on-write写时复制

为了节省fork子进程的内存消耗和耗时,fork出的子进程并不会copy父进程的内存,而是和父进程共享内存空间,父子进程只在发生内存写入操作时,系统才会分配新的内存为写入方保留单独的拷贝。

这就相当于子进程保留了fork瞬间时父进程的内存镜像,且后续父进程对内存的修改不会影响子进程。

上面的isTrigger之后到dump的流程,通过onTigger回调到dump流程

doHeapDump进入dump过程

对应KOOM核心代码ForkJvmHeapDumper.java的dump()

image.png

Copy-on-write的fork创建出的子进程,与父进程共享内存空间。既保留了镜像数据,同时子进程dump的过程也不会影响主进程执行。

                    KOOM fork dump流程示意图


实际fork过程:

暂停虚拟机需要调系统库,但谷歌从Android 7.0开始对调用系统库做了限制,基于此前提,快手自研了kwai-linker组件,绕过了这一限制


然后是解析流程

image.png 把结果add到GCPath

image.png 不同的Detector有不同的isLeak策略


5.采集性能对比

采集内存镜像需要暂停虚拟机,以确保在内存数据拷贝到磁盘的过程中,引用关系不会发生变化,暂停时间通常长达10秒以上,对用户来讲是难以接受的,这也是LeakCanary不推荐线上使用的重要原因之一。

KOOM利用Linux Copy-on-write机制fork子进程dump大大提高了dump效率。

线上真实用户的内存镜像,普通dump和fork子进程dump阻塞用户使用的耗时对比

image.png 耗时差距都在100倍以上

解析性能优化

KOOM没有采用LeakCanary1.0版本的HAHA解析引擎,使用HAHA解析过程中非常容易OOM,且解析速度极慢。LeakCanary2.0版本使用Shark新版解析引擎,KOOM基于Shark引擎进行解析。

兼容性

暂未支持Android R以上

只支持AndroidX,不支持Android Support Library

小结

内存阈值检测方式,将对象是否泄漏的判断延迟到了解析时,避免传统的频繁主动gc

使用COW机制来fork子进程做dump操作,能大大减少阻塞时长

附录

blog.csdn.net/Kwai_tech/a…  快手技术团队关于OOM治理

github.com/KwaiAppTeam… KOOM的github