一、引言
Apache Jmeter 2.13(以下简称Jmeter2)版本后,2.X系列就作古了。前些日子,Apache Jmeter 3.0(以下简称Jmeter3)版本正式发布,新生的事物,功能肯定强大了很多,但作为开源产品,稳定性自然要打些折扣,一位同学前几天在使用Jmeter3时不幸中招。
二、问题描述
原本好用的JDBC请求脚本,压测数据库,使用Jmeter3版本时直接OOM,回退至Jmeter2,一切正常,啥也别说,肯定是又出Bug了,本着求实的精神,咱来看看Jmeter3为毛OOM了。
三、排查过程
1.找出OOM的对象
首先在Jmeter3中配置输出HeapDump文件,在jmeter.sh中增加以下一行:
JVM_ARGS="-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=~/gc.hprof
然后启动Jmeter程序,观察Jmeter的JVM内存变化,再次OOM了,把输出的gc.hprof文件,利用Memory Analyzer来分析,得到一堆index文件如下图:
利用Eclipse的MAT插件,加载全部的index文件,查看哪个对象占用了内存,如下图:
看到AbstractJDBCTestElement类中的perConnCache对象,占用了441M的内存,确定是这个对象内存泄露了。
2.找出导致OOM的原因
先来看看这个perConnCache究竟是做什么用的
/** |
这是一个HashMap,用来缓存每个连接(即Connection,下文简称为Conn)的PreparedStatements。而笔者的压测脚本中,最大Conn数设定的是10个,就是说,perConnCache的size最大应该是10才对,OK,估计问题就出在这里,笔者猜测应该是每次请求的Conn都具有不同的HashCode。
为了验证这个猜测,笔者用远程Debug来查看一下,每次获取Conn时的HashCode。
Debug后发现,在Jmeter3中有一个有趣的现象,每次获取到的Conn果然具有不同的HashCode,但是Jmeter3与DB的Conn总数却刚好是10个,就是说——物理连接的总数量并没有变,但每次从连接池获取到的Connection对象却一直在变。
笔者又Debug了一下Jmeter2,Jmeter2的结果是符合预期的,物理连接的总数是10个,perConnCache的size也是10,每次从连接池中获取的Conn也都能在perConnCache中命中。
3.锁定问题点
不论是Jmeter2还是Jmeter3,JDBCSampler的实现都是一样的:
如图所示,这里有3个关键操作,获取Conn,执行SQL,关闭Conn。在执行SQL的步骤中,perConnCache会进行缓存验证,如果没有命中,则把新的Conn缓存。
每次getConnection()都是连接池的操作,所以,问题就应该出在Jmeter3使用的连接池实现上。
4.OOM原因解析
下面来解释一下,为什么Jmeter3的连接池就会出现OOM的问题。
因为Jmeter3使用的是dbcp2,在getConnection()方法中,每次不是直接返回Connection对象,而是都会新new一个PoolGuardConnectionWrapper对象,看下面的代码:
可以看到,Conn并没有变,但是每次取到的对象都是重新new的包裹类PoolGuardConnectionWrapper,每次都是新的对象,HashCode当然就不一样了,在HashMap无法命中,所以最终每一次getgetConnection()时,perConnCache都产生一个新成员,最终OOM。
为啥Jmeter2没有这个问题呢?Jmeter2使用的是excalibur-datasource,每次Connection取到的就是com.mysql.jdbc.JDBC4Connection对象本身,自然在perConnCache里面就可以命中了
四、修改方式:
方式一:抛弃dbcp2,回退到excalibur-datasource,问题解决
方式二:修改perConnCache缓存机制,不是用Connection作为Key,而是利用sql的内容作为key,当然也可以直接删除这个Cache。
笔者尝试去掉了缓存机制,没有再发生OOM问题,压测结果对整体的TPS影响也不大
五、结论:
Jmeter3稳定性还有待考验,近期不推荐使用,建议使用Jmeter2.13来替代。