关于hbase:HBase原理RegionServer核心组件之MemStore

36次阅读

共计 3757 个字符,预计需要花费 10 分钟才能阅读完成。

HBase 零碎中一张表会被程度切分成多个 Region,每个 Region 负责本人区域的数据读写申请。程度切分意味着每个 Region 会蕴含所有的列簇数据,HBase 将不同列簇的数据存储在不同的 Store 中,每个 Store 由一个 MemStore 和一系列 HFile 组成,如图所示。


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 执行落盘操作,就会呈现图所示场景。

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 的触发频率。

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 原理与实际》一书

正文完
 0