当前位置: 首页 > 后端技术 > Java

记得一个JMeter压测HTTPS性能问题

时间:2023-04-01 20:25:39 Java

介绍:在使用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上下文是相同的*/protectedstaticfinalThreadLocalresetStateOnThreadGroupIteration=ThreadLocal.withInitial(()->布尔值.FALSE);/***重置SSL状态。
*为了做到这一点,我们需要:*

    *
  • 在SSLManager上调用resetContext()
  • *
  • 关闭保持SSL状态的当前空闲或过期连接
  • *
  • 从{@linkHttpClientContext}
  • *
*@paramjMeterVariables{@linkJMeterVariables}*@paramclientContext{@linkHttpClientContext}*@parammapHttpClientPerHttpClientKey映射{@linkPair}持有{@linkCloseableHttpClient}and{@linkPoolingHttpClientConnectionManager}*/privatevoidresetStateIfNeeded(JMeterVariablesjMeterVariables,HttpClientContextclientContext,Map>mapHttpClientPerHttpClientKey){if(resetStateOnThreadGroupIteration.get()){//关闭当前线程对应连接池超时,空闲连接,重置连接池状态closeCurrentConnections(mapHttpClientPerHttpClientKey);//移除令牌clientContext.removeAttribute(HttpClientContext.USER_TOKEN);//重置SSL上下文((JsseSSLManager)SSLManager.getInstance()).resetContext();//将标志设置为false以确保只有第一个采样器进入此逻辑resetStateOnThreadGroupIteration.set(false);}}@OverrideprotectedvoidnotifyFirstSampleAfterLoopRestart(){log.debug("notifyFirstSampleAfterLoopRestart调用"+"withconfig(httpclient.reset_state_on_thread_group_iteration={})",RESET_STATE_ON_THREAD_GROUP_ITERATION);resetStateOnThreadGroupIteration.set(RESET_STATE_ON_THREAD_GROUP_ITERATION);}在每次基于ApacheHTTPClient4的HTTP采样器执行时,都会调用resetStateIfNeeded方法,在进入方法时读取httpclient.reset_state_on_thread_group_iteration配置,即如果resetStateOnThreadGroupIteration为true,则重置当前线程的连接池状态,重置SSL上下文,然后将resetStateOnThreadGroupIteration设置为false。由于JMeter的并发是基于线程实现的,resetStateOnThreadGroupIteration开关放在ThreadLocal中,在每次循环开始时,会调用notifyFirstSampleAfterLoopRestart方法来重置switch,运行一次后强制switch设置为false,这样可以保证每次循环只有第一个sampler进入这个逻辑,即每次循环只执行一次。这次总结解决了JMeter5.0及以上压力测试HTTPS协议的性能问题。经验总结如下:如果想让压力机发挥最大性能,可以将https.sessioncontext.shared设置为true,这样所有的线程都会共享同一个SSL上下文。会频繁握手,但无法模拟真实情况下的多用户场景。如果想模拟多个用户,循环执行某个动作,即一个线程组每次都模拟同一个用户的行为,可以将httpclient.reset_state_on_thread_group_iteration设置为false,也可以大大提高性能单机压力测试HTTPS。如果想让每个线程组每个周期模拟不同的用户,需要设置httpclient.reset_state_on_thread_group_iteration=true。这个时候压测会模拟多个用户频繁的SSL握手,压机的性能是最低的。根据经验,单机的上限是50并发左右。这也是JMeter5.0版本之后的默认设置。阿里云JMeter压测阿里云PTS压测工具[1]支持原生JMeter脚本,HTTPS压测中httpclient.reset_state_on_thread_group_iteration已默认设置为false,大大提升了压力机在HTTPS压测时的性能,节省了压力。测量成本。如果想模拟最真实的用户访问情况进行压测,可以通过修改JMeter环境中的自定义属性配置[2],将httpclient.reset_state_on_thread_group_iteration设置为true。此外,阿里云JMeter压测还有以下优势:零运维成本支持分布式压测,压测秒级监控即时监控,实时观察系统性能水平,支持RPS模式,直观直观全球系统吞吐量测算Region发起百万级并发流量,模拟真实用户分布支持阿里云VPC压测,一键接入云内网环境支持JMeter客户端插件,本地快速发起云压测不允许转载。