从JDK8升级到JDK11,使用G1 GC,HBase性能下降近20%。 JDK到底干了什么?

举报
RyanPeng 发表于 2021/12/17 08:40:10 2021/12/17
【摘要】 HBase从2.3.x开始正式默认的支持JDK11, HBase对于JDK 11的支持指的是HBase本身可以通过JDK11的编译、同时相关的测试用例全部通过。由于HBase依赖Hadoop和Zookeeper,而目前Hadoop和Zookeeper尚未支持JDK11,所以HBase中任然有一个jira来关注JDK11支持的问题。https://issues.apache.org/jira/...

HBase2.3.x开始正式默认的支持JDK11 HBase对于JDK 11的支持指的是HBase本身可以通过JDK11的编译、同时相关的测试用例全部通过。由于HBase依赖HadoopZookeeper,而目前HadoopZookeeper尚未支持JDK11,所以HBase中任然有一个jira来关注JDK11支持的问题。https://issues.apache.org/jira/browse/HBASE-22972.

G1 GCJDK9以后就称为默认的GC,而且HBase在新的版本中也采用G1 GC.对于HBase是否可以在生成环境中使用JDK11?笔者尝试使用JDK11来运行新的HBase,严重JDK11是否比JDK8有优势。

1      环境介绍

验证的方式非常简单,搭建一个3节点的HBase机器,安装HBase,采用的版本为2.3.2,关于HBase环境搭建可以参考官网。

另外为了验证,使用一个额外的客户端机器,通过HBase自带的PerformanceEvaluation工具(简称PE)来验证HBase读、写性能。PE支持随机的读、写、扫描,顺序读、写、扫描等。

例如一个简单的随机写命令如下:

hbase org.apache.hadoop.hbase.PerformanceEvaluation --rows=10000 --valueSize=8000 randomWrite 5

该命令的含义是:创建5个客户端,并且执行持续的写入测试。每个客户端每次写入8000字节,共写入10000.

PE使用起来非常简单,是HBase压测中非常流行的工具,关于PE更多的用法可以参考相关手册。

本次测试为了验证读写性能,采用如下配置:

org.apache.hadoop.hbase.PerformanceEvaluation --writeToWAL=true --nomapred --size=256 --table=Test1 --inmemoryCompaction=BASIC --presplit=50 --compress=SNAPPY sequentialWrite 120

JDK采用JDKu222JDK11.0.8分别进行测试,当切换JDK时,客户端和3HBase服务器同意切换。JDK的运行参数为:

-XX:+PrintGCDetails -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:-ResizePLAB

注意:这里对禁止ResizePLAB是业务根据HBase优化资料设置。

2      测试结果:JDK11性能下降

通过PE进行测试,运行结束有TPS数据,表示性能。


在相同的硬件环境、相同的HBase,仅仅使用不同的JDK来运行。同时为了保证结果的准确性,多次运行,取平均值。测试结果如下:


从表中可以快速的计算得到吞吐量下降,运行时间增加。


结论:使用G1 GCJDK11相对于JDK8来说性能明显下降。

3      原因分析

JDK8JDK11 G1 GC做了非常多的优化用于提高性能。为什么JDK11对于应用的来说更不友好?简单的总结一下从JDK8JDK11做的一些比较大的设计变化,如下表所示:

 

优化点

描述

IHOP启发式设置

IHOP用于控制并发标记的启动时机,在JDK9中引入该优化,根据应用运行的情况,计算IHOP的值,确保在内存耗尽以前启动并发标记。对于性能和运行时间理论上都是正优化,在一些的情况,例如可能会导致性能下降

Full GC的并行话

JDK10中将Full GC从串行实现优化为并行实现,该优化不会产生负面影响

动态线程调整

根据GC工作线程的负载情况,引入动态的线程数来处理任务。该优化会带来正效果,注意不是GC工作线程数目越多GC的效果越好(GC会涉及到多线程的任务窃取和同步机制,过多的线程会导致性能下降)

引用集的重构

引用集处理优化,设置处理大小、将并行修改为并发等

 

由于从JDK8JDK11特性变化太多,对于这样的性能下降问题,如何能快速有效的解决。我们做了如下的尝试

3.1      统一JDK8JDK11的参数,验证效果

由于JDK11JDK8实现变化很多,部分功能完全不同,但是这些变化的功能一般都有参数控制,一种有效的尝试:梳理JDK8JDK11关于G1的参数,将它们设置为相同的值,比如关闭IHOP的自适应,关闭线程调整等。这里简单的给出JDk8JDK11不同参数的比较,如下图所示:


将两者参数都设置和JDK8一样的值,重新验证测试,结果不变,JDK11性能仍然下降

3.2      GC日志分析,确定JDK11性能下降点

对于JDK8JDK11同时配置日志收集功能,重新测试,获得GC日志。通过GC日志分析,我们发现差异主要在G1 young gcobject copy阶段(耗时基本在这),JDK11Young GC耗时大概200msJDK8Young GC耗时大概100ms,两者设置的目标停顿时间都是100ms

JDK11GC日志片段


JDK8GC日志片段


我们对整个日志做了统计,有以下发现:

  • 并发标记时机不同,混合回收的时机也不同;
  • 单次GC中对象复制的耗时不同,JDK11明显更长;
  • 总体GC次数JDK11的更多,包括了并发标记的停顿次数;
  • 总体GC的耗时JDK11更多。

 


针对Young GC的性能劣化,我们重点关注测试了和Young GC相关的参数,例如:调整UseDynamicNumberOfGCThreadsG1UseAdaptiveIHOP GCTimeRatio均没有效果。

尝试使用不同的工具来进一步定位到底哪里出了问题。

3.3      JFR分析-确认日志分析结果

毕昇JDK11JDK8都引入了JFRJFR作为JVM中问题定位的新贵,我们也在该案例进行了尝试,关于JFR的原理和使用,参考本系列的其他文章(JFR原理介绍和使用)。

3.3.1        JDK11总体信息


JDK8中通过JFR收集信息。

3.3.2        JDK8总体信息

 


JFR的结论和我们前面分析的结论一致,JDK11中中断比例明显高于JDK8

 

3.3.3        JDK 11中垃圾回收发生的情况


3.3.4        JDK 8中垃圾回收发生的情况


从图中可以看到在JDK11中应用消耗内存的速度更快(曲线速率更为陡峭),根据垃圾回收的原理,内存的消耗和分配相关。

 

3.3.5        JDK11VM操作


 

3.3.6        JDK8VM操作


通过JFR整体的分析,得到的结论和我们前面的一致,确定了Young GC可能存在问题,但是没有更多的信息。

3.4      火焰图-发现热点

为了进一步的追踪Young GC里面到底发生了什么导致对象赋值更为耗时,我们使用Async-perf进行了热点采集。关于该工具的使用参考本系列的其他文章(如何使用火焰图解决性能问题)

3.4.1        JDK11的火焰图


3.4.2        JDK 11 GC部分火焰图


 

3.4.3        JDK8的火焰图


3.4.4        JDK 8 GC部分火焰图


 

通过分析火焰图,并比较JDK 8JDK 11的差异,可以得到:

JDK 11中,耗时主要在:

  • G1ParEvacuateFollowersClosure::do_void()
  • G1RemSet::scan_rem_set

JDK 8中,耗时主要在:

G1ParEvacuateFollowersClosure::do_void()

更一步,我们对JDK11里面新出现的scan_rem_set()进行更进一步分析,发现该函数仅仅和引用集相关,通过修改RSet相关参数(修改G1ConcRefinementGreenZone),将RSet处理尽可能的移除Young GC的处理。火焰图中参数不再成为热点,但是JDK11仍然性能下降。

比较JDK8JDK11G1ParEvacuateFollowersClosure::do_void()中的不同的,出来数组处理基本没有变化,我们将JDK 11此处的代码修改和JDK8完全一样,但是性能仍然下降。

结论:虽然G1ParEvacuateFollowersClosure::do_void()是性能下降的触发点,但是此处并不是问题的根因,应该是其他的原因造成了该函数调用次数增加或者耗时增加。

3.5      逐个版本验证-最终确定问题

我们分析了所有可能的情况,仍然无法快速找到问题的根源,只能使用最笨的办法,逐个版本来验证从哪个版本开始性能下降。

在大量的验证中,对于JDK9JDK10,以及小版本等都重新做了构建(关于JDK的构建可以参考官网),我们发现JDK9-B74JDK9-B73有一个明显的区别。为此我们分析了JDK9-B73合入的代码。发现改代码和PLAB的设置相关,为此梳理了所有PLAB相关的变动:

 

 

重新修改了代码,打印plab的大小。对比后发现desired_plab_sz大小,在性能正常的版本中该值为1024或者4096(分别是YoungPLAB和OLDPLAB),在性能下降的版本中该值为258。由此确认desired_plab_sz不正确的计算导致了性能下降。

 

3.6      PALB 为什么会引起性能下降?

PLAB是用于GC工作线程的并行复制内存时使用的缓存,用于减少多个并行线程在内存分配时锁竞争。PLAB的大小直接影响GC工作线程的效率。

GC引入动态线程调整的功能时,将原来PLABSize的的大小作为多个线程的总体PLAB的大小,将PLAB重新计算到,如下面代码片段:


其中desired_plab_sz主要来自YoungPLABSize和OldPLABSIze的设置。所以这样的代码修改改变了YoungPLABSizeOldPLABSize参数的语义。

 

另外,在本例中,通过参数显示的禁止了ResizePLAB是触发该问题的必要条件,当打开ResizePLAB后,PLAB会根据GC工作线程晋升对象的大小和速率来逐步调整PLAB的大小。

注意,众多资料说明:禁止ResziePLAB是为了防止GC工作线程的同步,这个说法是不正确的,PLAB的调整耗时非常的小。PLAB是JVM根据GC工作线程使用内存的情况,根据数学模型来调整大小,由于模型的误差,可能导致PLAB的大小调整不一定有人工调参效果好。如果你没有对YoungPLABSizeOldPLABSize进行调优,并不建议禁止ResizePLAB。在HBase测试中,当打开ResizePLABJDK8JDK11性能基本相同,也从侧面说明了该参数的使用情况。

 

3.7      解决方法&修复方法

由于该问题是JDK9引入,在JDK 9JDK 10, JDK 11, JDK 12, JDK 13, JDK 14, JDK 15, JDK 16都会存在性能下降的问题。

我们对该问题进行了修正,并提交到社区,具体见Jirahttps://bugs.openjdk.java.net/browse/JDK-8257145;代码见:https://github.com/openjdk/jdk/pull/1474。该问题在JDK 17中被修复。

同时该问题在毕昇JDK所有版本中第一时间得到解决。

 

当然对于没有自己JDK发行的同学,遇到这个问题,该如何解决?总不能等到JDK 17?一个临时的方法是显示的设置YoungPLABSizeOldPLABSize的值。YoungPLABSize设置为YoungPLABSize* ParallelGCThreads,其中ParallelGCThreadsGC并行线程数。例如YoungPLABSize原来为1024ParallelGCThreads8,在JDK9~16,将YoungPLABSize设置为8192即可。

其中参数 ParallelGCThreads的计算方法为:没有设置该参数时,当CPU个数小于等于8 ParallelGCThreads等于CPU个数,当CPU个数大于8ParallelGCThreads等于CPU个数的5/8.

3.8      小结

本文分享了针对JDK升级后性能下降的解决方法。对于Java开发人员如果遇到此类问题,通常可以先求助社区,但是由于社区响应较慢。自己可以先尝试:

  1. 对齐不同JDK版本的参数,确保参数相同,看是否可以快速重现
  2. 分析GC日志,确定是否由GC引起。如果是建议将所有的参数重新验证,包括移除原来的参数。本例中一个最大的失误是,在分析过程中将原来业务提供的参数ResizePLAB移除重新测试,浪费了很多时间。如果执行该步骤后,定位问题可能可以解决很多时间;
  3. 使用一些工具,比如JFRNMT、火焰图等。本例中尝试使用这些工具,虽然无果,但基本上确认了问题点。
  4. 最后的最后,还是没有结果,请找毕昇JDK社区吧

 

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。