总结《HBase原理与实践》第五章
目录
一、RegionServer的核心模块
RegionServer是Hbase最核心的组件,负责用户数据写入、读取、等基础操作。
1.1RegionServer内部结构
RegionServer是Hbase系统响应用户读写请求的工作节点组件,有以下几个核心模块。
有一个或多个HLog,一个BlockCache以及多个Region组成。
一个Region由多个Store组成,一个store是一个列簇,一个store由1个memstore和多个Hfile组成。
1.2HLog
Hbase系统故障恢复主从复制都是基于HLog来实现的,默认任何操作都会先写入HLog,再将数据写入MemStore,如果此时系统发生故障,那么Memstore里的数据还未来得及写入HFile,导致数据丢失,就会通过HLog来进行回放。
HBase主从复制需要主集群将HLog日志发送给从集群,从集群在本地执行回放操作,完成集群之间的数据复制。
- 每个RegionServer有1个或多个HLog,默认只有1个,Region共享HLog。
- HLog会日志滚动。写入的数据一旦落盘,对用的日志就会失效,HLog文件中的所有数据都已经完成落盘,就会从WALs文件夹移动到了oldWALs文件夹,此时并没有删除。
- 该HLog文件是否还在参与主从复制。
- 该HLog文件是否在oldWALs日志存在10分钟。
1.3 MemStore
HBase基于LSM树模型实现,所有数据写入和操作首先顺序写入日志HLog,再写入MemStore,当MemStore超过一定阈值(时间,大小)就会将数据写入磁盘。
优势:
- 所有写入都是顺序IO写入
- HFile中keyvalue数据按照Key排序,排序之后可以在文件级别根据有序的key建立索引树,极大提升顺序读写。
- MemStore作为缓存组件,缓存中最新写入的数据,有极大可能会被读取。
MemStore中使用的是concurrentSkipListMap,写入、查找、删除都是O(logN)的时间复杂度.
特点:线程安全,他在底层采用了CAS算法,保证了原子性
MemStore由两个concurrentSkipListMap来实现,当第一个超过一定的阈值,会新建一个concurrentSkipListMap B,B用来接收, A会执行异步f lush操作落盘形成HFile。
1.3.1 MemStore的GC问题
MemStore从本质上来看就是一块缓存,可以称为写缓存。众所周知在Java系统中,大内存系统总会面临GC问题
HBase中MemStore工作模式的特殊性更会引起严重的内存碎片,存在大量内存碎片会导致系统看起来似乎还有很多空间,但实际上这些空间都是一些非常小的碎片,已经分配不出一块完整的可用内存,这时会触发长时间的Full GC。
原因:HBase的RegionServer包含多个region,每个region根据列簇又会有多个MemStore,这些MemStore是共享内存的,这样,不同Region的数据写入对应的MemStore,因为共享内存,在JVM看来所有MemStore的数据都是混合在一起写入Heap的。
随着MemStore中数据的不断写入并且flush,整个JVM将会产生大量越来越小的内存条带,这些条带实际上就是内存碎片。随着内存碎片越来越小,最后甚至分配不出来足够大的内存给写入的对象,此时就会触发JVM执行FullGC合并这些内存碎片。
1.3.2 MSLAB内存管理方式
HBase借鉴了线程本地分配缓存(Thread-Local Allocation Buffer,TLAB)的内存管理方式,通过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC情况。
参考:https://blog.csdn.net/dnc8371/article/details/106701220/
1.3.3 MemStore Chunk Pool
比如一旦一个Chunk写满之后,系统会重新申请一个新的Chunk,新建Chunk对象会在JVM新生代申请新内存,如果申请比较频繁会导致JVM新生代Eden区满掉,触发YGC。试想如果这些Chunk能够被循环利用,系统就不需要申请新的Chunk,这样就会使得YGC频率降低,晋升到老年代的Chunk就会减少,CMS GC发生的频率也会降低。
1.4 HFile
MemStore中数据落盘之后会形成一个文件写入HDFS,这个文件称为HFile。
- Scanned Block 表示顺序扫描HFile时所有的数据块将会被读取。DataBlock 存储用户key-value数据,Leaf Index Block存储索引树的叶子节点数据,Bloom Block中存储布隆过滤器相关数据。
- Non-scanned Block 这部分主要存储Meta Block,这种Block大多数情况下可以不用关心。
- Load-on-open 在RegionServer打开HFile就会加载到内存,作为查询的入口。 主要存储HFile元数据信息,包括索引根节点、布隆过滤器元数据等
- Trailer 部分主要记录了HFile的版本信息、其他各个部分的偏移值和寻址信息。
HFile文件由各种不同类型的Block(数据块)构成,虽然这些Block的类型不同,但却拥有相同的数据结构。
HBase在读HFile时会加载所有HFile的Trailer部分以及load-on-open部分到内存中。
写HFile从上至下写,先写用户keyvalue数据,根据keyvalue信息生成叶子索引信息和布隆过滤器,在根据叶子索引信息和布隆过滤器生成对应的元数据信息,在封装HFile的版本信息,keyvalue条数,根据上述的信息生成各个偏移量和寻址信息。这个文件的写入由上至下,没有发生过随机写。
读HFile从下至上读,RegionServer在启动时会自动读Trailer和Load-on-open部分,先读Trailer的版本信息,读取压缩格式,元数据信息等。
1.4.1 HFile中与布隆过滤器相关的Block
HBase会为每个HFile分配对应的位数组,KeyValue在写入HFile时会先对Key经过多个hash函数的映射,映射后将对应的数组位置为1,get请求进来之后再使用相同的hash函数对待查询Key进行映射,如果在对应数组位上存在0,说明该get请求查询的Key肯定不在该HFile中。
问题:HFile文件越大,里面存储的KeyValue值越多,位数组就会相应越大。一旦位数组太大就不适合直接加载到内存了。
根据Key进行拆分,一部分连续的Key使用一个位数组。这样,一个HFile中就会包含多个位数组,根据Key进行查询时,首先会定位到具体的位数组,只需要加载此位数组到内存进行过滤即可,从而降低了内存开销。
然后一个HFile会产生多个布隆过滤器,也会有布隆过滤器索引。
随着HFile文件越来越大,Data Block越来越多,索引数据也越来越大,已经无法全部加载到内存中,多级索引可以只加载部分索引,从而降低内存使用空间。同布隆过滤器内存使用问题一样。
1.5 BlockCache
提升数据库读取性能的一个核心方法是,尽可能将热点数据存储到内存中,以避免昂贵的IO开销。
HBase也实现了一种读缓存结构——BlockCache。BlockCache主要用来缓存Block,Block是Hbase中读取的最小单元。
BlockCache是RegionServer级别的,随着RegionServer的启动完成BlockCache的初始化。BlockCache机制有如下几种:
- LRU(默认的)将数据放在JVM堆中
- SlabCache(使用较少) 允许堆外内存 为了缓解GC
- BucketCache 允许堆外内存 为了缓解GC
1.5.1 LRU缓存机制
ConcurrentHashMap来维护key-value,将最近最少使用的Block置换出来。
注意以下几点:
- 缓存分层策略 随机读写放在一个区,经常被读的放在一个区,常驻内存的放在一个区(如元数据信息)
- 容易引发GC,因为缓存分层策略的机制,导致老年代数据增多,容易引发GC,而且CMS算法会产生大量的内存碎片。
1.5.2 SlabCache(已经不建议使用)
SlabCache中固定大小内存设置会导致实际内存使用率比较低,而且使用LRUBlockCache缓存Block依然会因为JVM GC产生大量内存碎片。
1.5.3 BucketCache
一种Bucket存储一种指定BlockSize的Data Block,BucketCache会在初始化的时候申请多种不同大小的Bucket。Hbase在启动后决定了标签的分类,有4k,16K,64K,128K,512K等,按照block大小来分配Bucket,
BucketCache通过不同配置方式可以工作在三种模式下:
- heap (heap模式需要首先从操作系统分配内存再拷贝到JVM heap)
- offheap (使用JAVA NIO DirectByteBuffer实现堆外内存存储管理)
- file 使用类似SSD的存储介质来缓存Data Block
offheap 和 heap的优缺点:
内存分配:offerheap是操作系统分配内存,降低了GC概率,heap是操作系统分配内存在拷贝到jvm heap。
读取缓存:heap直接从jvm heap上读取,offheap模式则需要首先从操作系统拷贝到JVMheap再读取,因此更费时。
实际实现中,HBase将BucketCache和LRUBlockCache搭配使用,称为CombinedBlock-Cache。
LRUBlockCache缓存 index Block 和 BloomBlock
BucketCache缓存DataBlock
因此一次随机读需要先在LRUBlockCache中查到对应的Index Block,然后再到BucketCache查找对应Data Block。