丝般顺滑!全新垃圾回收器 ZGC 初体验

x_single
发布于 2022-5-7 13:46
浏览
1收藏

Java 语言的垃圾回收机制(即 GC )是提高 Java 代码开发效率的利器,然而 GC 引发的暂停往往严重影响 Java 业务的响应时间。Java11 引入了毫秒级暂停的 ZGC(The Z Garbage Collector),给 Java 业务响应时间的优化带来福音,成为众多业务从 Java8 切换到 Java11 的理由之一。
阿里巴巴维护的 Alibaba Dragonwell 11是一款免费且生产就绪的 OpenJDK 11 发行版,提供了性能增强和安全修复的长期支持。早在第一版发布时,Dragonwell 11 应阿里内部业务和云上客户的需求,默认提供了 Java 11 的实验性(experimental)ZGC 特性。随着 ZGC 进入生产实践而逐步落地,阿里业务和云上客户享受到响应时间的优化的过程中也遭遇到了一些实际问题。正因为如此,阿里巴巴发布了 Dragonwell11.0.11.7 ,其中的 ZGC 特性由 OpenJDK 11 中的实验特性改造为生产就绪(production-ready)特性,同时保证 Dragonwell 11 长期支持的质量稳定性。

丝般顺滑!全新垃圾回收器 ZGC 初体验-鸿蒙开发者社区本篇开启 Alibaba Dragonwell ZGC 系列的技术分享,介绍 Alibaba Dragonwell11 上的 ZGC:本系列共计 3 篇文章,本篇将介绍 GC 的基本概念和 ZGC 的规模化实践,第二篇将介绍 ZGC 的原理和调优,第三篇将介绍 Dragonwell 对于 ZGC 的生产就绪改造。
Java GC简介
垃圾回收 GC 是 Java 语言的自动内存管理机制,能够自动销毁垃圾对象(不再能够被引用的对象),从而释放内存以供后续使用。在 GC 的帮助下,Java 开发者只需专注在自身业务逻辑,调用 new 语句创建对象,而无需编写销毁对象的语句,从而提高了代码开发效率和代码质量。

GC性能指标
天下没有免费的午餐,GC 带来便捷的同时也带来显著的副作用。对于 Java 业务而言,用户通常关心两个指标:吞吐率 QPS(query per second,每秒可处理的请求数量)和响应时间 RT(response time)。GC 通常会对 QPS 和 RT 产生负面的影响。
GC 暂停会延长 RT,特别是长尾请求的RT P99/P999(从快到慢排名 99%/99.9% 的请求的 RT)。GC 为了保证回收算法的正确性,往往需要暂停所有正在执行业务逻辑的 Java 线程,来避免 GC 线程与分配对象的 Java 线程的竞争。Java 线程在暂停期间无法响应任何请求,导致业务的 RT 变长。
GC 会降低吞吐率(QPS上限)。GC 线程占用额外的 CPU 资源,从而影响 Java 线程使用 CPU 的份额。人们往往认为 GC 必然与暂停相生相伴。然而此观点不完全正确。现代 Java 的 GC 可以启动并发 GC 线程与 Java 线程并发执行。

丝般顺滑的ZGC体验
Java 语言提供了若干种 GC 来适应不同的需求。这些 GC 在吞吐率和响应时间两方面的表现有不同的特点:

  • Parallel GC:吞吐率高,GC 暂停时间长;
  • G1 GC,CMS GC:吞吐率和 GC 暂停时间都比较好,G1 GC 是 Java11 的默认 GC(目标 GC 暂停时间为 200ms),而 CMS GC 在 Java11 中已经不推荐使用;
  • ZGC,Shenandaoh GC:GC 暂停时间短,吞吐率一般。
    本文的主角 ZGC 是 OpenJDK11 引入的新一代 GC,暂停时间能够保持在 10ms 以内,且最高能支持 TB 级别的大堆。

Java11 之前的 GC 通常需要 100ms 以上的暂停,会给 RT P99 等指标带来负面影响,让运行中的 Java 业务仿佛在坑坑洼洼的道路上磕磕绊绊地前行。毫秒级别暂停的 ZGC 能够让 RT P99 进一步下降,运行中的 Java 业务得了到丝般顺滑的体验。ZGC 大多数情况下只需要调节堆的大小和并发 GC 线程数量,调优较为容易,大大节约心智成本。

ZGC 实践
本节在 ZGC 投入实战之前首先介绍了 ZGC 的适用场景,目的是让业务上线之前能够选择正确的 GC。我们在充分评估业务特点的基础上,让相应的业务运行在 ZGC 上,取得了 RT 的提升。然而 OpenJDK 11 的 ZGC 尚处于实验性阶段,我们在实践中遇到了一些问题。我们将这些问题作为风险项记录下来,并在 Dragonwell 11 中尝试解决这些问题。

ZGC 适用场景
ZGC 取得了卓越的毫秒级暂停性能,然而副作用是 ZGC 可能降低业务吞吐率( ZGC 项目主页声称损失至多 15% 的吞吐率)。其原因主要包括三个方面:

1.Java11 的 ZGC 是单代 GC,每一轮 ZGC 均需要处理长寿对象(多次 GC 之后依然存活的对象),而 Java11 以前的 GC 均是分代 GC,不需要每次 GC 都处理长寿对象;

2. ZGC 需要开启并发 GC 线程,减少 Java 线程使用 CPU 的份额;

3.ZGC 的读屏障(后文将介绍)使得每个从堆中加载对象的操作都有额外的开销。此外,由于 ZGC 不支持压缩指针技术,ZGC 在 32GB 以内小堆上无法享受压缩指针带来的性能提升。

综合以上的 ZGC 特点的描述,笔者总结了 ZGC 适用场景,供有意切换到 ZGC 的朋友们参考:

1.对长尾请求 RT P99/P999 等指标有高要求的 Java 业务:这些业务通常要求实时响应,对最慢的 1% 或 0.1% 的请求非常敏感;

2.机器的内存与 CPU 资源丰富:丰富的计算资源可以开启更大的堆和更多的并发 GC 线程;

3.可以容忍吞吐率降低:业务经过权衡后,认为 RT P99/P999 的指标比 QPS 指标更重要;

4.长寿对象相对较少:Java11 的 ZGC 尚未分代,无法高效地处理此类对象。
此外,Java 业务如果仍然运行在 Java 8 上,那么还需要考虑到切换到 Java 11 的代价。

ZGC规模化实践
阿里内部许多对长尾请求 RT 有严格要求的 Java 业务,为了突破 GC 暂停对于 RT 的瓶颈,这些 Java 业务逐渐升级到 Java11,并且选择 ZGC 作为 GC。下面展示了阿里内部使用 ZGC 获得 RT 提升的案例。本节提到的Concurrent Mark/Relocate将在本系列第二篇文章中阐述。

1.高性能数据库:Lindorm 是阿里内部高性能 NoSQL HBase 分支。Lindorm 在 ZGC 上稳定运行近两年,期间顺利通过双十一大考。Lindorm 运行期间的 ZGC 暂停时间稳定在 5ms 左右,最大暂停时间不超过 8ms 。ZGC 大大改善了线上运行集群的 RT 与毛刺指标,平均 RT 优化 15%~20%, P999 RT 减少到原先的一半以内。2019 年双十一蚂蚁集群在 ZGC 的加持下, Lindorm RT P999 时间从 12ms 降低到了 5ms 。下图展示了 Lindorm 在 ZGC 上的 GC 暂停表现(单位为微秒)。丝般顺滑!全新垃圾回收器 ZGC 初体验-鸿蒙开发者社区2.消息队列应用:RocketMQ 为了提升弹性伸缩能力,不再依赖本地文件系统,新增支持分布式文件系统作为存储。RocketMQ 原先采用 G1GC,但 GC 暂停时间达到 200ms 以上,即使经过大量的调优依然无法将暂停时间降下来。经过研究发现 GC 暂停的主要因素是访问分布式文件系统时必须基于 JNA 调用 C 语言库,而 JNA 依赖 finalizer 回收 native 内存中的对象,这些对象至少经过 2 个 G1 的 GC 周期才会被回收,大量对象转移(每次 GC 约有 50 万对象转移)导致长时间的暂停。由于 ZGC 回收 native 对象是在并发阶段完成的,从而避免长时间暂停。RocketMQ 仅仅设置了 ZGC 的堆大小和并发线程数量,就使得目前线上 GC 暂停时间都小于 2ms ,大大减少了系统访问的毛刺。下图展示了 RocketMQ 使用 ZGC 之后的 RT 指标。
丝般顺滑!全新垃圾回收器 ZGC 初体验-鸿蒙开发者社区3.风控调用:线上部分应用对风控调用耗时敏感。这些应用设置的服务调用超时时间很短(< 50ms ),而目前风控系统一次 Young GC 的耗时在 60ms 左右。只要遇到 GC ,业务调用就会超时。以红包业务为例,超时后红包要么发放不出去,要么发出去但有可能被羊毛党薅走,对业务都是有影响。为了提升可用性,需要 ZGC 的支持。线上实际运行 ZGC 的暂停时间保持在 10ms 以内,能够满足这些 RT 敏感的应用等需求。由于风控系统缓存对象较多,导致 Concurrent Mark 阶段时间较长,影响吞吐率提升。为了支持 ZGC 更流畅地运行,风控系统减少了缓存的长寿对象,从而提高 QPS 上限。

OpenJDK11 ZGC的风险
在以上实践过程的早期,我们采用了 OpenJDK11 的 ZGC ,而该特性仅仅处于实验性阶段。从 OpenJDK 11 发布实验性 ZGC 以来, ZGC 的稳定性得以增强,功能日臻完善。到 OpenJDK 15 发布的时候, ZGC 已经成为生产就绪的特性。OpenJDK 11 是长期支持的版本,而目前发布的 OpenJDK12-16 都不是长期支持的,因此在生产实践中直接部署 OpenJDK 15/16 来尝鲜生产就绪的 ZGC 存在困难。

以上实践表明,对于 10GB 到数百 GB 的 Java 堆,ZGC 的确能将暂停保持在 10ms 以内。然而这些业务均报告“ QPS 上不去”,也就是说对于吞吐型场景, ZGC 表现不理想。在吞吐型场景中, ZGC 的回收速度跟不上分配速度,出现了分配停顿(Allocation Stall),即暂停当前正在创建对象的线程,等待 ZGC 释放空闲空间。另外, Lindorm 还报告小堆上的 ZGC 整体效果甚至没有 G1GC 来得好。此外,以上实践也遇到了一些 OpenJDK11 实验性 ZGC 的问题:

1.无征兆的崩溃现象:Lindorm 发现两个变量指向同一个对象时,代码却检测这两个变量不相等;RocketMQ 业务也发现程序运行期间无征兆地崩溃了。仔细排查,可发现读屏障与加载操作相分离,中间可能进入 GC 暂停。这个情况已经在 JDK14 中修复。
2.最坏情况会发生 OOM :风控业务注意到 ZGC 可能抛出 OOM ,此现象发生在 Concurrent Relocate 阶段。ZGC 通常会预留一段空间供 Concurrent Relocate,然而 JDK11 的 ZGC 代码无法保障预留的空间是足够的,如果对象 Relocate 速度很快,就有可能抛出 OOM 。此问题在 OpenJDK16 中解决。

3.Page Cache Flush 影响 RT :ZGC 把堆分为小/中/大三种规格的 ZPage(不同大小的对象分配到不同类型的 ZPage 中),如果各种大小对象分配速度不稳定(比如中等大小的对象突然变多,那么就需要把小/大的 ZPage 转换成中等 ZPage ,此过程耗时长)。Lindorm 注意到此现象会严重影响 RT。OpenJDK15 的 ZGC 对这个现象有所缓解。

以上都是 ZGC 投入生产实践中亟待解决的问题。Alibaba Dragonwell 11 是 OpenJDK11 的下游,继承了 ZGC 在内的全部特性。在后面的篇章中,我们将分享 Dragonwell11 上进行的 ZGC 生产就绪改造的工作。在此之前,读者可以先尝试在 Dragonwell11 上开启 ZGC 。
Dragonwell11开启ZGC
Java 开发者需要将 JDK 更新到 Alibaba Dragonwell 11.0.11.7 及以上的版本。开启 ZGC 只需在 Java 启动时打开 -XX:+UseZGC 即可。读者不妨浏览 Dragonwell ZGC 相关调优选项。
Dragonwell11 下载:

https://github.com/alibaba/dragonwell11/releases/tag/dragonwell-11.0.11.7_jdk-11.0.11-ga 
Dragonwell ZGC 调优选项:

https://github.com/alibaba/dragonwell11/wiki/Alibaba-Dragonwell-11-Release-Notes#zgc-options
在后面的篇章中,我们将介绍 ZGC 的原理和调优技巧,并且能看到我们的 Alibaba Dragonwell11 通过对 ZGC 进行生产就绪改造,从而解决生产实践中的一些问题。
SIG 地址:
官网:https://openanolis.cn/sig/java/doc/216166872482840581
 

分类
标签
已于2022-5-7 13:46:51修改
收藏 1
回复
举报
回复
    相关推荐