Jmeter使用groovy导致OOM分析过程



本次我们以一次压测项目的实施过程为背景,详细介绍其具体的问题分析过程,无论是任何性能问题的分析都是需要通过现象分析本质,一步步抽茧剥丝找到问题的源头。


1、压测项目描述

压测目标:TPS达到5000

脚本描述:使用Jmeter压测工具编写,tcp sample为核心使用JRSS22预处理器和if控制器

场景描述:执行稳定性测试场景,运行12小时

环境描述:Jmeter压测环境使用Linux作为Agent压测

问题描述:在执行稳定性场景过程中,Jmeter客户端报错

错误信息如下:java.lang.OutOfMemoryError:Metaspace

2、分析问题过程

1)怀疑Jmeter本身的Metaspace空间设置过小导致OOM。做如下验证:

a、修改Metaspace空间大小,jmeter.sh启动文件中修改为1024

-Xms4g-Xmx4g -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m,并重启Jmeter

b、配置监控Jmeter内存的工具Glowroot,以及监控命令jstat

c、重新执行稳定性测试场景进行验证

d、验证结果:随着场景的执行时间,执行一段时间后仍然有内存OOM问题

通过在Jmeter上配置glowroot监控工具重点监控metaspace区域的使用情况

Jmeter使用groovy导致OOM分析过程_Java



通过glowroot监控发现metaspace区域使用逐渐增长

通过jstat命令监控JVM使用如下:发现频繁的发生full gc


Jmeter使用groovy导致OOM分析过程_Java_02

2)针对目前的OOM进行细化分析


通过以上的验证,怀疑有内存泄露的问题,主要的分析过程如下:

a、通过杀取对应的heapdump文件(杀取heapdump文件可以通过glowroot)进行细化分析

Jmeter使用groovy导致OOM分析过程_Java_03


b、通过分析heapdump文件,发现存在大量的groovyClassLoader占用,申请未被卸载释放。


Jmeter使用groovy导致OOM分析过程_Java_04

c、通过网络查询发现groovy确实存在此问题,具体的信息如下:

1)groovy每执行一次脚本,都会生成一个脚本的class对象,并new一个InnerLoader去加载这个对象,而InnerLoader和脚本对象都无法在fullGC的时候被回收,因此运行一段时间后将PERM占满,一直触发fullGC

2)所有的脚本都是由GroovyClassLoader加载的,每次加载脚本都会生成一个新的InnerLoader去加载脚本,但InnerLoader只是继承GroovyClassLoader,加载脚本的时候,也是交给GroovyClassLoader去加载

3)每次groovy编译脚本后,都会缓存该脚本的Class对象,下次编译该脚本时,会优先从缓存中读取,这样节省掉编译的时间。这个缓存的Map由GroovyClassLoader持有,key是脚本的类名,而脚本的类名在不同的编译场景下(从文件读取脚本/从流读取脚本/从字符串读取脚本)其命名规则不同,当传入text时,class对象的命名规则为:"script" + System.currentTimeMillis() +Math.abs(text.hashCode()) + ".groovy"

因此,每次编译的对象名都不同,都会在缓存中添加一个class对象,导致class对象不可释放,随着次数的增加,编译的class对象将PERM区撑满。

3、针对Groovy问题进行分析解决并验证

a、分析Jmeter脚本,脚本中使用到的groovy的地方有两处一个为JSR223预处理程序和if控制器${__groovy()}

Jmeter使用groovy导致OOM分析过程_Java_05

Jmeter使用groovy导致OOM分析过程_Java_06

b、网络上提供了3种方法解决此问题:

1) 降低groovy版本,从现有的2.4.16回退到2.1.7

2) 强制classloader.clearCache(),在脚本中增加此代码

3)在saveservice.properties配置文件中增加-Dgroovy.use.classvalue=true重启Jmeter

遗憾的是,分别验证以上三种方式均未解决此问题。


c、通过尝试替换掉_groovy解决此问题

1)将JSR223预处理器更改为Beanshell预处理器,验证后仍然存在此问题

2)怀疑if控制器${__groovy()}用法导致,修改此方法将${__groovy()}修改为${__javaScript()},进行场景验证。

d、验证结果分析

1)将脚本中的if控制器中的_groovy修改为${__javaScript()}后,场景验证执行处理效率较低(TPS只能达到2500)。

2)怀疑_javaScript消耗导致,将${__javaScript()}修改为${__jexl3()}后进行继续验证,场景执行处理效率恢复是__javaScript的2倍多TPS达到6000。

3)TPS目标达到后,继续执行稳定性验证,12小时执行未出现异常,此问题得到解决。


备注:虽然对于脚本验证来说将_groovy修改为_javaScript_jexl3都没有问题,但是执行压测的结果差异性很大,建议采用Jmeter使用说明的优先使用__jexl3

后续研究_javaScript的主要的消耗点,尝试是否有优化方法。


总结提示:

1、Jmeter为开源工具,开源工具是需要经过不断的实践纠错总结达到版本的迭代的,多发现问题为开源贡献一份力量

2、Jmeter中的While循环控制器和if控制器使用中,避免使用${__groovy()}${__javaScript()},使用${__jexl3()}

3、网络上的解决方案只能作为参考,还是需要做实践验证,有可能存在场景和代码差异,要多动手实践验证。

4、并不是所有的性能问题都是被测系统的问题,首先需要排除压测客户端本身的问题导致,才能达到压测服务方的目的。

5、本篇文章中使用到了Heapdump分析方法和Glowroot使用,请参考其他公众号文章。