在HBase中应用MemStore-Local Allocation Buffers解决Full GC问题:第二部分
随着内存使用成本越来越低,高并发海量数据应用开发者逐渐倾向于这样一种思路:“内存就是新的硬盘,硬盘就是新的磁带”。即通过尽量多的使用内存,和尽量多的顺序读写磁盘实现高吞吐。因此大规模使用内存引起的Java GC问题就成为了一个普遍问题。翻译这篇文章的初衷是:(1)该系列文章以Hadoop为例介绍了GC停顿带来的问题,比较生动。(2)比较详细的介绍了GC原理,特别是CMS错误产生的原因,并且有实验例子。(3)介绍了一系列GC参数及GC profile思路,比较有借鉴价值。(4)最终实现方式几乎和我自己的实现思路一致,可以作为对照。
http://www.cloudera.com/blog/2011/02/avoiding-full-gcs-in-hbase-with-memstore-local-allocation-buffers-part-2/
作者:Todd Lipcon, software engineer for Cloudera. Todd在2012年Hadoop峰会上介绍了:Optimizing MapReduce Job Performance,对性能调优有丰富经验。
在HBase中应用MemStore-Local Allocation Buffers解决Full GC问题:第二部分
在上周的文章中,我们注意到HBase有垃圾收集导致的长时间停顿问题,总结了Java 6 VM使用的不同垃圾收集算法。然后我们推测长时间的垃圾收集暂停是由内存碎片引起的,并设计了一个实验证实这一假说,并探讨哪种场景(workload)最最容易产生这种问题。
实验结果
概述
正如以前的描述,我在HBase服务器上跑了三个不同的场景,同时打开-XX:PrintFLSStatistics= 1收集详细GC日志。结果如下:
图的上半部分显示free_space,即堆的总自由空间。图底部显示max_chunk,即最大连续可用块空间的大小。X轴是时间(以秒为单位),Y轴是堆word大小,一个堆word是8个字节,因为我在运行64位JVM。
从图中很容易看出,这三个不同的场景有非常不同的内存使用特性。我们将仔细看看每个部分。
只写场景
我们可以看到两个有趣的情况:free_space在2.8GB和3.8GB之间波动。每次free_space到2.8G时,就开始执行CMS
并释放了1GB左右的空间。这表明,CMS开始收集的时间(maoyidao注:由-XX:CMSInitiatingOccupancyFraction = N参数指定)已调整到足够低 - 即CMS开始运行时堆中有足够的自由空间。我们也可以看到没有内存泄漏 - 堆使用状况相当一致,随着时间的推移,没有偏向任何方向的趋势。
然而CMS工作时,从图中看到最大连续可用块空间的大小(max_chunk)急剧下降近下降到0。每次达到0(如在时间102800秒),我们看到max_trunk陡然回升到了最大值。同时考虑GC日志,长时间的GC停顿正好和max_chunk陡然回升的时间吻合 - 即每次FGC后整个堆碎片被整理,使所有的自由空间成为一个完整的大型块。
因此我们认识到当堆中没有大的连续空闲块时,确实造成堆碎片并导致写场景中的GC长暂停。
有缓存逐出情况的只读场景
(maoyidao注:有缓存逐出的意思是缓存大小不足以缓存所有数据,因此有缓存更替的情况)
在第二个场景中,客户端仅执行读取,要读取的记录集是远远大于LRU缓存的大小。所以,我们看到大量的对象从缓存中被驱逐。
只读场景的free_space图反映了这一点 - 它显示了比只写场景更加频繁的GC。然而,我们注意到max_chunk图
显示max_chunk值几乎保持常数。这表明只读的工作量不造成堆碎片,即使缓存交互比只写要高得多。
没有缓存逐出情况的只读场景
第三种场景(overview图中的绿色部分),因为有没有缓存逐出的情况,内存中分配是为每个RPC请求提供服务的短生命周期对象。因此,他们从未被晋升为老年代,free_space和max_chunk时间序列保持完全不变。
试验总结
总结这个实验的结果:
我们想消除的FGC是由于碎片,而不是并发模式失败。
只写的场景导致比只读多很多的碎片。
HBase的写路径
为了在分布式集群存储一个非常大的数据集,Apache HBase把每个表分割成段,叫做Region(区)。每个Region都有一个指定的“开始键”和“停止键”,包含每个落在两者之间的行key。这个方案可以和基于RDBMS中的主键分区相比较(尽管HBase透明的管理分区)。每个Region通常是小于1GB,所以每一个HBase的集群中的服务器负责几百个Region,因此有大量读取和写入请求被发送到服务器。
一旦一个写请求已经达到了正确的服务器,新的数据首先被添加到一个内存中的结构称为:MemStore。这本质上是一个包含所有最近写入的数据的有序映Map。当然,内存是一种有限的资源,因此该地区的服务器仔细估算内存的使用情况,并在内存使用达到阈值时触发刷新,把数据写入到磁盘并释放内存。
MemStore碎片
让我们想象一下,一个Region服务器托管5个Region - 粉色,蓝色,绿色,红色和黄色。
随机写入均匀地分散在整个地区,并没有特定的顺序到达。新写入到达时,为每一行数据分配新的缓冲区,然后这些缓冲数据被提升到老年代,并在MemStore中等待几分钟,然后被刷新到硬盘。由于没有特定的顺序写入到达,来自不同地区的数据混杂在老一代。当其中一个region被刷新时,意味着我们不能释放任何大的连续块,因此一定会得到碎片:
这种行为的结果,正是我们的实验结果表明:随着时间的推移,导致一个FGC。
未完继续...
在这篇文章中,我们回顾我们的实验结果,明白了为什么在HBase内存碎片的原因。在本系列的下一个,即最后一篇文章中,我们将看看MemStore避免碎片的缓冲区设计。