前言

Arm架构以其兼具性能与功耗的特点,在智能终端以及嵌入式领域得到了广泛的使用,不断扩大其影响力。而在PC端以及数据中心,之前往往是x86架构在其中发挥着主要的作用。*近,随着人工智能、云计算等技术的兴起,5G网络的不断成熟,万物互联的时代是的应用的需求越来越多样化,使得对于芯片架构的需求也越来越多样化。

Arm架构在提供可靠性能的基础上,低功耗、低开销的特点使得它被越来越广泛的应用到数据中心和云计算中,成为其中必不可缺少的重要组成部分。亚马逊投入大量精力自研Arm服务器,并应用到AWS服务中,*多实现了成本45%的降低;阿里巴巴也在云服务中大量采用Arm服务器,并积*参与Linaro,Adoptium等组织,不断推动Arm架构的发展。

*近几年,腾讯对于Arm架构的需求也不断增加,各个产品线也不断引入Arm服务器,对于Arm架构软件的需求也在不断增长。KonaJDK团队在腾讯公司内部提供高性能、高稳定性的商用JDK版本,坚定地将Arm架构作为KonaJDK重点支持的架构之一,不断扩展JDK在Arm架构的功能,并不断提高Arm架构中JDK的性能。

%title插图%num

随着Arm架构在终端和云计算场景的广泛应用,JDK需要做好对于Arm架构的支持工作,才能更好地得到发展。目前在JDK社区,Arm架构属于*梯队支持架构。而对于Arm架构而言,Java语言“一次编译,到处运行” 的特性适合业务应用无缝推广到Arm平台,而JDK则是Java应用运行的必要条件。JDK对于Arm架构的支持,也是Arm生态推广的有力支撑。在这个过程中,KonaJDK团队希望和Arm紧密合作,共同发展。

腾讯和Arm在JDK方面的合作交流

KonaJDK

目前腾讯和Arm在JDK方面已经有了深入的交流和合作。双方针对JDK在Arm架构常见的性能问题,对于Arm架构新特性的支持情况等方面进行了广泛和深入的讨论,通过性能测试、数据交流、技术研讨等形式不断推动JDK在Arm架构的发展。

KonaJDK团队Arm平台优化技术介绍

KonaJDK

目前在Arm架构,KonaJDK平台已经发布了JDK8和JDK11两个版本,在2021晚些时候还会发布*新的JDK17版本。Kona JDK团队从功能、性能多方面出发,在Arm架构支撑KonaJDK的通用特性,并针对架构特征进行优化,保证Java应用向Arm平台迁移的一致性,为Arm架构推广做好准备。

ZGC:

GC使得程序不再需要手动控制内存的释放,有效的降低了内存管理相关错误产生的可能性。但是,对于GC算法而言,如何准确高效的进行内存清理是一个复杂的过程。随着业务需求的不断发展,GC算法也在不停地迭代,只有针对不同的业务目标,选择*合适的GC算法,才能够更好的帮助业务实现其目标。近几年,随着服务器硬件性能越来越强劲,其软件应用往往也需要更大的堆,从10G到100G,甚至TB级别。在这种环境下,传统的CMS、G1等GC算法,其停顿时间往往随着堆大小的增长而增加,对于超大堆在触发Full GC的时候,甚至可能产生分钟级别的停顿,这样对于延迟敏感的应用来说,GC 停顿已经成为阻碍 其广泛应用的一大顽疾,需要更适合的 GC 算法以满足这些业务的需求。

ZGC 是由JEP333引入JDK,希望彻底解决GC停顿带来的延迟问题,其设计目标为:每次GC停顿时间控制在10ms以下;相对于G1 GC,吞吐率下降不超过15%;支持大堆和特大堆,并且停顿时间不随着堆大小的增长而增长。ZGC从 JDK11 开始推出实验性版本,并随着JDK新版本发布不断补充完善,*终在JDK15中成为正式版本,保证了 Java 停顿时间不会随着堆大小和业务规模的增加而增长。为对GC停顿要求高的业务提供了一种更好的选择。

%title插图%num

图 1 ZGC性能(出自The Design of ZGC,Per Lidén)

KonaJDK团队为了满足业务的需求,在Tencent Kona JDK11版本中,完善了ZGC功能的补全,并进行了长期的验证落地,使得对GC停顿敏感的业务也能够在JDK11版本中满足对于低GC延迟的需求。JDK11在2018 年下半年发布,属于Long-Term Support版本,而后续LTS版本为JDK17,预计将于2021年9月发布,中间其他版本属于过渡开发版本,没有持续的更新和修复。因此,KonaJDK团队选择在JDK11完善ZGC的功能,满足业务的需要,即使后续JDK17发布之后,业务版本更新也需要一个过程,在这期间,仍然需要JDK11的支持。

对于Arm架构而言,在JDK11支持ZGC相对于x86架构是一个更大的挑战。x86架构从JDK11开始ZGC就作为Experimental特性开始发布,但是在Arm架构,从JDK13才有对于ZGC的支持。KonaJDK团队进行了大量的工作完成了Arm架构在JDK11中对于ZGC的支持:

  • 需要选择JDK在Arm架构中合适的提交移植到JDK11版本
  • 从JDK11到JDK13,ZGC代码以及Hotspot代码经过了多次重构,在代码移植过程中需要分析代码重构的功能以及影响,或者移植相关重构代码,或者根据JDK11对相关代码进行适配修改
  • 根据Arm架构的特征,适配团队对于ZGC的优化、功能增强以及Bug修复。
  • Arm属于RISC架构,而且使用弱有序内存模型,因此在适配相关汇编代码(特别是ZGC使用的barrier)时,需要仔细斟酌指令的选择,在保证正确性的基础上尽可能的降低开销,提高效率
  • 在Arm平台进行充分、全面的测试,保证相关代码的健壮性

KonaJDK团队在Arm结构支持ZGC的过程中,遇到的*大困难在于如何正确添加barrier指令保证正确性。由于Arm使用弱有序内存模型,在x86平台能够正确执行的代码在Arm架构下可能由于缺少必要的barrier导致产生随机错误。KonaJDK团队在初步完成ZGC支持代码之后,进行ZGC压力测试过程中,发现存在执行若干次GC之后,存在JDK随机崩溃的现象,发生几率几千分之一。通过对错误现场的分析,大概率怀疑是缺少必要的barrier所致。尝试通过对社区代码以及ZGC逻辑对问题进行分析,在这个过程中,JDK13和JDK11代码结构的不同进一步加大了分析的难度,*终KonaJDK团队完成该问题的修复,ZGC代码在Arm架构连续运行数百万次无问题。

和其他GC算法一样,ZGC也有其适用的业务场景。ZGC算法*大的优势是能够将停顿时间控制在10ms以下,特别适合对于停顿时间敏感的业务。但是为了实现如此短的停顿时间,ZGC的代价是一部分性能损失和内存消耗。ZGC通过将若干任务进行并发化改造,使得若干之前必须在停顿时完成的工作,可以和应用代码并发执行,有效的降低了必须的停顿时间。但是这种并发执行,以及其引入的各类Barrier,也会导致一定程度的应用吞吐率下降。通过整个 OpenJDK 社区的持续投入,当前 ZGC 在性能损失场景中的性能下降已经控制在很小的范围内。对于性能来说,如充足的内存下即大堆场景,ZGC 在各类 Benchmark 中能够超过 G1 大约 5% 到 20%,而在小堆情况下,则要低于 G1 大约 10%。

因此,不同的业务需要根据实际的情况,选择更为合适的GC算法,来保证吞吐率和停顿时间都能够满足业务的需求。目前来看,如果业务应用使用了超大堆(几十G甚至上百G)为了避免传统G1等GC算法Full GC时带来的几十秒甚至分钟级别的停顿,建议使用ZGC。另外如果业务对于停顿时间的有着严格的时限要求,那么也建议使用ZGC。

KonaFiber

应用在需要并发执行多项任务的时候,会创建多个线程,每个线程负责一项任务,从而实现任务的并发执行。但是,随着业务规模的不断增大,如果仍然为每一个任务创建一个线程,由于线程本身内存消耗较大,会导致占用大量的内存。另外,线程切换需要内核完成,大量的线程存在时,其频繁的切换开销也会影响并发执行的效率。协程就是为了解决这种情况而诞生的。协程是一种轻量级的线程,兼顾开发效率和执行效率。协程的切换在用户态完成,比线程切换开销小很多,同时对于内存的需求更低,相对的需要应用代码编写时关注部分协程切换的工作。协程相对于线程,在高并发场景能够取得更好的性能,应用越来越广泛。OpenJDK也启动了Java协程原生支持项目:Project Loom,开发时间超过3.5年,并在不断发展完善,即将成为Experimental特性。

KonaFiber是KonaJDK团队实现的协程方案,它在兼容OpenJDK社区Loom API的同时,提供了更好的切换性能,不过需要部分额外的内存开销。KonaFiber根据业务的需要,目前在JDK8和JDK11实现,和社区兼容的API使它成为可以和社区方案一起长期演进的协程方案。目前KonaFiber已经完成对于Arm架构的支持,能够满足Arm架构应用对于协程的需求。

%title插图%num

图 2 KonaJDK和Loom对比

为了满足业务的需求,提供更好的协程切换性能,KonaFiber采用了基于JKU的StackFul有栈方案,为每一个协程创建独立的堆栈。当进行协程切换的时候,JDK在对于协程Pin状态检测以及上下文保存之外,只需要修改Frame Pointer和Stack Pointer的值就可以完成协程的切换工作,逻辑简单且性能开销很小。不过相对于社区的方案,KonaFiber的StackFul方案对于内存的使用要多一些,更适用于对于内存消耗不太敏感,但是对于性能更敏感的业务场景。性能数据如图 2所示,左图表示在不同协程数量情况下,每秒内协程切换次数对比;右图对内存消耗进行了对比。

%title插图%num

图 3 KonaFiber性能对比

KonaFiber的实现注重优化以及代码重构,通过多种方式不断进行优化:

  • 协程轻量化,不断优化降低协程的资源消耗
  • 按需创建,根据业务的需要创建协程,降低内存使用
  • GC优化,优化实现,降低协程对GC引入的开销
  • 稳定性修复,通过广泛的测试以及业务适配,提高健壮性

相对于OpenJDK社区的协程方案Loom,KonaFiber提供了更高更稳定的调度性能。图 3对比了KonaFiber和Loom在不同协程数量情况下的每秒调度次数。

%title插图%num

图 4 调度性能对比

目前KonaFiber在KonaJDK8中已经开源,后续也会在KonaJDK11中开源,KonaJDK也会持续跟进Loom社区并不断完善KonaFiber的实现。

OWST优化

GC运行过程中,存在若干GC线程并行处理各种任务,但是不同任务的处理时间不等,使得各个GC线程之间负载分配并不平衡。JDK中通过如下的方式来平衡各个GC线程之间的负载,降低GC的停顿时间:当一个GC线程执行完成它被分配的任务之后,会查看其它GC线程的任务队列,如果存在这个线程可以执行的任务,那么它会将该任务“偷取”过来并执行。该过程持续循环直到GC结束。该方案实现了负载的自动平衡,但是执行过程中,由于可能多个GC线程同时“偷取”任务,在线程数量较多时,锁的竞争会比较激烈,同时抢锁过程中,各个GC线程的自旋等待也会导致一定的性能开销,使得该算法实际表现差强人意。

为了优化这个过程,Google在ISMM 2016发表了的论文提出了一种新的负载均衡算法:Optimized Work-Stealing Threads(OWST)。该算法的基本思想是:当存在多个GC线程需要“偷取”任务时,*终只有一个线程执行“偷取”操作,其它线程进入等待状态。执行“偷取”操作的线程检查各个GC线程的任务队列,根据任务个数唤醒线程,并执行任务。算法有效的降低了各个GC线程之间对于锁的竞争,提高了整个负载均衡的效率。

OpenJDK社区首先在Shenandoah GC上实现了OWST算法,在JDK12版本中合入主干分支并成为默认的Parallel Terminator。为了更好地支持LTS版本,KonaJDK团队将OWST算法相关代码移植到JDK8和JDK11,并完成相关代码适配和测试工作,经过业务端验证,为JDK8和JDK11添加了商用的OWST算法支持,有效降低了GC并行任务的执行时间,降低了GC的停顿时间。

通过对SPECjbb2015的性能进行测试,使用ParallelGC时OWST在对于max-jOPS基本没有影响的前提下,能够提升大约8%的critical-jOPS评分。另外对于腾讯内部大数据相关的Map/Reduce以及Spark SQL任务进行测试,执行性能也有10+%的提升。

业务应用

KonaJDK

目前在Arm架构,ZGC已经在腾讯的大规模生产中得到了实践应用。

ZGC将停顿时间控制在10ms以下的特性,使得它特别适合停顿时间敏感的业务。腾讯的WAF团队使用Java语言来快速实现产品功能迭代及上线。该团队有一个旁路安全服务,是一个基于Netty框架的Http服务。它对于端到端请求的时延要求特别严格,需要达到99.99% 请求时延小于 80ms 的 SLA 目标。传统的GC算法,难以达到如此高的标准,较长的停顿时间对于该服务有一定的负面影响,需要寻找一种更低停顿时间的GC算法。WAF团队之前采用了G1 GC算法,花费了大量的时间和精力对G1 GC进行选项调优以及代码层面的修改,但由于G1GC本身的不足,仍然存在请求抖动延迟,无法达到既定的 SLA 目标。后续在KonaJDK团队的配合下,通过切换ZGC算法,实现了该业务的P9999 请求延迟稳定小于 80ms,为用户提供了更快速、稳定的服务。

后续计划

KonaJDK

目前KonaJDK团队在Arm架构,主要在JDK8和JDK11版本进行优化和支撑,后续也会支撑JDK17等版本。KonaJDK团队会持续对JDK基础类库、运行时、内存管理、执行引擎等等各个模块进行分析和测试,不断扩展JDK的功能,提升性能。

KonaJDK团队会始终将Arm架构作为重点支撑平台,不断加大投入,推动并完善JDK对于Arm架构的支持,满足对于Arm架构不断增长的需求。