考虑CMS无法有效避免FGC,且单次GC耗时经常不可控。因此在如下两种场景下倾向于使用G1替换CMS:
- 大堆系统长时间FGC会引起上层服务异常,比如RegionServer/HiveServer等。
- 对读写毛刺比较敏感的在线数据库服务,比如在线推荐场景下的HBase,GC耗时过长就会导致整体可用率降低。
笔者在2019年开始将集团内部多数HBase所用CMS升级到G1,升级后大部分集群的GC性能提升还是比较显著的,接下来分为三个小节分别介绍相关的实践经验。
下面是两个经过简单参数调整后就立刻见到优化效果的案例:
-
A集群调优后GC耗时由调整前的100ms左右降低到20ms左右。如下图所示:
-
B集群调优后GC耗时由调整前的100ms降低到15ms左右,对应客户端写入RT由调整前的13ms降低到4ms。
生产线上一个RegionServer改成G1GC运行一段时间后发生了FGC,导致节点宕机,服务受到一定影响。查看了对应的GC日志,如下图所示:
从日志中可以看到这次GC没有GC出来任何空间,此时还需要为对象申请空间,发现内存已经用满了无法继续分配,最终导致Full GC。
经过进一步确认,发现MixGC触发的时机不对,进程设置的最大内存为40g,InitiatingHeapOccupancyPercent值为65,理论上所占内存达到40g*65%=26g的时候就应该执行MixGC。但实际上JVM执行MixGC的时候内存已经占用到了34g。如下图:
为什么会这样?实际上是因为RegionServer在运行过程中就需要这么多内存。这个根据Memstore的总大小就可以确认:
在高峰期光Memstore就需要25G的内存,已经达到了InitiatingHeapOccupancyPercent阈值所规定的的MixGC执行阈值,这还不算读缓存以及预留的10%~20%内存。可见,JVM就是需要36G那么多内存,GC不下去属于正常情况,一旦在GC的时候进来大量数据,就很有可能导致所有内存都被耗尽,最终FGC。
为什么Memstore占用那么多内存?是因为这个RegionServer上的region很多,达到了559个,一个region最大占用128M内存,25G内存也是容易达到的。
这个案例说明了在堆大小设置不合理的情况下是会发生这种FGC的。那应该如何有效避免呢?可以做如下两点优化:
2. 将参数hbase.regionserver.global.memstore.size需要从0.65稍微调小。、
对于RegionServer来说,堆内存大小需要结合Memstore以及Blockcache总大小进行设置,而不是拍脑袋。
生产线上另一个HBase集群从CMS GC改为G1GC后,一两天之后总发现在整点的时候会出现GC延迟的毛刺,如下图所示:
观察对应RegionServer节点的吞吐量,确认有业务会在整点有一个批量写入操作,如下图所示:
因此基本上可以确定是因为这个业务在整点的大吞吐量写入(单条记录字段很大)导致GC有个毛刺的。这个GC毛刺会导致业务写入延迟有所增大,如下所示:
DBA担心如果所有节点整点同时抖动,会不会对整个集群产生很大的影响,这个担心是合理的。需要说明的是,与G1GC对比,CMS GC在整点的时候并没有明显的抖动,所以G1GC肯定在这方面还有需要优化的点。
查看G1GC的gc日志,发现一个蹊跷的现象,每次在整点的一轮MixGC的最后一次都会出现一个长时间的GC卡顿,如下图所示:
看着日志,这次长时间的GC卡顿主要是Scan RS这个阶段比较长,2.1s中有1.99花费在Scan RS。那这个Scan RS是干什么的呢?
什么是RS(Remembered Set)?为什么Scan RS会很长?
这个内容在之前的系列文章中详细分析过,但为了文章的完整性,不麻烦读者再返回去找对应文章,这里再简单介绍一下。
G1GC模式下Java堆内存会分成一个一个的小Region(和HBase中Region不是一个概念),每个Region都会有一个对应的Remembered Set,这个Set主要记录那些引用了这个Region内对象的其他对象。为什么需要记录这些对象呢?是因为G1GC的GC属于整理式GC算法,即每次GC会将一个Region中活的对象拷贝到另一个空闲Region,并清空这个Region,这样就可以防止内存碎片的产生。很显然,对象在GC的过程中会发生移动,大家想想,如果对象移动的话,原先指向这些对象的引用是不是都需要更新,引用地址从原地址更新为新地址。为了方便查找那些需要更新地址的引用,就在Remembered Set中记录了那些指向本Region内对象的引用对象。说白了就是,哪个对象指向了这个Region的对象,就要在这个Region对应的Remembered Set中把你记下来,方便后续对你进行更新。上图中Scan RS阶段就是GC过程中扫描哪些指向了本Region中存活对象的引用对象。
Remembered Set具体是什么样的数据结构呢?
要解释这个问题,需要对上述的表述进行更细节的一些补充。首先来看Region这个概念,在实际实现中,每个Region会划分为很多小块,每个小块称为一个Card,每个Card的大小一般为512byte。但是Remembered Set中并不会记录哪个对象引用了对应Region中的对象,这个粒度太细,有可能导致Remembered Set很大,占用太多内存空间。在实际实现中,Remembered Set中记录的是哪个Region上的哪个Card上的对象引用了我这个Region中的对象,只是精确到Card粒度。
按照上述介绍,很显然,如果引用该Region中对象的其他Region(称为引用Region)越多,引用Region中引用Card越多,Remembered Set占用内存空间就会越大。为了控制Remebered Set占用内存空间的大小(毕竟这部分内存空间不是为业务服务的,不应该占用太多),Remembered Set根据引用Region个数的多少,设置了3种不同的实现方式,分别称为:
-
sparse per-region-table (PRT),使用HashMap方式记录引用关系,其中Map的Key是引用Region,Value是一个List,List中存储引用Region中的引用Card列表。
-
fine-grained PRT,还是使用HashMap方式记录引用关系,其中Map的Key是引用Region,但Value不再是List,而是一个bitmap,bit位为1表示对应的Card是引用Card,否则不是引用Card。
-
coarse-grained bitmap,从字面意思可以看出来这就是一个bitmap,不过bitmap中每个bit位引用粒度不再是Card,而是Region。如果bit位值为1,表示这个Region是引用Region,即这个Region中有对象引用了我这个Region中的对象。
上述3种实现方式中,spase PRT和fine-grained PRT都是精确到Card,而coarse-grained bitmap是精确到Region。
这里大家肯定有一个疑问,为什么Remembered Set会设计这3种实现方式呢?什么场景选择哪种实现方式?
Remembered Set实际使用哪种实现方式取决于它对应的Region有多热,就是说如果很多Region中的很多对象都引用这个Region中的对象,那这个Region就是很热,反之如果没有几个外部Region的对象引用这个Region中的对象,这个Region就冷一些。Region热度越高,如果采用fine-grained PRT的话,Remembered Set需要占用的内存空间就会越大,但Remembered Set是JVM自身运作的一个数据结构,不应该占用太多内存空间,不然就会挤占太多本应留给业务的内存空间,所以一旦Region热到一定程度,fine-grained PRT就会退化成coarse-grained bitmap,后者因为粒度更粗,占用内存会更小。但是,coarse-grained bitmap有个非常大的问题就是存储的粒度太粗,会导致扫描Remembered Set反向查询引用对象的时候,需要遍历引用Region查询引用对象,这个时间会很长。这就是Scan RS花费时间长的本质原因。
那有个问题就是fine-grained PRT退化为coarse-grained bitmap的触发条件具体是什么?
根据JVM源码分析,fine-grained PRT退化为coarse-grained bitmap主要取决于一个JVM参数-XX:G1RSetRegionEntries。源码中对这个参数的解释是:Max number of regions for which we keep bitmaps,我的理解是一旦引用Region的数量超过这个阈值,fine-grained PRT就会退化为coarse-grained bitmap。
-XX:G1RSetRegionEntries默认值是多少呢?
官方给的默认值是0,表示会根据当前其他配置进行自适应。根据源码分析,G1RSetRegionEntries默认值根据如下公式计算:
int region_size_log_mb = log(regionsize) - 20;
G1RSetRegionEntries = G1RSetRegionEntriesBase * (region_size_log_mb + 1);
其中regionsize表示Region的大小,由参数-XX:G1HeapRegionSize确定,HBase集群配置的是32M。G1RSetRegionEntriesBase是一个固定值,为256。
因此在32M大小Region的场景下,G1RSetRegionEntries 默认值为256 * (log(32 * 1024 * 1024) - 20 + 1) = 1536。
1. 业务整点写入会导致某些Region成为热Region。
2. 热Region对应的Remembered Set从fine-grained PRT退化为coarse-grained bitmap。
3. coarse-grained bitmap实现方式因为存储的是Region粒度,Scan RS阶段需要扫描对应Region中所有对象来确定是否引用了本Region中的对象,所需时间就会非常长。
4. G1GC在整点的时候Mix GC就会比较长。
按照这样的逻辑,那只需要牺牲一些内存空间,使得fine-grained PRT不要退化为coarse-grained bitmap就可以解决这个问题。所以理论上将-XX:G1RSetRegionEntrie从默认的1536增大就可以解决这个问题。目前线上将这个参数从默认值改为了4096,观察一周后,对应的GC延迟对比和业务写入延迟都得到了极大的改善,整点GC抖动不再出现,业务写入延迟也没有抖动。分别如下面两图所示:
上一篇文章系统介绍了G1GC的理论知识,本篇文章在此基础上介绍生产线上G1GC的相关实践案例。总结下来:
-
-
-
G1GC日志相比CMS GC来说非常详细,可以有效结合日志进行相关问题分析以及性能优化。
本文仅供学习!所有权归属原作者。侵删!文章来源: 大数据基建 -子和 :http://mp.weixin.qq.com/s/b7rpPwAsbZmIyNmVWD0alQ
文章评论