Jmeter压测,BeanShell内存溢出问题的排查及解决
测试场景
需要使用Jmeter对Go语言实现的后端服务执行阶梯递增式压测,每阶梯增加2000线程,每个阶梯维持1小时,直至加压到10000线程。
每秒发送1次请求,每次请求前需要使用到BeanShell PreProcessor获取实时时间,生成动态signature,添加到HTTP请求头中。开启HTTP请求的keep-alive,设置客户端实现方式为HttpClient4。
准备1台8核16G的Linux实例作为控制节点,10台8核16G的Linux实例作为工作节点(jmeter:5.4.1,jdk:1.8.0_291)
Linux及Jmeter配置优化
1、向/etc/sysctl.conf增加以下内容后,执行sysctl -p /etc/sysctl.conf,优化内核相关参数
net.ipv4.ip_forward = 1#开启路由功能
net.ipv4.conf.default.rp_filter = 1#禁用所有IP源路由
net.ipv4.conf.default.accept_source_route = 0#禁用icmp源路由选项
kernel.sysrq = 0#关闭SysRq功能,SysRq代表的是Magic System Request Key
kernel.core_uses_pid = 1 #控制core文件的文件名是否添加pid作为扩展
net.ipv4.tcp_syncookies = 1# tcp syncookie,默认关闭
kernel.msgmnb = 65536 #默认的每个消息队列的最大尺寸(byte),默认为16384
kernel.msgmax = 65536#消息队列中单条消息的最大尺寸(byte),默认8192
kernel.shmmax = 68719476736#共享内存中的最大内存块尺寸(byte),默认33554432(32M),这里是65536M
kernel.shmall = 4294967296 #kernel.shmall的单位是页面数,当前的x86体系上这个单位是4K,这里是2048G的共享内存总量,默认2097152
fs.file-max = 6553600 #系统级最大打开文件数,还要结合limits.conf的soft和hard限制
net.ipv4.tcp_max_tw_buckets = 5000#1st低于此值,TCP没有内存压力,2nd进入内存压力阶段,3rdTCP拒绝分配socket(单位:内存页)
net.ipv4.tcp_sack = 1#定义SYN重试次数
net.ipv4.tcp_window_scaling = 1 #开启窗口缩放功能
net.ipv4.tcp_rmem = 4096 87380 4194304 #接受缓冲的大小:MIN,DEFAULT,MAX
net.ipv4.tcp_wmem = 409616384 4194304 #socket的发送缓存区分配的MIN,DEFAULT,MAX
net.ipv4.tcp_max_syn_backlog = 8192#syn队列,默认1024,1280可能工作不稳定,需要修改内核源码参数
net.core.netdev_max_backlog = 32768#进入包的最大设备队列.默认是300,对重负载服务器而言,该值太低,可调整到2000.
net.core.somaxconn = 32768 #listen()的默认参数,挂起请求的最大数量.默认是128.对繁忙的服务器,增加该值有助于网络性能
net.core.wmem_default = 8388608#表示套接字发送缓冲区大小的缺省值,会覆盖net.ipv4.tcp_wmem的DEFAUL值
net.core.rmem_default = 8388608 #表示套接字接收缓冲区大小的缺省值
net.core.rmem_max = 16777216 #表示套接字接收缓冲区大小的最大值
net.core.wmem_max = 16777216 #表示套接字发送缓冲区大小的最大值,会覆盖net.ipv4.tcp_wmem的MAX值
net.ipv4.tcp_timestamps = 0 #禁用时间戳,时间戳可以避免序列号的卷绕
net.ipv4.tcp_synack_retries = 2 #syn-ack握手状态重试次数,默认5,遭受syn-flood攻击时改为1或2
net.ipv4.tcp_syn_retries = 2 #外向syn握手重试次数,默认4
net.ipv4.tcp_tw_recycle = 1 #开启 TCP 连接中 TIME-WAIT sockets 的快速回收,默认为 0 ,表示关闭。
net.ipv4.tcp_tw_reuse = 1#开启重用。允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,默认为 0 ,表示关闭;
net.ipv4.tcp_mem = 94500000 915000000 927000000 #1低于此值,TCP没有内存压力,2在此值下,进入内存压力阶段,3高于此值,TCP拒绝分配socket.上述内存单位是页
net.ipv4.tcp_max_orphans = 3276800 #选项用于设定系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上,如果超过这个数字,孤立连接将立即被复位并打印出警告信息
net.ipv4.tcp_fin_timeout = 30 #修改系統默认的 TIMEOUT 时间
net.ipv4.tcp_keepalive_time = 300 #表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为5分钟。
net.ipv4.ip_local_port_range = 102465000#表示用于向外连接的端口范围。缺省情况下过窄:32768到61000,改为1024到65535。
net.ipv4.ip_conntrack_max = 655360#增大iptables状态跟踪表
net.ipv4.netfilter.ip_conntrack_tcp_timeout_established = 180#设置默认 TCP 连接时长为180秒
2、向/etc/security/limits.conf增加以下内容
*soft nofile 65535
*hard nofile 65535
再向/etc/profile增加以下内容,执行source /etc/profile,持久优化文件句柄
ulimit -n 65535
4、jmeter.properties配置设置
httpclient.reset_state_on_thread_group_iteration=false #线程组每次循环是否重置连接状态
client.tries=3 #初始化远程工作节点重试次数
client.retries_delay=3000 #初始化远程工作节点超时时间
httpclient4.retrycount=1 #请求重试次数
httpclient4.idletimeout=60000 #连接空闲时间
httpclient4.time_to_live=60000 #连接保持时间
5、bin/jmeter文件,JVM优化
: "${HEAP:="-Xms12g -Xmx12g -Xss512k -XX:MaxMetaspaceSize=1g"}"
: "${GC_ALGO:="-XX:+UseG1GC -XX:+DisableExplicitGC -XX:MaxGCPauseMillis=100 -XX:G1ReservePercent=20 -XX:+UseStringDeduplication -XX:ConcGCThreads=2"}"
问题现象
远程调用执行,持续40分钟以后,设置的12GB堆空间不足,jmeter发送的请求量大幅下降。
jmeter脚本使用ps -ef|grep java|awk '{printf $2}'|head -1|xargs -I {} jstat -gc {} 2000,查看JVM的GC状态。可查看到大量老年代资源累积,最终触发FullGC,而后不断频繁触发FullGC。
GC信息排查问题
1、使用jmap -dump:live,format=b,file=heapLive.hprof ${jmeter_pid}命令,获取堆快照文件
2、将文件下载到本地后,执行jvisualvm命令,打开jvisualvm,并加载堆快照文件,分析堆空间使用信息
heap信息通过分析堆快照,我们可以看到,堆空间的使用,主要被bsh类下的实例及String类型使用。
通过堆快照分析,我们定位出了Jmeter的性能问题主要出现在Beanshell生成signature给http请求使用这块。首先,我们先禁用掉BeanShell PreProcessor组件,直接以每台1000线程(共计10000线程)的并发量运行,看内存溢出问题是否消失,来验证我们的猜想。
禁用掉BeanShell PreProcessor组件后,以每台1000线程(共计10000线程)的并发量运行,最终看到,工作节点的内存最终维持到了8GB左右,并可持续运行40min以上,证明了我们的猜想是正确的。
所以我这边,首先是对BeanShell中的代码进行了优化,使用StringBuilder拼接字符串,将signature生成并put后,设置所有new出的对象为null。启用BeanShell PreProcessor组件,而后再次以每台1000线程(共计10000线程)的并发量运行,工作节点最终仍然出现了内存溢出的问题,说明存在内存溢出的地方可能并不是beanshell的代码。
4、网上查阅资料,最终确定是Jmeter的BeanShell组件存在内存溢出的问题,官方文档的说明如下:
beanshell在长时间运行的Jmeter脚本中,使用BeanShell组件,会占用大量内存;如果需要长时间运行,则需要设置BeanShell组件中reset Interpreter选项为True。
但是通过设置BeanShell组件中reset Interpreter选项为True,我们再次运行Jmeter测试,会发现虽然内存增加的问题得到了较大的改善,但测试的吞吐量无法达到期望效果,这显然也是我无法接受的。
通过jstack -F ${jmeter_pid}命令,发现大量的线程处于阻塞状态,通过阅读Jmeter源码发现解释器重置,每次都需要重新执行java.lang.ClassLoader.loadClass方法,该方法存在一个synchronized同步块阻塞了线程,从而导致吞吐率无法提高。
BeanShellServer loadClass问题解决
由于BeanShell组件存在内存溢出的问题,且在设置reset Interpreter选项为True后,吞吐量会被限制,我不得不选择放弃使用BeanShell来实现生成signature的功能。
目前可供我使用的方案有两种:
1、使用官方支持的Groovy或Jexl3等开发脚本来实现(通过查询网上资料,了解到Groovy虽然内存占用在合理范围内,但和BeanShell存在相同的类加载引发的线程阻塞问题,导致吞吐率很低)
2、使用Jmeter自定义函数(我目前使用的是这一种方式,经测试能有效的避免内存溢出问题,并且不会存在线程阻塞导致吞吐量不达标的问题)
关于如何实现Jmeter自定义函数,详见文章:https://www.jianshu.com/p/37f1e8329fc7