HBase原理--RegionServer核心组件之MemStore

HBase系统中一张表会被水平切分红多个Region,每一个Region负责本身区域的数据读写请求。水平切分意味着每一个Region会包含全部的列簇数据,HBase将不一样列簇的数据存储在不一样的Store中,每一个Store由一个MemStore和一系列HFile组成,如图所示。数据库

image.png
Region结构组成数组

HBase基于LSM树模型实现,全部的数据写入操做首先会顺序写入日志HLog,再写入MemStore,当MemStore中数据大小超过阈值以后再将这些数据批量写入磁盘,生成一个新的HFile文件。LSM树架构有以下几个很是明显的优点:缓存

•这种写入方式将一次随机IO写入转换成一个顺序IO写入(HLog顺序写入)加上一次内存写入(MemStore写入),使得写入性能获得极大提高。大数据领域中对写入性能有较高要求的数据库系统几乎都会采用这种写入模型,好比分布式列式存储系统Kudu、时间序列存储系统Druid等。安全

•HFile中KeyValue数据须要按照Key排序,排序以后能够在文件级别根据有序的Key创建索引树,极大提高数据读取效率。然而HDFS自己只容许顺序读写,不能更新,所以须要数据在落盘生成HFile以前就完成排序工做,MemStore就是KeyValue数据排序的实际执行者。数据结构

•MemStore做为一个缓存级的存储组件,老是缓存着最近写入的数据。对于不少业务来讲,最新写入的数据被读取的几率会更大,最典型的好比时序数据,80%的请求都会落到最近一天的数据上。实际上对于某些场景,新写入的数据存储在MemStore对读取性能的提高相当重要。多线程

•在数据写入HFile以前,能够在内存中对KeyValue数据进行不少更高级的优化。好比,若是业务数据保留版本仅设置为1,在业务更新比较频繁的场景下,MemStore中可能会存储某些数据的多个版本。这样,MemStore在将数据写入HFile以前实际上能够丢弃老版本数据,仅保留最新版本数据。架构

MemStore内部结构

上面讲到写入(包括更新删除操做)HBase中的数据都会首先写入MemStore,除此以外,MemStore还要承担业务多线程并发访问的职责。那么一个很现实的问题就是,MemStore应该采用什么样的数据结构,既可以保证高效的写入效率,又可以保证高效的多线程读取效率?并发

实际实现中,HBase采用了跳跃表这种数据结构,固然,HBase并无直接使用原始跳跃表,而是使用了JDK自带的数据结构ConcurrentSkipListMap。ConcurrentSkipListMap底层使用跳跃表来保证数据的有序性,并保证数据的写入、查找、删除操做均可以在O(logN)的时间复杂度完成。除此以外,ConcurrentSkipListMap有个很是重要的特色是线程安全,它在底层采用了CAS原子性操做,避免了多线程访问条件下昂贵的锁开销,极大地提高了多线程访问场景下的读写性能。异步

MemStore由两个ConcurrentSkipListMap(称为A和B)实现,写入操做(包括更新删除操做)会将数据写入ConcurrentSkipListMap A,当ConcurrentSkipListMap A中数据量超过必定阈值以后会建立一个新的ConcurrentSkipListMap B来接收用户新的请求,以前已经写满的ConcurrentSkipListMap A会执行异步f lush操做落盘造成HFile。分布式

MemStore的GC问题

MemStore从本质上来看就是一块缓存,能够称为写缓存。众所周知在Java系统中,大内存系统总会面临GC问题,MemStore自己会占用大量内存,所以GC的问题不可避免。不只如此,HBase中MemStore工做模式的特殊性更会引发严重的内存碎片,存在大量内存碎片会致使系统看起来彷佛还有不少空间,但实际上这些空间都是一些很是小的碎片,已经分配不出一块完整的可用内存,这时会触发长时间的Full GC。

为何MemStore的工做模式会引发严重的内存碎片?这是由于一个RegionServer由多个Region构成,每一个Region根据列簇的不一样又包含多个MemStore,这些MemStore都是共享内存的。这样,不一样Region的数据写入对应的MemStore,由于共享内存,在JVM看来全部MemStore的数据都是混合在一块儿写入Heap的。此时假如Region1上对应的全部MemStore执行落盘操做,就会出现图所示场景。

image.png

MemStore f lush产生内存条带

上图中不一样Region由不一样颜色表示,右边图为JVM中MemStore所占用的内存图,可见不一样Region的数据在JVM Heap中是混合存储的,一旦深灰色条带表示的Region1的全部MemStore数据执行f lush操做,这些深灰色条带所占内存就会被释放,变成白色条带。这些白色条带会继续为写入MemStore的数据分配空间,进而会分割成更小的条带。从JVM全局的视角来看,随着MemStore中数据的不断写入而且f lush,整个JVM将会产生大量愈来愈小的内存条带,这些条带实际上就是内存碎片。随着内存碎片愈来愈小,最后甚至分配不出来足够大的内存给写入的对象,此时就会触发JVM执行Full GC合并这些内存碎片。

MSLAB内存管理方式

为了优化这种内存碎片可能致使的Full GC,HBase借鉴了线程本地分配缓存(Thread-Local Allocation Buffer,TLAB)的内存管理方式,经过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC状况。具体实现步骤以下:

1)每一个MemStore会实例化获得一个MemStoreLAB对象。

2)MemStoreLAB会申请一个2M大小的Chunk数组,同时维护一个Chunk偏移量,该偏移量初始值为0。

3)当一个KeyValue值插入MemStore后,MemStoreLAB会首先经过KeyValue.getBuffer()取得data数组,并将data数组复制到Chunk数组中,以后再将Chunk偏移量往前移动data. length。

4)当前Chunk满了以后,再调用new byte[2 1024 1024]申请一个新的Chunk。

这种内存管理方式称为MemStore本地分配缓存(MemStore-Local AllocationBuffer,MSLAB)。下图是针对MSLAB的一个简单示意图,右侧为JVM中MemStore所占用的内存图,和优化前不一样的是,不一样颜色的细条带会汇集在一块儿造成了2M大小的粗条带。这是由于MemStore会在将数据写入内存时首先申请2M的Chunk,再将实际数据写入申请的Chunk中。这种内存管理方式,使得f lush以后残留的内存碎片更加粗粒度,极大下降Full GC的触发频率。

image.png

MemStore Chunk Pool

通过MSLAB优化以后,系统由于MemStore内存碎片触发的Full GC次数会明显下降。然而这样的内存管理模式并不完美,还存在一些“小问题”。好比一旦一个Chunk写满以后,系统会从新申请一个新的Chunk,新建Chunk对象会在JVM新生代申请新内存,若是申请比较频繁会致使JVM新生代Eden区满掉,触发YGC。试想若是这些Chunk可以被循环利用,系统就不须要申请新的Chunk,这样就会使得YGC频率下降,晋升到老年代的Chunk就会减小,CMS GC发生的频率也会下降。这就是MemStore Chunk Pool的核心思想,具体实现步骤以下:

1)系统建立一个Chunk Pool来管理全部未被引用的Chunk,这些Chunk就不会再被JVM看成垃圾回收。
2)若是一个Chunk没有再被引用,将其放入Chunk Pool。
3)若是当前Chunk Pool已经达到了容量最大值,就不会再接纳新的Chunk。
4)若是须要申请新的Chunk来存储KeyValue,首先从Chunk Pool中获取,若是可以获取获得就重复利用,不然就从新申请一个新的Chunk。

MSLAB相关配置

HBase中MSLAB功能默认是开启的,默认的ChunkSize是2M,也能够经过参数"hbase.hregion.memstore.mslab.chunksize"进行设置,建议保持默认值。Chunk Pool功能默认是关闭的,须要配置参数"hbase.hregion.memstore.chunkpool.maxsize"为大于0的值才能开启,该值默认是0。"hbase.hregion.memstore.chunkpool.maxsize"取值为[0, 1],表示整个MemStore分配给Chunk Pool的总大小为hbase.hregion.memstore.chunkpool. maxsize * Memstore Size。另外一个相关参数"hbase.hregion.memstore.chunkpool.initialsize"取值为[0, 1],表示初始化时申请多少个Chunk放到Pool里面,默认是0,表示初始化时不申请内存。

文章基于《HBase原理与实践》一书

相关文章
相关标签/搜索