解决内存泄漏(1)-ApacheKylin InternalThreadLocalMap泄漏问题分析

313 阅读5分钟

开源产品迭代快,但也容易存在隐患。有时会遇到意料之外的问题,需要研究代码解决。内存泄漏是一个很常见的问题,会导致服务不稳定,影响可用性。本文讲述了如何使用MAT和BTrace解决apache kylin内存泄漏问题,重点阐明如何定位问题,分析原因,验证猜想。

希望能抛砖引玉,让大家遇到类似内存泄漏问题时能够有所借鉴。

背景

公司自助报表业务从kylin2.0集群迁移到Kylin3.0集群时,Kylin job角色每隔2,3天所有进程OOM一遍,服务很不稳定,需要尽快解决。

调查思路

构建服务是32GB堆内存的java进程,OOM要么是内存确实不够用,要么是内存泄漏。考虑到报表业务之前使用的kylin2.0也是32GB内存,没遇到类似的OOM,所以首先怀疑内存泄漏,可能是2.0以后的新特性引入了问题。

再考虑到kylin3.0小集群用了很久也没有OOM,怀疑跟业务量和用法有关系。报表业务每天要构建几千次,构建模型各异,容易暴露出问题。一般来说,容器,Netty,ThreadLocal是内存泄漏的重灾区。要调查内存泄漏,可以使用内存分析工具MAT来分析堆内存。找到哪些对象内存用的多,以及对象的引用关系。找到怀疑的对象后,再做进一步的代码分析,用BTrace打印调用日志确定问题的原因,最后解决问题。

定位问题

java启动参数一般都会加-XX:+HeapDumpOnOutOfMemoryError,OOM时可以dump堆内存,生成java_pidxxxx.hprof文件供我们分析。

可以使用MAT(Memory Analyzer Tool)分析hprof文件。MAT的使用可自行谷歌,有需要可以给我留言。要注意的是要堆内存有几十GB,需要几十GB的空闲内存做分析。一般把MAT部署在内存空闲的测试服务器上,用vncserver提供可视化操作支持。

拷贝hprof文件到MAT服务器,启动MAT,加载hprof文件。

加载时如果报错

An internal error occurred during: 
"Parsing heap dump from **\java_pid6564.hprof'".Java heap space

需要增加MAT的启动内存,至少比hprof文件大

open the MemoryAnalyzer.ini file
change the default -Xmx1024m to a larger size

MAT分析结果

img

img

img

上图可以看出,占用内存较多的对象是调度线程或其他线程的线程对象,每个线程对象占用了较大内存。每次dump,线程对象大小不一,从200MB到1GB都有。但同一次dump,每个线程对象的大小几乎是一样的。线程中主要是InternalThreadLocalMap对象占用了内存,其中成员变量Object[]数组的length有几千万甚至上亿,并且每个线程的数组长度都是一样。这十有八九是内存泄漏。

那么InternalThreadLocalMap是什么对象呢?

分析原因

git blame,发现InternalThreadLocalMap 是KYLIN-3716 引入的,从jira看是借鉴了netty的相关代码,将ThreadLocal的引用替换为InternalThreadLocal(netty叫FastThreadLocal),从而让查询请求内部加载上下文的速度更快。那么加快的原理又是什么?

ThreadLocal在线程内部用map维护了线程内各个本地对象的引用,使用时通过查map定位到具体的对象。

而InternalThreadLocal在线程内部维护了本地对象数组,使用时查数组。对java来说,用数组索引查对象要比查map快。从实现来看,每次构建InternalThreadLocal对象,Ojbect[]的的有效索引位会加1,来缓存本地线程内对应的对象引用。但索引位只有加没有减,如果InternalThreadLocal对象构建的特别多,Object[]的length就会很大,就会有内存问题。

一般来说,ThreadLocal对象都是当做静态成员变量使用的,一个进程有几十个对象就很多了,替换成InternalThreadLocal,数组也不会很大,不会有问题。但如果不加控制,当成普通的成员变量来用,就会有内存泄漏的风险。

梳理了一下kylin项目的引用,发现大多数引用都是静态成员变量,但有几个类比如DataTypeSerializer,其成员变量也使用了InternalThreadLocal,这就有可能会内存泄漏。理论上将用在对象成员变量上的引用改回ThreadLocal问题就解决了,但稳妥起见,还是应该先验证一下我们的猜想是否正确。

验证猜想

使用BTrace来验证我们的猜想。BTrace脚本可以加拦截代码,打印方法调用的上下文。把BTrace脚本挂到JVM上,不用改变线上代码,不用重启,就可以输出我们想要的日志,是定位线上问题的神器。如何使用可自行谷歌,有需要可以给我留言。

通过BTrace脚本的日志,可以看到构建服务确实会频繁构建InternalThreadLocal对象,主要是DataTypeSerializer对象初始化时调用的。几个小时内InternalThreadLocal构建次数就达到了几十上百万。而查询服务就没有频繁构建的问题,猜想被验证正确。

优化结果

将对象成员变量的InternalThreadLocal引用改回ThreadLocal,重启服务。InternalThreadLocal对象运行几天也只有十几次构建,不随时间增多,没再出现OOM,问题解决。相关pr已贡献给kylin社区。

作者:初晓【滴滴出行资深软件开发工程师】