58 数据平台部 58 技术
导读: 58 同城大数据团队,鉴于目前线上大数据集群各个组件 GC 方面的问题,考虑逐步在适合的大数据组件上应用高版本 JDK 上的 ZGC,目前已经在 HBase 集群上成功落地,在某些场景上有比较明显的效果。58 同城 HBase 集群应用的 JDK 为腾讯公司开源的 Tencent Kona JDK11。未来会在逐步应用 ZGC 的过程中,享受 ZGC 带来的好处。
背景
为了满足不同的业务需求,Java 的 GC 算法也在不停迭代,对于特定的应用,选择其最适合的 GC 算法,才能更高效的帮助业务实现其业务目标。很多低延迟高可用 Java 服务的系统可用性经常受 GC 停顿的困扰。GC 停顿指垃圾回收期间 STW(Stop The World),当 STW 时,所有应用线程停止活动,等待 GC 停顿结束。比如 HBase 方面,如果 regionserver 进程的 GC 停顿时间过长,会导致大量请求延迟,对客户端影响较大。对于这类延迟敏感的应用来说,GC 停顿已经成为阻碍 Java 广泛应用的一大顽疾,需要更适合的 GC 算法以满足这些业务的需求。
目前我们线上 HBase 集群应用的都是 CMS 作为 Java 服务进程的垃圾回收器,在不同的应用场景下经过了多次调优之后,基本满足于一般业务场景下对延迟等方面性能的要求。但是随着业务的发展,会有某些高敏感的场景,请求延迟要求较高一些,此时集群性能就会捉襟见肘。通过线上集群监控系统查看某一台 RS 节点的 GET 请求延迟 P99,发现毛刺比较多,分析对应时间内(大约 24 个小时)的 GC 信息,发现 GC 平均暂停时间为 57.4ms(这些 GC Pause 中 99% 以上都是 Allocation Failure 原因产生的 YongGC 导致的,这里结合具体的场景,为了避免业务高并发请求的时间节点 GC 频率过高,配置了新生代大小为 8G,大约占整个堆 26.7%),GC 频率整体上看比较正常(平均 63.35s 会发生一次 GC Pause)。但是发现某些时间点 gc 频率会高一些,比如会达到平均 4.5s 发生一次 GC Pause,结合平均暂停时间看对延迟敏感的业务影响较大。之前也考虑过使用 G1,但是经过各种原因没有进行应用,比如 G1 本身存在一些问题。为了降低 GC 停顿对系统可用性的影响,我们考虑调研、测试和应用 ZGC。
常见 GC 回收器介绍
在介绍 ZGC 之前,先简单说下 CMS 和 G1 等。CMS 是基于标记-清除算法实现的(新生代为复制算法),G1 从整体来看是基于“标记-整理”算法实现的,从局部(两个 Region)上来看是基于“复制”算法实现的。像复制、标记-清除、标记-整理算法,也可以称为 Tracing GC。所有的 Tracing GC 均需要以下三个步骤:
- 找出所有的 GC Roots 集合,这是 Tracing GC 算法的起点,GC Roots 主要为运行时的关键数据结构中存放的指向堆对象的指针,如线程栈上的堆对象指针等。
- 标记过程:从 GC Roots 开始遍历整个对象图,找出所有存活的对象。而剩余未被标记的对象则为死对象。
- 清理过程:将死掉的对象清理掉,释放其占用的内存。当然清理时可以直接释放对象内存,也可以将所有的活对象移动到一块连续的区域里,并将原来的内存空间释放。
上面三个步骤均需要一致的信息,为了保证一致性,需要采取同步措施,而最简单的同步措施就是暂停所有的 Java 线程,即 Stop-The-World(STW),在 STW 期间,GC 线程就可以安全的访问各种运行时数据、对象图、更新对象指针等。
不同的 GC 算法实现,其 STW 阶段需要完成的任务大相径庭,造成不同 GC 算法 STW 时长的不同。每种 GC 都有一定的实现目标和应用场景,比如 CMS 和 G1 致力于以较小的吞吐率损失换取较小的停顿和较高的响应;ZGC 和 ShenandoahGC 则关注极致停顿,尽一切可能减少 STW 的工作量,从而实现 ms 级别的停顿时间。我们看下不同的 GC 方式的简单对比介绍、特性、缺点,如下列表所示:
GC 方式 | 介绍 OR 特性 | 缺点 |
---|---|---|
CMS | * 并发,低停顿* 标记-清除算法(新生代使用 ParNew)* 分代收集 | * CMS 收集器对 CPU 资源十分敏感;* CMS 收集器无法处理浮动垃圾;* CMS 收集器是基于“标记清除-算法”,收集完成后会产生大量空间碎片 |
G1 | * 可预测停顿* 标记-复制* 并行与并发* 分代收集* 空间整合 | * 停顿时间过长,通常 G1 的停顿时间要达到几十到几百毫秒,这个数字其实已经非常小了,但是我们知道垃圾回收发生导致应用程序在这几十或者几百毫秒中不能提供服务,在某些场景中,特别是对用户体验有较高要求的情况下不能满足实际需求。* 内存利用率不高,通常引用关系(Remembered Set)的处理需要额外消耗内存,一般占整个内存的 1%~20% 左右。* 支持的内存空间有限,不适用于超大内存的系统,特别是在内存容量高于 100GB 的系统中,会因内存过大而导致停顿时间增长。 |
Shenandoah GC | * low-pause-time* 标记-复制* 支持并发的整理算法* 单代收集* 基于 brooks pointers* 只有 OpenJDK 才会包含的收集器 | * Shenandoah GC 的并发性是以降低应用程序的吞吐量为代价* 高运行负担使得吞吐量下降* 使用大量的读写屏障,尤其是读屏障,增大了系统的性能开销 |
ZGC | * low-pause-time* 标记-复制* 单代收集* 基于 colored pointers、Load Barrier* 大部分时间是并发进行的 | * ZGC 没有分代,高并发的对象分配速率低于 G1.* 当前 ZGC 不支持压缩指针,其内存占用相对于 G1 来说要稍大 |
遇到的问题
在 XX 公司负责 HDFS 集群维护工作时,线上 HDFS 集群 Namenode 进程(大内存)经过了应用 CMS、G1 和尝试应用 Shenandoah GC 的过程,下面简单说下应用 CMS、G1 和 Shenandoah GC 遇到过的一些问题(只是对遇到的问题大概地整理)。
1. 应用 CMS 期间,出现了 System.GC()导致的 Full GC 问题:早期应用 CMS GC 运行一段时间,期间不断调优 gc 配置,但是也发现了一些问题,比如会出现 System.gc()导致的 full gc 问题(具体原因当时没有完全确定),可以通过 XX:+DisableExplicitGC 参数直接禁用这类 GC(亦可以通过-XX:+ExplicitGCInvokesConcurrent 参数减缓这类 GC),后面在测试环境测试对比 CMS 和 G1 之后,考虑在线上环境直接应用 G1。
2. 应用 G1 期间,出现了长时间 GC 的问题: 应用 G1 在调优之后在很长的一段时间之内,运行还算稳定,但是随着常驻内存逐渐变大等因素出现了长时间 GC 的问题,没有达到 G1 的停顿可控的设计目标,问题原因是 Scan RSet 时间过高,长时间的 GC 几乎所有的时间消耗都在 Scan Rset 上,线上环境经过不断调高 G1RSetRegionEntries 解决了 Scan Rset 时间过长问题,但是这种解决方案会导致进程堆外内存过高,所以是以提高内存占用为代价来保证 GC 稳定。参考 Java 官网 G1 相关文档(https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector-tuning.htm#JSGCT-GUID-E26056D1-02A5-4367-94EF-72C66D314AF7)
3. 尝试应用 Shenandoah GC: 应用 G1 出现长时间暂停的问题后,考虑使用新的 GC 方式,在测试环境测试 Shenandoah GC 性能,并和 G1 进行对比后,发现 Shenandoah GC 的暂停时间基本上能达到官网上描述的低停顿的目标, 尝试在生产环境上应用,应用后发现每天流量高峰时,大量 rpc 请求延迟。在测试环境测试批量创建对象,发现应用 Shenandoah GC 比应用 G1 慢一些,具体细节原因没有继续跟进,直接在生产环境更换回 G1 方式。
ZGC 介绍与实现原理
ZGC(The Z Garbage Collector) 是 JDK 11 中推出的一款低延迟垃圾回收器,它的设计目标包括:
- 停顿时间不超过 10ms;
- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
- 支持 8MB~4TB 级别的堆(未来支持 16TB)。
由设计目标可知,ZGC 主要是为现在及未来大堆的管理问题服务,致力于以最小的性能损失换取最大的停顿优势。从 Oracle 发布的测试数据来看,SPECjbb2015 基准测试,使用 128G 堆,暂停时间 ZGC 远低于 Parallel GC 和 G1GC,如下图所示。
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进:ZGC 在标记、转移和重定位阶段几乎都是并发的,这是 ZGC 实现停顿时间小于 10ms 目标的最关键原因。所以,ZGC 几乎在所有地方并发执行的,停顿时间几乎就耗费在初始标记上,这部分的时间是非常少的。ZGC 的执行过程可以细分为 6 个阶段,如下图所示:
- 第一个阶段是初始标记,标记 gc roots 直接引用的对象。
- 第二个阶段是并发标记/重定位:与 G1 一样,并发标记是遍历对象图可达性分析的阶段,标记存活对象。同时上一轮 GC 的指针更新(Remap)放到当前阶段执行,从而减少对对象图的遍历。
- 第三个阶段是重新标记:重新标记那些在并发标记阶段发生变化的对象。
- 第四个阶段是并发转移准备:这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些 Region,将这些 Region 组成重分配集(Relocation Set)。ZGC 每次回收都会扫描所有的 Region,用范围更大的扫描成本换取省去 G1 中记忆集的维护成本。
- 第五个阶段是初始转移:迁移 Root 中的对象,在 Forwarding Tables 中记录新老地址的映射,不会停顿太长时间。
- 第六个阶段是并发转移:并发的搬移对象,在 Forwarding Tables 中记录新老地址的映射。
如上所示(其中红色标记的阶段是需要 STW 的阶段),ZGC 只有三个 STW 阶段:初始标记,重新标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有 GC Roots,其处理时间和 GC Roots 的数量成正比,一般情况耗时非常短;再标记阶段 STW 时间很短,最多 1ms,超过 1ms 则再次进入并发标记阶段。即,ZGC 几乎所有暂停都只依赖于 GC Roots 集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与 ZGC 对比,G1 的转移阶段完全 STW 的,且停顿时间随存活对象的大小增加而增加。
一些实现上的细节点说明:
- 与 G1 不同的是,ZGC 的标记是在指针上而不是在对象上进行的,标记阶段会更新着色指针中的 Marked0、Marked1 标志位。记录在指针的好处就是对象回收之后,这块内存就可以立即使用。存在对象上的时候就不能马上使用,因为它上面还存放着一些垃圾回收的信息,需要清理完成之后才能使用。
- 重映射(Remap),把所有已迁移活跃对象的引用重新指向新的正确地址。实现上,由于想要将所有引用都修正过来需要跟 Mark 阶段一样遍历整个对象图,所以把 Remap 放到下一次的 GC 的并发标记阶段中执行(上边已经做了简单说明)。
- 重分配(即阶段五和阶段六)是 ZGC 执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护了一个转发表(Forward Table),记录从旧对象到新对象的转换关系。ZGC 收集器能仅从引用上就明确得知一个对象是否处于重分配集中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据 Region 上的转发表记录将访问转到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC 将这种行为称为指针的“自愈”(Self-Healing)能力。
ZGC 特征总结如下(细节不做过多说明):
- 所有阶段几乎都是并发执行的
- 并发执行的保证机制,就是 Colored Pointer(着色指针) 和 Load Barrier(读屏障)
- 像 G1 一样划分 Region,但更加灵活
- 和 G1 一样会做 Compacting-压缩
- 没有 G1 占内存的 Remember Set,没有 Write Barrier 的开销
- 支持 Numa 架构
- 并行
- 单代
上面介绍了 ZGC 的回收流程以及特征,下面说下 ZGC 的关键技术。
ZGC 关键技术
与传统的标记对象算法相比,ZGC 通过 Colored Pointers(着色指针)在指针上做标记,在访问指针时加入 Load Barrier(读屏障),实现了并发转移。比如当对象正在被 GC 移动,但对象地址未及时更新,那么应用线程可能访问到旧地址而出错。ZGC 在应用线程访问对象时增加“读屏障”,通过着色指针判定对象是否被移动过。如果发现对象被移动了,读屏障就会先把指针更新到新地址再返回(如果还在迁移中,业务线程会协助 GC 线程迁移)。因此,只有单个对象读取时有概率被减速,而不存在为了保持应用与 GC 一致而粗暴整体的 Stop The World。
Colored Pointers(着色指针)
着色指针是一种将信息存储在指针中的技术。
在 64 位系统中,ZGC 利用了对象引用中 4bit 进行着色标记(低 42 位:对象的实际地址),结构如下:
Colored Pointers(着色指针)用以下几个 bits 表示:
- Marked0/marked1: 判断对象是否已标记,GC 周期交替时更换标记位,使上次 GC 周期标记的状态失效,所有引用都变为未标记
- Remapped: 判断指针是否已指向新的地址,不再通过转移表 Forwarding Tables 映射
- Finalizable: 判断对象是否只能被 Finalizer 访问,即是否实现了 finalize()方法
Colored Pointers(着色指针)001、100、010 在三种状态循环交替
- 001:Marked0 作为标记位,标记活跃对象
- 100:Relocation Set 中对象迁移完成后记为 100,表示迁移完成
- 010:下一个 GC 周期,切换 Marked1 作为标记位,把标记为 100(上个 GC 周期迁移的对象)、001(上个周期标记的活跃对象)更新为 010
注意事项:由于 ZGC 利用 64 位的对象指针,因此,ZGC 无法支持 32 位的操作系统,同样也无法支持压缩指针(CompressedOops)
Load Barrier(读屏障)
读屏障是 JVM 向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。
读屏障示例:
String n = person.name; // Loading an object reference from heap
// 从堆中加载对象引用,需要先加一个Load Barrier,判断当前指针是否Bad Color,即对象是否在GC时被移动了,如果移动了,修正指针
<load barrier needed here>
String p = n; // No barrier, not a load from heap
n.isEmpty(); // No barrier, not a load from heap
int age = person.age; // No barrier, not an object reference
ZGC 中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。
G1 只有写屏障没有读屏障,复制移动的过程需要 stop the world,而 ZGC 通过读屏障、remark 标记和重定向表来并发拷贝非 GC Roots 对象,减少了 STW,所以 ZGC 的停顿比 G1 的停顿少很多。
优缺点总结:
ZGC 主要优点如下(相比 G1):
- 更低延迟:GC 停顿时间更短,不超过 10ms
- 更大内存:堆内存支持范围更大(8MB-16TB)
ZGC 做为一个较新的垃圾回收器,有比较明显的优点,当然也存在一些缺点或者问题:
- 分代对提高吞吐量意义重大,分代可以提高对象内存分配和垃圾回收的效率,ZGC 没有分代,高并发的对象分配速率低于 G1.
- 当前 ZGC 不支持压缩指针,其内存占用相对于 G1 来说要稍大。
ZGC 应用场景:
前边已经提到过了,ZGC 适用于大内存低延迟服务的内存管理和回收。ZGC 最大的优势就是无论多大的堆内存场景下都能够保证停顿时间在 10ms 以下,但是这种超低停顿的实现是以性能损失和内存消耗为代价的。ZGC 没有分代,一些场景会影响吞吐量,当前 ZGC 不支持压缩指针,其内存开销会比 G1 大一些。了解每种 gc 方式的优劣,结合场景的特点选择适合的 GC 方式,才能让应用达到比较好的服务性能。推荐以下两种应用可以使用 ZGC 来提升业务体验。
- 超大堆的应用,超大堆(百 G 以上)下,使用 CMS 或者 G1 如果发生 Full GC,停顿时间会很长,可能会造成业务的中断,这种场景使用 ZGC 可以有效的避免长时间的暂停,比如大数据方面的 hdfs 大集群的 namnode 进程。
- 延迟要求高的应用,一些请求延迟要求较高的应用,比如大数据方面,业务读写延迟 P99 要求较高的 HBase 组件,可以考虑采用 ZGC 来降低停顿时间。
ZGC 调优介绍
和其他垃圾收集器一样,ZGC 也需要根据服务的具体特点进行调优,调优目的是提高服务的质量(可用性和性能方面), 在内存使用和 GC 频率之间找到平衡,不能过度调优,系统 CPU 使用率不宜过高,避免产生风险问题,影响服务。
调优的量化指标:
请求延迟
系统吞吐率
下面分别从降低请求延迟和提高系统吞吐量两个方面进行调优举例说明:
1. 请求延迟方面
出现请求延迟增加,一般是服务进程运行一段时间之后,或者是请求流量增加幅度较大。
请求延迟方面调优主要有以下几种情况:
A. 单次 GC STW 时间较长,大大超过了 ZGC 的设计目标的 10ms
这种情况分析 STW 的三个阶段的时间消耗相关信息,根据具体情况进行调优。
比如:发现 Pause Mark Start 时间较长,可以查看 GC 统计信息中 GC ROOT 相关的信息,Pause Roots ClassLoaderDataGraph 耗时、Pause Roots CodeCache 耗时等等,根据具体的情况进行优化。
B. 发生了一些内存分配阻塞(Allocation Stall)
内存分配阻塞:当内存不足时线程会阻塞等待 GC 完成,关键字是"Allocation Stall"。一般是自适应算法计算的 GC 触发间隔较长,导致 GC 触发不及时,引起了内存分配阻塞,导致停顿,这种情况需要确认 GC 的执行频率:
a. 如果 GC 执行频率很高,说明应用进程的内存资源确实有一些不足,如果资源允许情况下可以调整配置增加堆的大小。
b. 如果 GC 执行频率较低,可以具体分为以下几种情况:
b1. GC 触发机制和出发条件阈值导致 GC 触发较晚,调整触发机制,调整触发条件阈值,尽早地执行 GC。
一般自适应算法计算的 GC 触发间隔较长,导致 GC 触发不及时,引起了内存分配阻塞,导致停顿。调整的方法主要有:
- 可以选择开启”基于固定时间间隔“的 GC 触发机制:-XX:ZCollectionInterval。比如调整为 5 秒,甚至更短。
- 增大修正系数-XX:ZAllocationSpikeTolerance,更早触发 GC。ZGC 采用正态分布模型预测内存分配速率,模型修正系数 ZAllocationSpikeTolerance 默认值为 2,值越大,越早的触发 GC,Zeus 中所有集群设置的是 5。
b2. GC 触发及时,但是 GC 并发阶段执行时间过长,比如 Concurrent Mark 时间过长,导致 GC 执行频率低。
调整方法:增大-XX:ConcGCThreads,加快并发标记和回收速度。ConcGCThreads 默认值是核数的 1/8,8 核机器,默认值是 1。该参数影响系统吞吐,一般情况下如果 GC 间隔时间大于 GC 周期,不建议调整该参数,具体要结合业务场景的特点去调整此参数,比如一些流量高峰时产生了内存分配阻塞问题,GC 间隔时间较短,但是其他时间段 GC 间隔时间很长的情况下,为了提高 GC 效率,建议也要调高此参数,提高 GC 效率,避免出现内存分配阻塞的问题。
2. 吞吐率方面
一般 ZGC 频率过高或者参数设置不合理会对应用进程吞吐率方面影响较大。
a. 控制 GC 执行频率, 如果 GC 执行频率过高,会导致多占用 CPU 资源;
调整方法:结合业务进程运行情况,可以适当调整触发机制,调整触发阈值,可以使 GC 触发晚一点, 调整的力度需要结合请求延迟方面综合考虑。
b. 并发阶段占用 CPU 资源相关的配置-XX:ConcGCThreads,结合业务情况合理配置。
调整方法:合理适当的配置-XX:ConcGCThreads。
综上,ZGC 调优需要结合多个方面进行综合考虑,明确调优目的,统计分析 GC 信息,按照一些调优策略逐步地进行优化,线上环境不宜单次调整过大,避免带来一些其他影响,调整前需要综合多个方面评估影响。
ZGC 应用
ZGC 是 JDK11 版本推出的垃圾回收器,所以在 JDK11 以及之后的版本的 JDK 下才能应用 ZGC,目前一般使用的 JDK8 版本较多一些,所以应用 ZGC 涉及到更换 JDK,有一定的升级成本,需要检查和升级项目的相关低版本依赖,比如 Jetty、jruby 等,还需要解决一些其他相关的报错问题等等。
- 本文地址:58 同城 HBase 平台 ZGC 应用实践
- 本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出
选择 JDK 版本
经过对比,我们选择了腾讯开源的 Tencent Kona JDK11,此 JDK 主要的优化项目有有向量计算(Vector API)、开箱即用的 ZGC、超大堆和内存成本优化策略。
编译&运行测试
我们使用的 HBase 版本是基于社区 1.4.11 基础上开发的版本,不支持 JDK11 及以上版本环境下的编译和运行,所以需要解决一些问题。主要遇到了如下的一些问题:
A. 依赖的组件包不支持 JDK11,如 Jetty、Jruby 等。需要升级对应的组件到高版本;
B. 个别类找不到,需要找到替换类或者依赖包。
C. 一些类无法访问,比如 jdk.internal.misc.Unsafe,需要在对应的子 moudle 的 pom 里的 maven-compiler-plugin 插件 configuration 配置上添加--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED,包括如果运行时出错,还需要在启动参数上添加上--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED。
D. 其他问题,比如 hbase shell 中的一些 warn log 等,通过调整代码解决。
ZGC 性能测试(对比 CMS)
在测试集群分别应用 CMS 和 ZGC 进行压测,测试 HBase 集群 3 台服务器(1 master、2 rs),测试 Client 在 master 节点上(避免和 rs 抢占系统资源,影响测试效果)
GC 性能对比
考虑写入过程会 memstore 占用内存资源,构造写压力场景,对比 GC 性能。
节点 | Throughput | Avg Pause GC Time | Pause Time | |
---|---|---|---|---|
RS1 | CMS | 98.968% | 152ms | 1 min 22 sec 10 ms |
RS1 | ZGC | 99.978% | 1.49ms | 1 sec 348 ms |
RS2 | CMS | 99.747% | 148ms | 1 min 19 sec 710 ms |
RS2 | ZGC | 99.978% | 1.47ms | 1 sec 380 ms |
ZGC 平均 GC 暂停时间比 CMS 短了很多,总的 GC 暂停时间少了很多,吞吐率稍高一点。
服务性能对比
读写平均延时对比
读写吞吐量对比
写入压测(100%WRITE): 粗略计算 ZGC 比 CMS 提升 6.15% 的性能。
workloada(50%WRITE,50%READ):粗略计算整体读写 ZGC 比 CMS 提升 5.21% 的性能, 粗略计算 READ 性能提升 11%。
workloade(95%SCAN,5%WRITE): 粗略计算 ZGC 比 CMS 提升 24% 的性能。
以上测试对比结果在不同的压测场景或者不同集群环境下可能会有所不同,不代表线上真实环境上的表现情况。下面说下在线上环境应用 ZGC 后各方面的总结。
线上应用总结
线上 HBase 集群升级新版本, 应用 ZGC,在个别分组上出现了请求延迟增加或过高的问题,经过不断的调优,目前线上集群的所有节点都已全部应用了 ZGC。应用 ZGC 之后,总结下服务性能、GC 性能方面应用 ZGC 的效果,同时也总结下遇见的问题以及解决办法。
GC 性能方面效果:
文章开头介绍了一个 regionserver 节点的 gc 情况,我们那这个节点 regionserver 进程应用 CMS 和 ZGC 进行对比说明。
节点 | Throughput | Avg Pause GC Time | Pause Time | |
---|---|---|---|---|
节点 1 | CMS | 99.912% | 57.4ms | 1 min 15 sec 920 ms |
节点 1 | ZGC | 99.944% | 3.71ms | 47 sec 930 ms |
ZGC 平均 GC 暂停时间比 CMS 短很多,大概为应用 CMS 的 6.46%,总的 GC 暂停时间少了很多,大概为应用 CMS 的 63%,吞吐率稍高一点。
服务性能方面效果:
一些 HBase 表的 put 或者 get 请求延迟方面有一定效果,升级后可以有效降低请求延迟峰值,减少延迟时间。下面会列出一部分进行说明。
- 一张核心 HBase 表(公司大部分业务均在使用,请求量较高)的批量写入延迟 P99 峰值减少了一些,大概为升级前 76.5%。
- 对比应用 ZGC 前后的两个时间段,个别 HBase 表在读流量增长近 10 倍的情况下,升级应用 ZGC 后 Get 请求延迟 P99 相较反而更加平稳一些,耗时峰值更少一些, 如下两个截图所示。
升级前:
升级后,
- 个别分组下的节点应用 ZGC 后批量写入延迟方面效果较明显,批量写入延迟 P99 稳定了很多,且整体降低了很多,如下两个截图所示(图中红框标记的是升级后的监控数据):
遇见的问题&解决办法
个别分组在应用 ZGC 后,出现了请求延迟的问题,下面列出一个分组应用 ZGC 后出现的问题以及解决方式
此分组请求特点分析:读写请求量较高,大部分为 put 写入请求。
此分组下节点应用 ZGC 后,出现了一些请求延迟过高的问题。经过排查是内存分配阻塞的问题(gc log 中出现了一些 Allocation Stall)导致请求延迟较高。继续分析 gc log,发现 gc 的耗时几乎都在 Concurrent Mark 阶段。所以考虑的解决办法一般有:
A. 直接增加内存,相当于多预留一定大小的内存,可以避免或者缓解内存分配阻塞的问题。
B. 优化 GC 参数,加快 Concurrent Mark 阶段的执行速度,减少一次完整 GC 的执行时间,提高 GC 的执行效率。
调整过程:
- 首先选择了第二种解决办法,优化 GC 参数,加快 Concurrent Mark 阶段的执行速度,同时调优了一些其他参数:
目前去掉了固定 GC 间隔时间参数,调高了正态分布模型预测系数 ZAllocationSpikeToleranceZAllocationSpikeTolerance(默认值为 2,值越大,越早的触发 GC),优化后调整到了 10,同时调高了 ConcGCThreads 参数来加快 Concurrent Mark 阶段的执行,ConcGCThreads 参数经过几次调优后调整到了 12。
- 经过第一步调优后,发现有时还会有内存分配阻塞导致的请求延迟问题,然后调整内存大小,由-Xmx30g -Xms30g 调整到了-Xmx40g -Xms40g。
ZGC 问题总结
经过具体的实践应用,结合一些技术资料,总结下 ZGC 的一些问题:
-
单代 GC 吞吐低:最显著的问题是 Concurrent Mark 阶段都需要全堆标记(耗时长),导致回收速度跟不上对象分配速度
1.1 会出现分配停顿(Allocation Stall),需要启动一次新的 ZGC,这次 ZGC 周期内所有应用线程都要暂停下来
1.2 最坏情况甚至发生 OOM:Concurrent Relocate 阶段如果剩余的空间依然不够,就会抛出 OOM;
-
GC 线程并发运行导致 CPU 偏高
为了加快并发标记阶段的执行,避免出现内存分配阻塞问题,线上环境一些分组配置的 ConcGCThreads 参数大一些,比如如下统计的节点的 CPU 数据,cpu.user 会从原来的峰值大约 18.47% 升高到应用 ZGC 时的峰值大约 56.01%,大概为原来的 300%。
- 对象分配卡顿问题
- RSS 非常高问题,这是由于目前 Linux 内核的 RSS 统计对这种 ZGC 应用的多重映射机制(multi-mapping)考虑的不是很完整导致的。
ZGC 采用 multi-mapping 实现了三份虚拟内存指向同一份物理内存,理论上在内核使用小页的 Linux 版本上,使用 ZGC 的 Java 进程 RSS 会比真实占用的高出 3 倍,这种异常的统计会带来一定的困扰。而在内核使用大页的 Linux 版本上,有不同的表现,这部分三映射的物理内存会算在 hugetlbfs inode 上,而不会统计到当前 Java 进程上。
同时通过 top 命令查看,发下 VIRT 和 RES 高很多。
经过实践,可以说明 ZGC 适合大内存进程,或者对单次请求延迟耗时要求较高的场景。最好再预留一部分内存的情况下,可以有效避免内存分配阻塞的问题。
总结
本篇文章简单介绍了 ZGC 的运行流程和特点,然后简单分析了 ZGC 的一些核心技术,如着色指针、读屏障等。并在相同的 YCSB 压测场景下,分别测试了 CMS 和 ZGC 在 HBase 上 GC 的表现能力,从 GC 停顿时间和读写吞吐、延迟等方面,做了比较详细的对比。然后又总结了在生产环境上的应用效果及问题等,HBase 使用 ZGC 在一些场景下可以有效的降低了服务端的延时。
未来 58 大数据平台将会逐步地在适合的应用场景上使用 ZGC,更好的服务于公司各个业务。HBase 方面也会持续进行优化,提高 HBase 服务的 SLA,在服务好离线业务的同时,未来可以逐步地扩展 HBase 在 58 在线业务方面的使用。最后推荐大家升级 ZGC,ZGC 作为下一代垃圾回收器,性能非常优秀,非常适合对延迟较敏感的业务场景。
参考文献:
[1] The Design of ZGC.
http://cr.openjdk.java.net/~pliden/slides/ZGC-PLMeetup-2019.pdf
[2] ZGC 官网:https://wiki.openjdk.java.net/display/zgc/Main
部门简介:
TEG-大数据部-数据平台部负责 58 数据中台的大数据基础平台能力的建设,拥有单集群 5000+ 的 Hadoop 集群,日万亿级实时数据分发,PB 级存储等,招聘大数据架构方向相关职位(HDFS/HBase/YARN/Spark/Presto/Flink/Kafak/Clickhouse/Druid/Kylin 等),联系邮箱 yuyi03@58.com,注明“大数据架构”。
作者简介:
李营,数据平台部 HBase 方向研发工程师。
注意:本文归作者所有,未经作者允许,不得转载