给 Arm 生态添把火,腾讯 Kona JDK Arm 架构优化实践
前言
Arm 架构以其兼具性能与功耗的特点,在智能终端以及嵌入式领域得到了广泛的使用,不断扩大其影响力。而在 PC 端以及数据中心,之前往往是 x86 架构在其中发挥着主要的作用。最近,随着人工智能、云计算等技术的兴起,5G 网络的不断成熟,万物互联的时代是的应用的需求越来越多样化,使得对于芯片架构的需求也越来越多样化。
Arm 架构在提供可靠性能的基础上,低功耗、低开销的特点使得它被越来越广泛的应用到数据中心和云计算中,成为其中必不可缺少的重要组成部分。亚马逊投入大量精力自研 Arm 服务器,并应用到 AWS 服务中,最多实现了成本 45%的降低;阿里巴巴也在云服务中大量采用 Arm 服务器,并积极参与 Linaro,Adoptium 等组织,不断推动 Arm 架构的发展。
最近几年,腾讯对于 Arm 架构的需求也不断增加,各个产品线也不断引入 Arm 服务器,对于 Arm 架构软件的需求也在不断增长。KonaJDK 团队在腾讯公司内部提供高性能、高稳定性的商用 JDK 版本,坚定地将 Arm 架构作为 KonaJDK 重点支持的架构之一,不断扩展 JDK 在 Arm 架构的功能,并不断提高 Arm 架构中 JDK 的性能。
随着 Arm 架构在终端和云计算场景的广泛应用,JDK 需要做好对于 Arm 架构的支持工作,才能更好地得到发展。目前在 JDK 社区,Arm 架构属于第一梯队支持架构。而对于 Arm 架构而言,Java 语言“一次编译,到处运行” 的特性适合业务应用无缝推广到 Arm 平台,而 JDK 则是 Java 应用运行的必要条件。JDK 对于 Arm 架构的支持,也是 Arm 生态推广的有力支撑。在这个过程中,KonaJDK 团队希望和 Arm 紧密合作,共同发展。
腾讯和 Arm 在 JDK 方面的合作交流
目前腾讯和 Arm 在 JDK 方面已经有了深入的交流和合作。双方针对 JDK 在 Arm 架构常见的性能问题,对于 Arm 架构新特性的支持情况等方面进行了广泛和深入的讨论,通过性能测试、数据交流、技术研讨等形式不断推动 JDK 在 Arm 架构的发展。
KonaJDK 团队 Arm 平台优化技术介绍
目前在 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 停顿要求高的业务提供了一种更好的选择。
图 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 架构应用对于协程的需求。
图 2 KonaJDK 和 Loom 对比
为了满足业务的需求,提供更好的协程切换性能,KonaFiber 采用了基于 JKU 的 StackFul 有栈方案,为每一个协程创建独立的堆栈。当进行协程切换的时候,JDK 在对于协程 Pin 状态检测以及上下文保存之外,只需要修改 Frame Pointer 和 Stack Pointer 的值就可以完成协程的切换工作,逻辑简单且性能开销很小。不过相对于社区的方案,KonaFiber 的 StackFul 方案对于内存的使用要多一些,更适用于对于内存消耗不太敏感,但是对于性能更敏感的业务场景。性能数据如图 2 所示,左图表示在不同协程数量情况下,每秒内协程切换次数对比;右图对内存消耗进行了对比。
图 3 KonaFiber 性能对比
KonaFiber 的实现注重优化以及代码重构,通过多种方式不断进行优化:
协程轻量化,不断优化降低协程的资源消耗
按需创建,根据业务的需要创建协程,降低内存使用
GC 优化,优化实现,降低协程对 GC 引入的开销
稳定性修复,通过广泛的测试以及业务适配,提高健壮性
相对于 OpenJDK 社区的协程方案 Loom,KonaFiber 提供了更高更稳定的调度性能。图 3 对比了 KonaFiber 和 Loom 在不同协程数量情况下的每秒调度次数。
图 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+%的提升。
业务应用
目前在 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 团队在 Arm 架构,主要在 JDK8 和 JDK11 版本进行优化和支撑,后续也会支撑 JDK17 等版本。KonaJDK 团队会持续对 JDK 基础类库、运行时、内存管理、执行引擎等等各个模块进行分析和测试,不断扩展 JDK 的功能,提升性能。
KonaJDK 团队会始终将 Arm 架构作为重点支撑平台,不断加大投入,推动并完善 JDK 对于 Arm 架构的支持,满足对于 Arm 架构不断增长的需求。
欢迎关注「腾源会」微信公众号,这里是全球开源内容信息的聚集地,包括但不限于全球开源资讯,开源项目及技术文章,开源活动报道及开源人物采访。
评论