1、前言
使用JMeter进行性能测试,有时候需要编写程序辅助,主要的编程方式主要有BeanShell、JSR223脚本(Groovy,Javascript)。这里对BeanShell、Groovy编程的性能测试过程中出现的问题和解决方法进行探讨。本文结合实际项目-车联网的性能测试场景进行说明。
2、车联网背景
车载终端设备(OBD)是一款具备通行功能的移动设备,安装在汽车上,实时采集汽车行驶过程中的各项数据,并通过通信模块发送给后台,后台的计算平台会对汽车的各项数据进行计算(油耗、里程、驾驶行为分析等)。OBD设备发送给后台的数据包主要包含:时间点、经纬度、速度、当前油耗、当前里程。
3、性能测试场景
目标:N个车载终端设备给后台发送多段行驶里程数据,后台计算的性能瓶颈。OBD通过TCP协议发送汽车行驶过程的数据包,所以一段行程里程中,包含有N个数据包,而且每个数据包的时间应该为线性增长的,而且其经纬度、速度、油耗均需要动态变化。那么进行性能测试时,时间、经纬度、速度、油耗、里程这些都需要通过编程实现。
4、BeanShell编程
使用BeanShellPreProcessor组件
根据规范的报文格式,传入时间点、经纬度、里程、油耗等,调用java程序合成16进制的传输报文。
主Sampler- “Java请求-报文合成”,为一个java程序,负责根据传入的参数,合成16进制的可供OBD设备使用的传输报文。

处理GPS包报文的BeanShell
主要生成时间点、经纬度数据:主要逻辑已经封装到JAVA程序,BeanShell为最低限度(最少编程)的程序调用。

处理CAN包报文的BeanShell
主要生成时间点、速度、里程、油耗数据:主要逻辑已经封装到JAVA程序,BeanShell为最低限度(最少编程)的程序调用。

压力测试
100个线程施压,4核CPU,8G内存,JVM配置的最大堆3.6G,JVM配置线程栈256K。
压力机本身系统监控
可以看到,7分钟内已经把3.6G的堆内存全部吃完,最后OOM内存耗尽系统崩溃。

JVM内存分析
BeanShell内部对象占用的堆内存超过了50%。

结论
压力线程开始阶段工作正常,后阶段由于内存不足,频繁full GC导致阻塞时间加长;
内存占用非常高,很短时间就把内存消耗殆尽,30分钟内程序崩溃;
JMeter这种使用BeanShell的方式,存在严重的内存泄露;
此种常规的压力测式方法,不能形成有效的压力。
5、Groovy编程
JMeter官网推荐使用JSR223脚本进行替代BeanShell。使用Groovy脚本可拥有类编译和编译缓存的优势。
使用JSR223 PreProcessor组件

处理报文的Groovy程序
程序逻辑与BeanShell版本一致,只是换成groovy语言改写。
压力测试
100个线程施压,4核CPU,8G内存,JVM配置的最大堆3.6G,JVM配置线程栈256K,持续30分钟。
压力机本身系统监控
可以看出:
堆内存占用大幅降低(对比BeanShell版本),已使用堆最高为2.4G,为可使用3.6G堆的2/3或66.7%。
类装载曲线持续陡峭,说明Groovy相关类在测试期间持续加载。

压力线程监控
压测线程大部分时间都不干活,而处于阻塞和睡眠状态中。

压测线程堆栈分析
对JVM进行heap dump后,对其线程堆栈分析。发现阻塞点在类加载。
java.lang.ClassLoader.loadClass为什么会导致阻塞?我们来看看其源码就明白了,加载类的方法有一个synchronized同步块,需要线程竞争锁。


结论
内存占用在合理范围内;
需要持续加载类;
压力测试线程工作不合理,大部分时间处于由于类加载引发的阻塞;
由于大量线程阻塞,吞吐率很低;
在JMeter中,不适合使用groovy编程进行压力测试。
6、BeanShell编程再探
由于BeanShell的Interpreter存在内存泄露,常规方法无法支持长时间的压力测试。JMeter官网推荐,在使用BeanShell进行长时间测试时,打开选项“Reset bsh.Interpreterbefore each call”,则在每次调用BeanShell程序前,都把解释器重置,以释放解释器之前占用的内存。
6.1、重置选项的BeanShell
BeanShellPreProcessor

压力测试
100个线程施压,4核CPU,8G内存,JVM配置的最大堆3.6G,JVM配置线程栈256K,持续30分钟。
压力机本身系统监控
内存使用量较少,已使用堆<1G,已分配堆<1.25G;
类加载曲线平稳,没有出现抖动;

压力线程监控
从开始到结束,线程都阻塞严重。

阻塞分析
由于解释器重置,所以每次都需要重新加载类,线程阻塞点仍然是类加载:

JVM使用CPU分析
CPU时间主力:
bsh.classpath.ClassManagerImpl.classForName()方法:类加载,占比48.8%;
bsh.BshClassManager.plainClassForName方法:类加载相关,占比41.2%;
BinaryTCPClient4OBD.read()方法:报文TCP数据包发送给后台后等待响应,占比6.7%。

压测吞吐率分析
吞吐率表现尚可,TPS一直稳定在200左右,最终TPS=196.5/s。
结论
内存占用相对较低,30分钟内程序不会OOM崩溃;
每次调用BeanShell前重置解释器,从而每次解释脚本均需要重新尝试加载类,导致线程大量阻塞;
重置解释器,释放了内存,但阻塞了线程,导致吞吐率无法提高;
性能比groovy版本好,在压力测试持续30分钟,总吞吐率在200/s左右的情况下,是一个可选的方案。
6.2、改进的重置选项BeanShell
有没有一种既能通过重置解释器来释放内存,而线程阻塞时间大大缩短的鱼和熊掌兼得?如果实现的话,内存降下来,线程阻塞减少,吞吐率升高,那就^_^太好了^_^。
Idea: 固定重置,改为按需重置。
为此,需要一点微创新。
微创新
修改JMeter源代码
修改org.apache.jmeter.util包下的BeanShellTestElement类:加入一个按需重置标识。

按需重置
JMeter脚本中,加入计数器,用来计算迭代次数:

到达一定的迭代次数,触发重置:

压力测试
100个线程施压,4核CPU,8G内存,JVM配置的最大堆3.6G,JVM配置线程栈256K,持续30分钟。
压力机本身系统监控
内存使用率较高60%-80%,内存通过GC后回收顺利,占用率仍在合理范围;
类加载曲线平稳。

压力线程监控
进入reset阶段,线程阻塞时间变长;reset结束,类加载完成后,线程阻塞时间减少;
随着运行时间的增长,阻塞的频率升高。


JVM使用CPU分析
CPU时间主力:
BinaryTCPClient4OBD.read()方法:报文TCP数据包发送给后台后等待响应,占比69.1%,为原始重置版本的10倍;
bsh.classpath.ClassManagerImpl.classForName()方法:类加载,占比16.5%;
bsh.BshClassManager.plainClassForName方法:类加载相关,占比12.9%;

压测吞吐率分析
平时TPS在450+,reset阶段TPS在350左右,总TPS=462.5/s;
吞吐率是原始重置版本的2.35倍。
结论
按需重置版本,内存占用较高,但处于合理范围内;
按需重置版本,CPU的使用效率大幅增加,从而形成有效压力;
按需重置版本,测试吞吐率倍增;
当前的按需重置版本,随着时间的增长,线程的阻塞频率升高,按需重置的算法有改进空间。
7、BeanShell内存泄露分析
官方解释
Each BeanShelltest element has its own copy of the interpreter (for each thread). If the testelement is repeatedly called, e.g. within a loop, then the interpreter isretained between invocations unless the "Reset bsh.Interpreter before eachcall" option is selected.
Somelong-running tests may cause the interpreter to use lots of memory; if this is the case try using the reset option.[reference 2]
技术分析
JMeter中的请求组件(如HTTPSampler)对象实例,在循环执行过程中,并不是每次都创建一个新的对象来执行,实际上只会创建一次然后重复使用。
JMeter使用XStream技术从JMX文件反序列化得到各组件实例,在启动测试线程时,clone一份给测试线程,测试线程使用过程中不会创建请求组件对象。
请求组件引用栈:HTTPSamper -》BeanShellPrePrecessor –》BeanShellInterpreter –》 bsh.Interpreter–》 NameSpace, BshClassManager, Parser. 其中,BeanShell执行过程中的变量、类等都存放在全局的NameSpace对象(内部数据结构为HashTable)里。
内存泄露应该发生在NameSpace中:

单线程调试JMeter,发现NameSpace中的methods和classManager.listeners发生内存泄露:

8、总结
Groovy编程进行性能测试,压力线程阻塞严重,CPU使用效率低,效果不佳。
BeanShell编程进行性能测试,不带重置选项,可能有严重的内存泄露,短时间内即把内存消耗殆尽,会导致OOM,引发系统崩溃。
BeanShell编程进行性能测试,带重置选择,压力线程阻塞十分严重,CPU使用效率不高,吞吐率不高,但可长时间运行,适合进行稳定性测试。
BeanShell编程进行性能测试,按需重置Interpreter,阻塞、CPU效率、吞吐率等,表现最佳,性能最好。
复杂BeanShell脚本,长时间循环运行,可能引发内存泄露而系统崩溃。解决方法为:使用重置选项,或优化脚本代码。
压力测试,BeanShell程序应该去除不必要中间结果、变量赋值,不引用其他脚本,尽可能的短小精悍。
网友评论