起因
基于一些原因,需要将kettle6.1升级到新版的kettle8.2。升级后发现kettle8每隔几天出现
GC overhead limit exceeded 或者 OutOfMemoryError:Java heap space;
一开始猜测kettle8为了提高性能,而牺牲更多的堆内存,便从开始的Xmx2048m 增加到 Xmx4096m.
可是没过几天还是一样出现问题,又从4096m增加到6144m.
问题依旧得不到解决.
通过排查日志发现内存溢出的问题发现: 定时重复执行的作业出现数据库连接不上,
导致好几天的作业一直都在报错数据库连接拒绝,一直到内存溢出,kettle奔溃.
排查发现,只要作业是正常执行,kettle8.2就不会出现内存溢出的问题.
但是只要有作业一直报异常,那么只要1-2天(也可能更短,看作业执行频率),
kettle8.2就会内存溢出挂掉,
kettle6.1不会出现此种情况.
场景重现
测试环境部署kettle8.2,模拟重现.
在kettle8.2 启动脚本增加
-Dcom.sun.management.jmxremote.port=9008 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
本地jconsole可以远程实时监控kettle运行时的内存与线程情况(具体操作自行百度)
增加
-Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=n
本地idea可以远程debug
在idea中git clone kettle8.2的源码.
往kettle上发了10个作业,每隔5秒重复执行.其中9个作业是正常执行的,
1个作业连接一个不存在的数据库,会一直报连接拒绝错误
jconsole监控如下图
红框时间区间是该错误job停用的时间段,期间内存能维持稳定。
在测试服务器上使用命令jmap -heap,发现老年代内存居高不下,无法回收。
排查
再使用命令jmap -histo:live >> a.log(JVM会先触发gc)排查哪些对象没有被回收
327 83712 org.pentaho.di.trans.Trans
327 20928 org.pentaho.di.trans.steps.selectvalues.SelectValuesMeta
327 20928 org.pentaho.di.trans.steps.tableinput.TableInputMeta
327 31392 org.pentaho.di.trans.steps.insertupdate.InsertUpdateMeta
866 27712 org.pentaho.di.trans.TransHopMeta
基本上可以断定是每次错误执行的作业中的转换(trans)没有被GC回收。
但是仅根据堆内存中的对象数量无法判断是哪里还在引用这些对象。
使用命令 jmap -dump:live,format=b,file=./dump.dat 生成内存镜像
使用命令 jhat -J-Xmx1024M ./dump.dat (jdk自带分析工具)
执行后等待console 中输入start HTTP server on port 7000 即可使用浏览器访问 IP:7000
随机查看了几个引用trans对象的对象,看不出什么
kettle的对象互相引用太多太复杂,jhat这个工具很难直观找到最上层的引用对象
下载dump文件到本机,使用eclipse的插件 memory anaylize 分析dump
如下图
在树形图上容易trans对象没有被回收是因为被最上层的HashMap引用
在树形图中找到容易定位问题代码对象是
org.pentaho.osgi.blueprint.collection.utils.ServiceMap
查看源码,猜测作业执行异常的情况,没有执行itemRemoved方法,导致本该被回收的对象在内存越积越多
在源码中新增、修改两个方法打上断点,开启远程debug 如下图
调试一个正常的作业和错误的作业,发现确实如推测的,执行报错的作业最终没有执行itemRemoved.
调试过程跳过
定位具体代码 org.pentaho.di.trans.Trans
public void execute( String[] arguments ) throws KettleException {
prepareExecution( arguments );
startThreads();
}
正常执行的job 会在 prepareExecution方法中的transMeta.setEmbeddedMetastoreProviderKey(....)
中触发serviceMap的itemAdded()
并在startThreads()方法中调用transMeta.disposeEmbeddedMetastoreProvider()中
触发serviceMap的itemRemoved()
异常的job会在prepareExecution方法中初始化step时抛出数据库连接异常,导致后续不会进入到startThreads().
这下内存溢出的问题明确了,稍微修改源码,打包替换原先的kettle-engine.8.2.0.jar.
至此,kettle8.2内存泄漏问题彻底解决
后续
1、对比kettle6和kettle8的源码,发现了 EmbeddedMetastoreProvider
是kettle8新增的一个点,所以kettle6此场景不存在内存泄漏
2、测试过kettle8.3,也存在与8.2一样的内存泄漏问题.