作者:Tina Tsou, 合著者 Shiyou Huang
Arm架构简介
近年来,腾讯内部对Arm架构的需求显著增加。随着腾讯的各种产品线引入了Arm服务器,对Arm架构优化软件的需求也随之增加。
为了满足对Arm技术日益增长的需求,腾讯和Arm一直在工具、软件和其他技术上进行合作,以使软件开发人员更容易在腾讯平台上构建基于Arm的产品和服务。
腾讯的KonaJDK是这项工作的核心。KONA JDK为腾讯环境创建java应用程序提供了高性能、高稳定性的基础。本博客描述了Arm作为KonaJDK一流平台的地位,以及Arm开发人员可以在JDK17中利用的功能。
ZGC
垃圾收集(GC)消除了程序手动释放内存的需要,降低了内存管理相关错误的可能性。对于GC算法来说,准确高效地清理内存是一个复杂的过程。内存数据集大小的增长(从10GB到100GB甚至1TB)进一步加剧了这一问题。GC算法是不断迭代的,只有选择最合适的GC算法才能帮助腾讯的业务实现目标。
诸如cmS和G1之类的GC算法往往会随着堆的大小而增加暂停时间。当超级堆触发完全GC时,它们甚至可能产生分钟级别的暂停。这已经成为它在延迟敏感应用程序中广泛使用的主要障碍,需要更合适的GC算法来满足这些业务的需要。
JDK中添加了ZGC以解决GC暂停引起的延迟问题,目标是将暂停时间保持在10ms以下。这将使吞吐量相对于G1减少不超过15%,同时支持较大和超大的堆大小。JDK11中引入了ZGC的实验版本,经过不断改进,ZGC成为JDK15的官方元素。
图1:ZGC性能(根据Liden的ZGC设计)
KonaJDK团队做了大量工作来完成JDK11中ZGC的Arm体系结构支持:
- 从JDK11到JDK13,ZGC代码和热点代码已经过多次重构。这需要在代码移植、移植相关重构代码或使代码适应JDK11的过程中分析代码重构的功能和影响。
- KonaJDK适应团队优化了ZGC,增强了功能,并修复了bug。
- Arm是一种RISC架构,具有弱内存模型。因此,在调整汇编代码(特别是ZGC使用的屏障)时,需要仔细考虑指令选择,以最小化开销并提高效率,同时确保正确性。
- 该团队在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
当应用程序需要并发执行多个任务时,它会创建多个线程,每个线程负责并发执行一个任务。随着工作负载的增长,为每个任务创建一个线程可能会消耗大量内存。此外,线程切换需要内核完成。当存在多个线程时,频繁的切换开销会对性能产生不利影响。开发“协程(Co-routines)”是为了解决这种情况。
协程是考虑部署和执行效率的轻量级线程。共同例程切换是在用户状态下进行的,这比在内核中进行的线程切换要便宜得多。协程也比线程需要更少的内存。为了在高并发情况下获得更好的性能,协程的使用比线程更广泛。
KonaFiber是腾讯开发的一个协程,它提供了更好的交换性能,同时与OpenJDK社区Loom API兼容。KonaFiber在JDK8和JDK11中实现。KonaFiber支持Arm体系结构,能够满足需要协程的基于Arm的应用程序的需求。
图2:Kona JDK与Loom
为了满足工作负载的需要并提供更好的协同切换性能,KonaFiber使用基于JKU的StackFul方案为每个协程创建一个独立的堆栈。切换协程时,除了检查协同例程的pin状态及其上下文外,只需修改帧指针和堆栈指针即可完成切换。KonaFiber的StackFul解决方案使用更多内存,更适合于对内存消耗不太敏感但对性能更敏感的工作负载。
协程切换性能数据如图3所示。左图显示了每秒共例行程序开关数量的比较;右图比较了内存消耗。
图3:协程切换方法的性能比较
图4:每个协程方法的内存使用情况
KonaFiber的实现侧重于代码重构,并以多种方式不断优化:
- 协程不断优化以减少资源消耗。
- 对GC进行了优化,以减少协程给GC带来的开销
- 它经过广泛测试,可提高鲁棒性和稳定性
KonaFiber提供了比Loom(OpenJDK社区的共同例程)更高、更稳定的调度性能。图4比较了不同协程中KonaFiber和Loom之间每秒的调度数。
图5:Loom协程调度性能
图6:KonaFiber 协程调度性能
KonaFiber现在在KonaJDK8中是开源的,并且将在KonaJDK11中是开源的。KonaJDK继续与Loom社区跟进,并不断改进KonaFiber的实施。
最优工作线程(OWST)优化
在GC操作期间,有几个GC线程并行处理各种任务。但是不同任务的处理时间不同,这使得GC线程之间的负载分配不平衡。JDK通过查看其他GC线程的任务队列来平衡GC线程之间的负载并减少GC的暂停时间。如果有一个线程可以执行的任务,它将“窃取”该任务并执行它。该过程一直持续到GC结束。
该方案实现了自动负载均衡。但是在执行过程中,多个GC线程“窃取”同一任务的可能性会导致它们之间的竞争,从而影响性能。
为了优化这一过程,谷歌在2016年发布了一种新的负载平衡算法,称为最优工作线程(OWST)。使用OWST,当有多个GC线程想要“窃取”一个任务时,一个线程最终执行“隐藏”操作,而其他线程则进入等待状态。执行“隐藏”操作的线程检查各个GC线程的任务队列,根据任务数唤醒线程,并执行任务。该算法有效地减少了不同GC线程之间对锁的竞争,提高了负载平衡的效率。
OpenJDK社区首先在Shenandoah GC上实现了OWST算法,将主干分支合并到JDK12版本中,并使其成为默认的并行终止符。为了更好地支持LTS版本,KonaJDK团队将OWST移植到JDK8和JDK11,并完成了相关的代码调整和测试。经过验证,在JDK8和JDK11中增加了商用OWST算法支持,有效减少了GC并行任务的执行时间,减少了GC的暂停时间。
通过测试SPECjbb2015的性能,使用ParallelGC的OWST能够将关键jOPS分数提高约8%,而对最大jOPS的影响很小。此外,腾讯内部与大数据相关的Map/Reduce和Spark SQL任务也进行了测试,性能提升了10%。
后续计划
KonaJDK通过JDK8和JDK11在Arm体系结构上得到优化和支持,对JDK17的支持得到了增强。KonaJDK团队不断分析和测试模块,如JDK的底层类库、运行时、内存管理、执行引擎等,不断扩展JDK的功能并提高性能。
KonaJDK团队致力于Arm体系结构,并正在投资改进该技术,以满足对Arm体系结构不断增长的需求。
注:本博客是根据腾讯此前发布的一篇文章翻译而成。原文请点击这里。
https://mp.weixin.qq.com/s/8xRLM0DDwjcyjp7nEanhQQ