HBASE(Memstore的专属JVM策略MSLAB)

        现在要提到一个全新的策略MSLAB,虽然它目的也是减少Full GC, 但是它的意义不止于此。就像我之前说的,堆内存足够大的时候发生Full GC 的停留时间可以长达好几分钟。解决这个问题不能完全靠JVM的GC回收 策略,最好的解决方案是从应用本身入手,自己来管好自己的内存空 间。

         随着硬件科技的进步,现在的服务器内存可以达到32GB、64GB甚至 100GB,人们发现就算是使用CMS策略来进行垃圾回收(GC),依然会触 发Full GC。但是在2GB、4GB的时代,一次Full GC最多也就几十秒,不 会超过一分钟;但是随着内存的加大,Full GC的时间逐渐变长。增加 的速率是8~10秒/G。你可以想象一下拥有100GB的堆内存的 RegionServer进行一次Full GC究竟要花费多少时间。而采用了CMS后还 是发生Full GC的原因是:

        (1)同步模式失败(concurrent mode failure):在CMS还没有 把垃圾收集完的时候空间还没有完全释放,而这个时候如果新生代的对 象过快地转化为老生代的对象时发现老生代的可用空间不够了。此时收 集器会停止并发收集过程,转为单线程的STW(Stop The World)暂 停,这就又回到了Full GC的过程了。 不过这个过程可以通过设置XX:CMSInitiatingOccupancyFraction=N来缓解。N代表了当JVM启动垃 圾回收时的堆内存占用百分比。你设置的越小,JVM越早启动垃圾回收 进程,一般设置为70。

        (2)由于碎片化造成的失败(Promotion Failure due to Fragmentation):当前要从新生代提升到老年代的对象比老年代的所 有可以使用的连续的内存空间都大。比如你当前的老年代里面有500MB 的空间是可以用的,但是都是1KB大小的碎片空间,现在有一个2KB的对 象要提升为老年代却发现没有一个空间可以插入。这时也会触发STW暂 停,进行Full GC。 这个问题无论你把-XX:CMSInitiatingOccupancyFraction=N调多小 都是无法解决的,因为CMS只做回收不做合并,所以只要你的 RegionServer启动得够久一定会遇上Full GC。 很多人可能会疑惑为什么会出现碎片内存空间? 我们知道Memstore是会定期刷写成为一个HFile的,在刷写的同时 这个Memstore所占用的内存空间就会被标记为待回收,一旦被回收了, 这部分内存就可以再次被使用,但是由于JVM分配对象都是按顺序分配 下去的,所以你的内存空间使用了一段时间后的情况如图8-1 所示。

HBASE(Memstore的专属JVM策略MSLAB)

       假设红色块占用的内存大小都是1KB,此时有一个2KB大小的对象从 新生代升级到老生代,但是此时JVM已经找不到连续的2KB内存空间去放 这个新对象了,如图8-2所示。

HBASE(Memstore的专属JVM策略MSLAB)

         JVM也没有办法,为了不让情况继续地恶化下去,只好停止接收一 切请求,然后启用一个单独的进程来进行内存空间的重新排列。这个排 列的时间随着内存空间的增大而增大,当内存足够大的时候,暂停的时 间足以让ZooKeeper认为我们的RegionServer已死。   

         其实JVM为了避免这个问题有一个基于线程的解决方案,叫 TLAB(Thread-Local allocation buffer)。当你使用TLAB的时候,每 一个线程都会分配一个固定大小的内存空间,专门给这个线程使用,当 线程用完这个空间后再新申请的空间还是这么大,这样下来就不会出现 特别小的碎片空间,基本所有的对象都可以有地方放。缺点就是无论你 的线程里面有没有对象都需要占用这么大的内存,其中有很大一部分空 间是闲置的,内存空间利用率会降低。不过能避免Full GC,这些都是 值得的。 

         但是HBase不能直接使用这个方案,因为在HBase中多个Region是被 一个线程管理的,多个Memstore占用的空间还是无法合理地分开。于是 HBase就自己实现了一套以Memstore为最小单元的内存管理机制,称为 MSLAB(Memstore-Local Allocation Buffers)。这套机制完全沿袭了 TLAB的实现思路,只不过内存空间是由Memstore来分配的。

        MSLAB的具体实现如下:

  1. 引入chunk的概念,所谓的chunk就是一块内存,大小默认为 2MB。
  2. RegionServer中维护着一个全局的MemStoreChunkPool实例,从 名字很容看出,是一个chunk池。
  3. 每个MemStore实例里面有一个MemStoreLAB实例。
  4. 当MemStore接收到KeyValue数据的时候先从ChunkPool中申请一 个chunk,然后放到这个chunk里面。
  5. 如果这个chunk放满了,就新申请一个chunk。 如果MemStore因为刷写而释放内存,则按chunk来清空内存。

       由此可以看出堆内存被chunk区分为规则的空间,这样就消除了小 碎片引起的无法插入数据问题,但是会降低内存利用率,因为就算你的 chunk里面只放1KB的数据,这个chunk也要占2MB的大小。不过,为了不 发生Full GC,这些都可以忍。

       跟MSLAB相关的参数是:

  • hbase.hregion.memstore.mslab.enabled:设置为true,即打开 MSLAB,默认为true。           
  •  hbase.hregion.memstore.mslab.chunksize:每个chunk的大 小,默认为2048 * 1024 即2MB。       
  • hbase.hregion.memstore.mslab.max.allocation:能放入chunk 的最大单元格大小,默认为256KB,已经很大了。   
  •  hbase.hregion.memstore.chunkpool.maxsize:在整个memstore 可以占用的堆内存中,chunkPool占用的比例。该值为一个百分 比,取值范围为0.0~1.0。默认值为0.0。
  • hbase.hregion.memstore.chunkpool.initialsize:在 RegionServer启动的时候可以预分配一些空的chunk出来放到 chunkPool里面待使用。该值就代表了预分配的chunk占总的 chunkPool的比例。该值为一个百分比,取值范围为0.0~1.0,默 认值为0.0。

 

具体MSLAB的效果如何

          在此引用Cloudera的工程师Todd Lipcon在Avoiding Full GCs in HBase with MemStore-Local Allocation Buffers文章中做的连续几天 的压力测试结果,如图8-4所示是不用MSLAB的JVM性能曲线。

HBASE(Memstore的专属JVM策略MSLAB)

       该图中的max_chunk可不是MSLAB中的chunk,这里的chunk代表了 JVM中连续可用的堆内存空间的最大值。可以看到该曲线出现了多次波 峰,然后缓慢下降,这意味着每次出现波峰的地方都发生了一次Full GC。JVM把堆内存空间重排了,这样才能清理出连续的可用空间。如图 8-5所示是采用了MSLAB后的曲线。

HBASE(Memstore的专属JVM策略MSLAB)

        从图8-5中可以看出最大连续堆内存空间自从降下来后就没有再起 来过,这意味着完全没有发生过Full GC!虽然最大连续堆内存空间一 直很低,但是没有关系,我们并不需要这么大的连续空间。所有的空间 已经被MSLAB的chunk所瓜分,所以可用空间的大小非常均匀。

 

G1GC和MSLAB可以一起用吗

      可以,他们之间并没有冲突。你可能会觉得G1GC跟MSLAB的实现思 路非常接近,那为什么还要发明MSLAB策略呢?因为G1GC是MSLAB发明后 才出现的策略。关于G1GC和MSLAB同时启用的测试,HBase的PMC成员 ramkrishna.s.vasudevan曾做过一次测试,以下引用自该报告。

      测试条件:

  • RegionServer堆内存:64GB。
  • JVM回收策略:G1GC。
  • 工具:HBase的 org.apache.hadoop.hbase.PerformanceEvaluation,简称PE。
  • 批量插入150GB的数据,每行数据拥有10个列。 采用50、100线程分别测试插入时间和GC次数。

     测试组1:用50线程插入数据,完成的时间比较,如表8-1所示。

HBASE(Memstore的专属JVM策略MSLAB)

    测试组2:用100线程插入数据,完成的时间比较,如表8-2所示。

HBASE(Memstore的专属JVM策略MSLAB)

     由此可以看出打开了MSLAB写入速度,提升了10%~12%,速度的提升 主要是因为减少 了GC的次数,JVM的工作更有效率了。

      在100线程的情况下,GC的次数比较,如表8-3所示。

HBASE(Memstore的专属JVM策略MSLAB)

      还是在100线程的情况下,GC的时间综合。如表8-4所示。

HBASE(Memstore的专属JVM策略MSLAB)

 

       所以打开MSLAB跟使用G1GC并没有冲突,性能还略有提升。