原文链接:https://www.heapdump.cn/article/2601008
编者按:在HBase业务场景中,我试图将JDK从8升级到11,并使用G1GC作为垃圾收集器,但性能下降了20%。是什么导致了性能下降?如何定位解决?本文介绍了如何利用JFR、火焰图等工具识别问题,并通过逐一版本验证,最终找到导致性能问题的代码。在毕升JDK,每个人都带头解决问题,并最终将其推给上游社区。希望通过本文的介绍,让读者了解如何解决大版本升级中遇到的性能问题;同时也提醒Java开发者正确使用参数(使用前要理解参数的含义)。
HBase官方从2.3开始默认支持JDK11 . x HBase对JDK11的支持意味着h base本身可以通过JDK 11编译,所有相关测试用例通过。因为HBase依赖Hadoop和Zookeeper,而最新的Hadoop和Zookeeper还没有支持JDK11,所以HBase还有一个jira关注JDK11的支持。
具体参考:https://issues.apache.org/jira/browse/HBASE-22972.
G1GC从JDK9开始成为默认GC,HBase在新版本中也采用了G1GC。HBase可以在生产环境中使用JDK11吗?笔者尝试使用JDK11运行新的HBase,验证JDK11是否比JDK8有优势。
此外,为了验证,还额外使用了一台客户端机器,通过自己的PerformanceEvaluation工具(简称PE)来验证HBase的读写性能。PE支持随机读写扫描,顺序读写扫描。
例如,一个简单的随机写命令如下:
HBA seorg . Apache . Hadoop . h base . performance evaluation-rows=10000-valueSize=8000 random write 5
该命令的意思是创建5个客户端并执行连续写测试。每个客户端一次写入8000字节,总共10000行。
PE使用起来非常简单,是HBase压力测量中非常受欢迎的工具。关于PE的更多用法,请参考相关手册。
为了验证读写性能,本测试采用以下配置:
org . Apache . Hadoop . h base . performance evaluation-write towal=true-noma pred-size=256-table=Test1-in memory compaction=BASIC-pre split=50-compress=snappysequentialwrite 120
JDK分别由JDK8u222和JDK11.0.8测试。切换JDK时,客户端和3-HBase服务器会统一切换。JDK的运行参数是:
-XX: printgc details-XX: use G1 GC-XX: maxgcpausemillis=100-XX:-ResizePLAB
注意:这里禁止使用ResizePLAB,因为业务是根据HBase优化数据设置的。
在相同的硬件环境下,相同的HBase,只使用不同的JDK运行。同时为了保证结果的准确性,多次运行,取平均值。测试结果如下:
从表中可以很快计算出吞吐量减少,运行时间增加。
结论:与JDK8相比,使用G1GC后JDK11的性能明显下降。
因为JDK8到JDK11的特性变化太多,为了快速有效的解决这个性能下降问题,我们做了以下尝试。
将两个参数设置为与JDK8相同的值,并重新验证测试。结果不变,JDK11的性能依然下降。
JDK11中GC日志片段:
JDK8中GC日志片段:
我们对整个日志做了统计,有以下发现:
并发标记时机不同,混合回收的时机也不同;
单次GC中对象复制的耗时不同,JDK11明显更长;
总体GC次数JDK11得更多,包括了并发标记的停顿次数;
总体GC的耗时JDK11更多。
针对YoungGC的性能劣化,我们重点关注测试了和YoungGC相关的参数,例如:调整UseDynamicNumberOfGCThreads、G1UseAdaptiveIHOP、GCTimeRatio均没有效果。
下面我们尝试使用不同的工具来进一步定位到底哪里出了问题。
毕昇JDK11和毕昇JDK8都引入了JFR,JFR作为JVM中问题定位的新贵,我们也在该案例进行了尝试,关于JFR的原理和使用,参考本系列的技术文章:JavaFlightRecorder-事件机制详解。
JDK8中通过JFR收集信息。
JFR的结论和我们前面分析的结论一致,JDK11中中断比例明显高于JDK8。
从图中可以看到在JDK11中应用消耗内存的速度更快(曲线速率更为陡峭),根据垃圾回收的原理,内存的消耗和分配相关。
通过JFR整体的分析,得到的结论和我们前面的一致,确定了YoungGC可能存在问题,但是没有更多的信息。
为了进一步的追踪YoungGC里面到底发生了什么导致对象赋值更为耗时,我们使用Async-perf进行了热点采集。关于火焰图的使用参考本系列的技术文章:使用perf解决JDK8小版本升级后性能下降的问题
通过分析火焰图,并比较JDK8和JDK11的差异,可以得到:
在JDK11中,耗时主要在:
1)G1ParEvacuateFollowersClosure::do_void()
2)G1RemSet::scan_rem_set
在JDK8中,耗时主要在:
1)G1ParEvacuateFollowersClosure::do_void()
下一步,我们对JDK11里面新出现的scan_rem_set()进行更进一步分析,发现该函数仅仅和引用集相关,通过修改RSet相关参数(修改G1ConcRefinementGreenZone),将RSet的处理尽可能地从YoungGC的操作中移除。火焰图中参数不再成为热点,但是JDK11仍然性能下降。
比较JDK8和JDK11中G1ParEvacuateFollowersClosure::do_void()中的不同,除了数组处理外其他的基本没有变化,我们将JDK11此处的代码修改和JDK8完全一样,但是性能仍然下降。
结论:虽然G1ParEvacuateFollowersClosure::do_void()是性能下降的触发点,但是此处并不是问题的根因,应该是其他的原因造成了该函数调用次数增加或者耗时增加。
我们分析了所有可能的情况,仍然无法快速找到问题的根源,只能使用最笨的办法,逐个版本来验证从哪个版本开始性能下降。
在大量的验证中,对于JDK9、JDK10,以及小版本等都重新做了构建(关于JDK的构建可以参考官网),我们发现JDK9-B74和JDK9-B73有一个明显的区别。为此我们分析了JDK9-B73输入的代码。发现该代码和PLAB的设置相关,为此梳理了所有PLAB相关的变动:
B66版本为了解决PLABsize获取不对的问题(根据GC线程数量动态调整,但是开启UseDynamicNumberOfGCThreads后该值有问题,默认是关闭)修复了bug。具体见jira:DeterminingthedesiredPLABsizeadjuststothethenumberofthreadsatthewrongplace
B74发现有问题(desired_plab_sz可能会有相除截断问题和没有对齐的问题),重新修改,具体见8079555:REDO-DeterminingthedesiredPLABsizeadjuststothethenumberofthreadsatthewrongplace
B115中发现B74的修改,动态调整PLAB大小后,会导致很多情况PLAB过小(大概就是不走PLAB,走了直接分配),频繁的话会导致性能大幅下降,又做了修复NetPLABsizeisclippedtomaxPLABsizeasawhole,notonaperthreadbasis
重新修改了代码,打印PLAB的大小。对比后发现desired_plab_sz大小,在性能正常的版本中该值为1024或者4096(分别是YoungPLAB和OLDPLAB),在性能下降的版本中该值为258。由此确认desired_plab_sz不正确的计算导致了性能下降。
PLAB是GC工作线程在并行复制内存时使用的缓存,用于减少多个并行线程在内存分配时的锁竞争。PLAB的大小直接影响GC工作线程的效率。
在GC引入动态线程调整的功能时,将原来PLABSize的大小作为多个线程的总体PLAB的大小,将PLAB重新计算,如下面代码片段:
其中desired_plab_sz主要来自YoungPLABSize和OldPLABSIze的设置。所以这样的代码修改改变了YoungPLABSize、OldPLABSize参数的语义。
另外,在本例中,通过参数显式地禁止了ResizePLAB是触发该问题的必要条件,当打开ResizePLAB后,PLAB会根据GC工作线程晋升对象的大小和速率来逐步调整PLAB的大小。
注意,众多资料说明:禁止ResziePLAB是为了防止GC工作线程的同步,这个说法是不正确的,PLAB的调整耗时非常的小。PLAB是JVM根据GC工作线程使用内存的情况,根据数学模型来调整大小,由于模型的误差,可能导致PLAB的大小调整不一定有人工调参效果好。如果你没有对YoungPLABSize、OldPLABSize进行调优,并不建议禁止ResizePLAB。在HBase测试中,当打开ResizePLAB后JDK8和JDK11性能基本相同,也从侧面说明了该参数的使用情况。
由于该问题是JDK9引入,在JDK9,JDK10,JDK11,JDK12,JDK13,JDK14,JDK15,JDK16都会存在性能下降的问题。
我们对该问题进行了修正,并提交到社区,具体见Jira:https://bugs.openjdk.java.net/browse/JDK-8257145;代码见:https://github.com/openjdk/jdk/pull/1474;该问题在JDK17中被修复。
同时该问题在毕昇JDK所有版本中第一时间得到解决。
当然对于短时间内无法切换JDK的同学,遇到这个问题,该如何解决?难道要等到JDK17?一个临时的方法是显式地设置YoungPLABSize和OldPLABSize的值。YoungPLABSize设置为YoungPLABSize*ParallelGCThreads,其中ParallelGCThreads为GC并行线程数。例如YoungPLABSize原来为1024,ParallelGCThreads为8,在JDK9~16,将YoungPLABSize设置为8192即可。
其中参数ParallelGCThreads的计算方法为:没有设置该参数时,当CPU个数小于等于8,ParallelGCThreads等于CPU个数,当CPU个数大于8,ParallelGCThreads等于CPU个数的5/8)。
本文分享了针对JDK升级后性能下降的解决方法。Java开发人员如果遇到此类问题,可以按照下面的步骤尝试自行解决:
1.对齐不同JDK版本的参数,确保参数相同,看是否可以快速重现;
2.分析GC日志,确定是否有GC引起。如果是,建议将所有的参数重新验证,包括移除原来的参数。本例中一个最大的失误是,在分析过程中没有将原来业务提供的参数ResizePLAB移除重新测试,浪费了很多时间。如果执行该步骤后,定位问题可能可以节约很多时间;
3.使用一些工具,比如JFR、NMT、火焰图等。本例中尝试使用这些工具,虽然无果,但基本上确认了问题点;
本文分享了针对JDK升级后性能下降的解决方法。Java开发人员如果遇到此类问题,可以按照下面的步骤尝试自行解决:
1.对齐不同JDK版本的参数,确保参数相同,看是否可以快速重现;
2.分析GC日志,确定是否有GC引起。如果是,建议将所有的参数重新验证,包括移除原来的参数。本例中一个最大的失误是,在分析过程中没有将原来业务提供的参数ResizePLAB移除重新测试,浪费了很多时间。如果执行该步骤后,定位问题可能可以节约很多时间;
3.使用一些工具,比如JFR、NMT、火焰图等。本例中尝试使用这些工具,虽然无果,但基本上确认了问题点;
4.最后的最后,如果还是没有解决,请联系毕昇JDK社区(https://gitee.com/openeuler/bishengjdk-8)。毕昇JDK社区每双周周二举行技术例会,同时有一个技术交流讨论GCC、LLVM和JDK等相关编译技术
上一篇:青海有没有医科大学,多少分