HBase中的MemStore

HBase的MemStore是基于LSM-Tree的数据结构,利用ConcurrentSkipListMap实现高效写入与查询。为解决内存碎片和GC压力,HBase引入了MemStoreLAB和ChunkPool,优化了内存分配,减少FullGC。进一步,通过MemStoreOffheap将Chunk内存堆外化,提高系统性能。这些优化策略提升了HBase在大数据场景下的写入和读取效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

HBase中的MemStore

1、什么是MemStore

2、MemStore的作用

3、什么是跳跃表

4、HBase MemStore实现与优化之旅

4.1 MemStore实现

4.2 MemStore中原生KeyValue对象内存存储优化

4.3 MemStore相关配置参数


HBase中的MemStore

HBase是一个基于HDFS的低成本、分布式LSM结构的数据库。可以支持毫秒级别查询;支持海量的PB级的大数据存储,适用于高QPS的随机读写和前缀范围查询等场景。

1、什么是MemStore

HBase中,Region是集群节点上最小的数据服务单元,用户数据表由一个或多个Region组成。在Region中每个ColumnFamily的数据组成一个Store。每个Store由一个Memstore和多个HFile组成,如下图所示:

< center>图1 Region结构组成 < /center>

2、MemStore的作用

  1. HBase是基于LSM-Tree模型的,所有的数据更新插入操作都首先写入Memstore中(同时会顺序写到日志HLog中),达到指定大小之后再将这些修改操作批量写入磁盘,生成一个新的HFile文件,这种设计可以极大地提升HBase的写入性能;

  2. HBase为了方便按照RowKey进行检索,要求HFile中数据都按照RowKey进行排序,Memstore数据在flush为HFile之前会进行一次排序,将数据有序化;(HFile中keyvalue数据按照key排序,可以在文件级别根据有序的key建立索引树,极大提升数据读取效率。但是HDFS本身只允许顺序读写,不能更新)

  3. 根据局部性原理,新写入的数据会更大概率被读取,因此HBase在读取数据的时候首先检查请求的数据是否在Memstore,写缓存未命中的话再到读缓存中查找,读缓存还未命中才会到HFile文件中查找,最终返回merged的一个结果给用户。

  4. 实现更多高级优化:2.0之后的内存内合并等

MemStore既需要保证高效的写入效率,又要保证高效的多线程读取效率,所以MemStore采用跳跃表的数据结构(JDK自带的ConcurrentSkipListMap)。

3、什么是跳跃表

ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。(采用CAS原子性操作,避免多线程访问条件下昂贵的锁开销)

ConcurrentSkipListMap的底层是通过跳表来实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。

跳表(SkipList):使用“空间换时间”的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。在查找时,首先按照层级查找,比如:当前跳表最高层级为3,即每个结点中不仅记录了next结点(层级1),还记录了next的next(层级2)、next的next的next(层级3)结点。现在查找一个结点,则从头结点开始先按高层级开始查:head->head的next的next的next->。。。直到找到结点或者当前结点q的值大于所查结点,则此时当前查找层级的q的前一节点p开始,在p~q之间进行下一层级(隔1个结点)的查找......直到最终迫近、找到结点。此法使用的就是“先大步查找确定范围,再逐渐缩小迫近”的思想进行的查找。

例如:有当前的跳表存储如下:有4个层级,层级1为最下面的level,是一个包含了所有结点的普通链表。往上数就是2,3,4层级。

现在,我们查找结点值为19的结点:

参考网址:https://www.jianshu.com/p/dc252b5efca6

扩展:

ConcurrentSkipListMap和TreeMap

ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。

在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。 但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点: 1、ConcurrentSkipListMap 的key是有序的。 2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。 在非多线程的情况下,应当尽量使用TreeMap。此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMapTreeMap进行包装,也可以提供较好的效率。对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度。 所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度。 注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。

4、HBase MemStore实现与优化之旅

参考博客:http://hbasefly.com/2019/10/18/hbase-memstore-evolution/

4.1 MemStore实现

ConcurrentSkipListMap可以实现高效的插入/删除/查询操作,其复杂度都是O(logN)。ConcurrentSkipListMap的结构如下图所示:

                                                                                                                                 图2 ConcurrentSkipListMap的结构

基于ConcurrentSkipListMap这样的基础数据结构,按照最简单的思路来看,如果写入一个KeyValue到MemStore中,肯定是如下的写入步骤:

1.在JVM堆中为KeyValue对象申请一块内存区域。

2.调用ConcurrentSkipListMap的put(K key, V value)方法将这个KeyValue对象作为参数传入。

                                                                                                                                 图3 基于跳表实现的最基础MemStore模型

这样看起来,实现非常简单。根据key查询时可以利用跳跃表的有序性。但是这样的内存存储模型会有很多问题(具体见下节),本文就基于这个最原始的模型开始优化之旅。

再看图3这个存储模型,可以发现MemStore从内存管理上来说主要由两个部分组成,一个是原生KeyValue的内存管理,见上图下半部分;一个是ConcurrentSkipListMap的内存管理,见上图上半部分。接下来笔者分别就这两个部分的内存管理优化,分成两个小节进行深入介绍。

4.2 MemStore中原生KeyValue对象内存存储优化

对于HBase这样基于LSM实现的MemStore来说,上述实现方案每写入一个KeyValue,在没有写入ConcurrentSkipList之前就需要申请一个内存对象,可以想见,对于很多写入吞吐量几万每秒的业务来说,每秒就会有几万个内存对象产生,这些对象会在内存中存在很长一段时间,对应的会晋升到老生代,一旦执行了flush操作,老生代的这些对象会被GC回收掉。这样的内存玩法,会导致JVM的GC压力非常大。

GC压力主要来源于:这些内存对象一旦晋升到老生代,执行完Major GC后会存在大量的非常小的内存碎片,这些内存碎片会引起频繁的Full GC,而且每次Full GC的时间会异常的长。

4.2.1 MemStore引入MemStoreLAB

针对上面的问题,MemStore借鉴线程本地分配缓存(Thread Local Allocation Buffer,TLAB)机制,实现了MemStoreLAB,简称MSLAB。通过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC的问题。基于MSLAB实现写入的核心流程如下:

  • 每个memstore会实例化得到一个MemStoreLAB对象。

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

  • 当memStore有新的KV数据插入时,通过KeyValue.getBuffer()得到data数组,并将data数组复制到Chunk数组中,并增加偏移量为=之前偏移量+data.length

  • 当前Chunk数组满了之后,在调用 new Byte[210241024]申请一个新的Chunk数组

基于MSLAB的MemStore可以表征为下图:

                                                                                                                                 图4 基于MSLAB实现的MemStore示意图

                                                                                                                                 图5 MSLAB效果示意图

经过MSLAB优化之后,系统因为memstore内存碎片触发的Full Gc次数明显降低。但是还是有一些小问题,比如一旦Chunk数组写满之后,系统会重新申请一个新的Chunk,新建Chunk对象会在JVM新生代申请新内存。如果申请比较频繁,会导致JVM新生代Eden区满掉,触发ygc。为了解决这个问题,引出了MemStore Chunk Pool。

4.2.2 MemStore引入ChunkPool

MSLAB机制中KeyValue写入Chunk,如果Chunk写满了会在JVM堆内存申请一个新的Chunk。引入ChunkPool后,申请Chunk都从ChunkPool中申请,如果ChunkPool中没有可用的空闲Chunk,才会从JVM堆内存中申请新Chunk。如果一个MemStore执行flush操作后,这个MemStore对应的所有Chunk都可以被回收,回收后重新进入池子中,以备下次使用。基本原理如图6、图7所示:

                                                                                                                                 图6 基于ChunkPool实现的Chunk管理模型

每个RegionServer会有一个全局的Chunk管理器,负责Chunk的生成、回收等。MemStore申请Chunk对象会发送请求让Chunk管理器创建新Chunk,Chunk管理器会检查当前是否有空闲Chunk,如果有空闲Chunk,就会将这个Chunk对象分配给MemStore,否则从JVM堆上重新申请。每个MemStore仅持有Chunk内存区域的引用,如图3中MemStoreLAB的小格子。

下图是MemStore执行Flush之后,对应的所有Chunk对象中KV落盘形成HFile,这部分Chunk就可以被Chunk管理器回收到空闲池子。

                                                                                                                                 图7 MemStore Flush过程中Chunk回收过程

使用ChunkPool的好处是什么呢?因为Chunk可以回收再使用,这就一定程度上降低了Chunk对象申请的频率,有利于Young GC。

4.2.2 MemStore Offheap实现

除过ChunkPool之外,HBase 2.x版本针对Chunk对象优化的另一个思路是将Chunk使用的这部分内存堆外化。关于堆外内存的细节内容,以后再做详细介绍。

Chunk堆外化实现比较简单,在创建新Chunk时根据用户配置选择是否使用堆外内存,如果使用堆外内存,就使用JDK提供的ByteBuffer.allocateDirect方法在堆外申请特定大小的内存区域,否则使用ByteBuffer.allocate方法在堆内申请。如果不做配置,默认使用堆内内存,用户可以设置hbase.regionserver.offheap.global.memstore.size这个值为大于0的值开启堆外,表示RegionServer中所有MemStore可以使用的堆外内存总大小。

4.3 MemStore相关配置参数

hbase.hregion.memstore.mslab.enabled=true // 开启MSALB hbase.hregion.memstore.mslab.chunksize=2m // chunk的大小,越大内存连续性越好,但内存平均利用率会降低 hbase.hregion.memstore.mslab.max.allocation=256K // 通过MSLAB分配的对象不能超过256K,否则直接在Heap上分配,256K够大了

Chunk Pool配置 hbase.hregion.memstore.chunkpool.maxsize = [0,1] 0-1之间取值。默认是0,大于0才会开启,表示占比

hbase,hregion,memstore.chunkpool.initialsize=[0,1] 0-1之间取值,默认0,表示初始化时申请多少个chunk放入到chunkPool

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值