介绍:在使用JMeter压测时,发现同样的后端服务,单机500并发下,HTTP和HTTPS协议压测RT差距非常大。同时观察到后端服务的各个监控指标水位都很低,因此怀疑性能瓶颈在JMeter压力客户端。作者:Fuyi问题背景在使用JMeter进行压测时,发现同一个后端服务在单机500并发下,HTTP和HTTPS协议压测的RT差距非常大。同时观察到后端服务的各个监控指标水位都很低,因此怀疑性能瓶颈在JMeter压力客户端。问题分析的起点:垃圾回收首先观察压机中CPU占用率和内存占用率高,详细查看各个线程的CPU占用率和内存占用率:top-Hp{pid}发现CPU占用率进程接近100%GC线程的CPU使用率高,再看gc的频率和耗时。发现每秒都有YoungGC,累计耗时比较长。所以,从频繁的GC入手,定位问题。java/bin/jstat-gcutil{pid}1000压测时,对JMeter的运行进程进行HeapDump后,分析堆内存:可以看到cacheMap对象占用了93.3%的内存,使用通过SSLSessionContextImpl类引用并分析源码可以看出,在构造每个SSLSessionContextImpl对象时,都会初始化两个软引用Cache,sessionHostPortCache和sessionCache。因为是软引用,JVM只有在内存不足的时候才会回收这类对象。//默认缓存大小privatefinalstaticintDEFAULT_MAX_CACHE_SIZE=20480;//包私有SSLSessionContextImpl(){cacheLimit=getDefaultCacheLimit();//默认缓存大小,这里默认是20480timeout=86400;//默认,24小时//使用软引用//这里初始化了两个默认大小为20480的缓存,这是频繁GC的原因sessionCache=Cache.newSoftMemoryCache(cacheLimit,timeout);sessionHostPortCache=Cache.newSoftMemoryCache(cacheLimit,timeout);}//获取默认缓存大小privatestaticintgetDefaultCacheLimit(){如果(defaultCacheLimit>=0){返回defaultCacheLimit;}OnelseOnisSSL&Logger。("ssl")){SSLLogger.warning("无效的系统属性javax.net.ssl.sessionCacheSize,"+"使用默认会话缓存大小("+DEFAULT_MAX_CACHE_SIZE+")代替");}}catch(Exceptione){//不太可能,为了安全记录它if(SSLLogger.isOn&&SSLLogger.isOn("ssl")){SSLLogger.warning("系统属性javax.net.ssl.sessionCacheSize是"+"不可用,使用默认值("+DEFAULT_MAX_CACHE_SIZE+")代替");}}returnDEFAULT_MAX_CACHE_SIZE;}通过以上代码,发现sessionCache和sessionHostPortCache缓存的默认大小是DEFAULT_MAX_CACHE_SIZE,也就是20480。对于我们的压测场景来说,如果每次都重新建立连接,这个缓存根本就不需要。看代码逻辑,我们发现其实可以通过javax.net.ssl.sessionCacheSize来设置缓存的大小,在JMeter启动的时候,加入JVM参数-Djavax.net.ssl.sessionCacheSize=1,设置缓存大小为1,重新测试验证,观察GC,可以看到,YGC明显下降,从每秒1次,下降到5-6秒1次。然后观察压测的RT,结果...结果是1800ms。原来100ms的业务压缩到1800ms。看来不是SSLSession缓存的问题。回到GC的耗时分析,仔细看看。其实只有一次FullGC,阻塞的时间并不多。YoungGC虽然频繁,但是阻塞时间很短,不足以让CPU加解密SSL。所有的计算时间片都被抢占。看起来压力只是大量的SSL握手,造成性能瓶颈。思路调整:为什么SSL握手频繁回到问题背景,我们在做压力测试,单机运行高并发模拟用户。出于性能原因,完全可以在一次握手后共享SSL连接,而无需后续握手。为什么?为什么JMeter握手如此频繁?带着这个疑问,看了下JMeter官方文档,果然有惊喜!原来JMeter有2个开关来控制是否重置SSL上下文选项。第一个是https.sessioncontext.shared来控制是否全局共享同一个SSLContext。如果设置为true,则每个线程共享相同的SSL上下文。性能要求最低,但不模拟真正的多用户SSL握手。第二个开关httpclient.reset_state_on_thread_group_iteration是线程组每次循环是否重置SSL上下文。5.0以后默认为true,也就是说每次循环都会重置SSL上下文。看来这是频繁SSL握手的原因。问题验证回归测试会在jmeter.properties中配置每个线程周期,不会重置SSL上下文,在PTS控制台再次启动压测,RT直接下降10倍。httpclient.reset_state_on_thread_group_iteration=false修改前和修改后的源码验证下面从源码层面来分析一下JMeter是如何实现SSL上下文的循环重置的。代码如下:/***是否应该重置SSL状态/上下文*任何基于HC的实现的共享状态,因为SSL上下文是相同的*/protectedstaticfinalThreadLocal
*为了做到这一点,我们需要:**
*@paramjMeterVariables{@linkJMeterVariables}*@paramclientContext{@linkHttpClientContext}*@parammapHttpClientPerHttpClientKey映射{@linkPair}持有{@linkCloseableHttpClient}and{@linkPoolingHttpClientConnectionManager}*/privatevoidresetStateIfNeeded(JMeterVariablesjMeterVariables,HttpClientContextclientContext,Map
