深入了解Hbase(二)Hbase中的RegionServer详解
深入了解Hbase(二)Hbase中的RegionServer详解
深入了解Hbase中的RegionServer
RegionServer是 HBase系统中最核心的组件,主要负责用户数据写入、读取等基础操作。RegionServer组件:HLog ,MemStore、HFile以及 BlockCache。
(1)RegionServer核心模块
一个RegionServer由一个(或多个)HLog、一个 BlockCache 以及多个Region组成。
-
HLog用来保证数据写入的可靠性;
-
BlockCache可以将数据块缓存在内存中以提升数据读取性能;
-
Region是 HBase 中数据表的一个数据分片,一个 RegionServer上通常会负责多个Region 的数据读写。一个Region由多个Store组成,每个Store存放对应列簇的数据,比如一个表中有两个列簇,这个表的所有Region就都会包含两个Store。每个Store包含一个MemStore和多个 HFile,用户数据写入时会将对应列簇数据写入相应的 MemStore,一旦写入数据的内存大小超过设定阈值,系统就会将MemStore中的数据落盘形成HFile文件。HFile存放在HDFS上,是一种定制化格式的数据存储文件,方便用户进行数据读取。
(2)HLog
HBase 中系统故障恢复以及主从复制都基于HLog实现。默认情况下,所有写入操作(写入、更新以及删除)的数据都先以追加形式写入HLog,再写人MemStore。
-
大多数情况下,HLog并不会被读取,但如果RegionServer在某些异常情况下发生宕机,此时已经写入MemStore中但尚未flush到磁盘的数据就会丢失,需要回放HLog补救丢失的数据。
-
此外,HBase主从复制需要主集群将HLog日志发送给从集群,从集群在本地执行回放操作,完成集群之间的数据复制。
1、HLog文件结构
-
每个RegionServer拥有一个或多个HLog(默认只有1个,1.1版本可以开启 MultiWAL功能,允许多个HLog)。每个HLog是多个Region共享的,
上图中Region A 、Region B和 Region C共享一个HLog文件。 -
HLog中,日志单元WALEntry(图中小方框)表示一次行级更新的最小追加单元,它由 HLogKey和 WALEdit两部分组成,其中HLogKey 由 table nameregion name以及 sequenceid等字段构成。
2、HLog文件存储
HBase中所有数据(包括HLog以及用户实际数据)都存储在HDFS的指定目录:
其中,/hbase/WALs存储当前还未过期的日志;/hbase/oldWALs存储已经过期的日志。
/hbase/WALs目录下通常会有多个子目录,每个子目录代表一个对应的RegionServer。
hadopp01表示对应的RegionServer域名,160020为端口号,1657503428004为目录生成时的时间戳。每个子目录下存储该RegionServer内的所有HLog文件。
3、HLog文件生命周期
HLog生命周期包含4个阶段:
-
HLog构建: HBase的任何写入(更新、删除)操作都会先将记录追加写人到HLog文件中。
-
HLog滚动: HBase后台启动一个线程,每隔一段时间(由参数
hbase.regionserver.logroll.period
决定,默认1小时)进行日志滚动。 -
HLog 失效:写入数据一旦从 MemStore中落盘,对应的日志数据就会失效。为了方便处理,HBase中日志失效删除总是以文件为单位执行。杳看某个HLog文件是否失效只需确认该HLog文件中所有日志记录对应的数据是否已经完成落盘,如果日志中所有日志记录已经落盘,则可以认为该日志文件失效。一旦日志文件失效,就会从WALs文件夹移动到oldWALs文件夹。注意此时HLog并没有被系统删除。
4 ) HLog删除:Master后台会启动一个线程,每隔一段时间(参数hbase.master.cleaner.interval
,默认1分钟)检查一次文件夹 oldWALs下的所有失效日志文件,确认是否可以删除,确认可以删除之后执行删除操作。
确认条件主要有两个:
- 该HLog文件是否还在参与主从复制。对于使用HLog进行主从复制的业务,需要继续确认是否该HLog还在应用于主从复制。
- 该HLog文件是否已经在OldWALs目录中存在10分钟。为了更加灵活地管理HLog生命周期,系统提供了参数设置日志文件的TTL(参数
hbase.master.logcleaner.ttl
,默认10分钟)。
(3)MemoryStore
HBase系统中一张表会被水平切分成多个Region,每个 Region负责自己区域的数据读写请求。水平切分意味着每个Region 会包含所有的列簇数据,HBase将不同列簇的数据存储在不同的Store中,每个Store由一个 MemStore和一系列HFile组成。
1、MemoryStore内部结构
HBase基于LSM树模型实现,所有的数据写入操作首先会顺序写入日志HLog,再写入MemStore,当MemStore中数据大小超过阈值之后再将这些数据批量写入磁盘,生成一个新的HFile文件。
LSM树架构有如下几个非常明显的优势:
- 这种写入方式将一次随机IO写入转换成一个顺序IO写入(HLog顺序写入)加上一次内存写入(MemStore写入),使得写入性能得到极大提升。
- HFile中 KeyValue数据需要按照Key排序,排序之后可以在文件级别根据有序的Key建立索引树,极大提升数据读取效率。HDFS本身只允许顺序读写,不能更新,因此需要数据在落盘生成HFile之前就完成排序工作,MemStore就是KeyValue数据排序的实际执行者。
- MemStore作为一个缓存级的存储组件,总是缓存着最近写入的数据。对于很多业务来说,最新写入的数据被读取的概率会更大,最典型的比如时序数据,80%的请求都会落到最近一天的数据上。
- 在数据写入HFile之前,可以在内存中对KeyValue数据进行很多更高级的优化。比如,如果业务数据保留版本仅设置为1,在业务更新比较频繁的场景下,MemStore中可能会存储某些数据的多个版本。这样,MemStore在将数据写人 HFile之前实际上可以丢弃老版本数据,仅保留最新版本数据。
MemStore由两个ConcurrentSkipListMap(称为A和B)实现,写入操作(包括更新删除操作)会将数据写入ConcurrentSkipListMap A,当
ConcurrentSkipListMap A中数据量超过一定阈值之后会创建一个新的ConcurrentSkipListMap B来接收用户新的请求,之前已经写满的ConcurrentSkipListMap
A 会执行异步flush操作落盘形成HFile。
ConcurrentSkipListMap底层使用跳跃表来保证数据的有序性,并保证数据的写入、查找、删除操作都可以在O(logN)的时间复杂度完成。
除此之外,ConcurrentSkipListMap有个非常重要的特点是线程安全,它在底层采用了CAS原子性操作,避免了多线程访问条件下昂贵的锁开销,极大地提升了多线程访问场景下的读写性能。
2、MemoryStore中的GC
为什么MemoryStore会引起严重的内存碎片?
图为JVM中 MemStore所占用的内存图,可见不同Region 的数据在JVM Heap中是混合存储的,一旦深灰色条带表示的Region1的所有MemStore数据执行flush操作,这些深灰色条带所占内存就会被释放,变成白色条带。这些白色条带会继续为写入 MemStore的数据分配空间,进而会分割成更小的条带。
从JVM全局的视角来看,随着MemStore 中数据的不断写入并且flush,整个JVM将会产生大量越来越小的内存条带,这些条带实际上就是内存碎片。随着内存碎片越来越小,最后甚至分配不出来足够大的内存给写入的对象,此时就会触发JVM执行Full GC合并这些内存碎片。
Hbase为了减少内存碎片,做了哪些优化?
HBase借鉴了线程本地分配缓存(Thread-Local Allocation Buffer,TLAB)的内存管理方式,通过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC 情况,称为MemStore本地分配缓存(MSLAB)。
-
- 每个MemStore会实例化得到一个MemStoreLAB对象。
-
- MemStoreLAB会申请一个2M大小的Chunk 数组,同时维护一个Chunk偏移量,该偏移量初始值为0。
-
- 当一个KeyValue值插入 MemStore后,MemStoreLAB会首先通过KeyValue.getBuffer()取得data数组,并将data数组复制到Chunk数组中,之后再将Chunk偏移量往前移动data.length。
-
- 当前Chunk满了之后,再调用new byte[2 *1024 * 1024]申请一个新的Chunk。
上图,右侧为JVM中 MemStore所占用的内存图,和优化前不同的是,不同颜色的细条带会聚集在一起形成了2M大小的粗条带。这是因为MemStore会在将数据写入内存时首先申请2M的Chunk,再将实际数据写入申请的Chunk 中。这种内存管理方式,使得flush之后残留的内存碎片更加粗粒度,极大降低FullGC的触发频率。
经过MSLAB后的“小问题”
经过MSLAB优化之后,系统因为MemStore内存碎片触发的Full GC次数会明显降低。然而这样的内存管理模式并不完美,还存在一些“小问题”。
比如一旦一个Chunk 写满之后,系统会重新申请一个新的Chunk,新建Chunk对象会在JVM新生代申请新内存,如果申请比较频繁会导致JVM新生代Eden区满掉,触发 YGC。Hbase利用MemoryStore Chunk Pool来解决。
-
系统创建一个Chunk Pool来管理所有未被引用的Chunk,这些Chunk就不会再被JVM当作垃圾回收。
-
如果一个 Chunk没有再被引用,将其放入 Chunk Pool。
-
如果当前Chunk Pool已经达到了容量最大值,就不会再接纳新的Chunk。
-
如果需要申请新的Chunk来存储KeyValue,首先从 Chunk Pool中获取,如果能够获取得到就重复利用,否则就重新申请一个新的 Chunk。
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,表示初始化时不申请内存。
(4)HFile
HFile文件主要分为4个部分:Scanned block部分、Non-scanned block部分、Load-on-open部分和 Trailer。
Scanned Block部分:表示顺序扫描HFile时,所有的数据块将会被读取。这个部分包含3种数据块:Data Block,Leaf Index Block 以及 Bloom
Block。其中Data Block中存储用户的KeyValue数据,Leaf Index Block中存储索引树的叶子节点数据,Bloom Block中存储布隆过滤器相关数据。
Non-scanned Block部分:表示在 HFile顺序扫描的时候数据不会被读取,主要包括Meta Block和 Intermediate Level Data Index Blocks两部分。
Load-on-open部分:这部分数据会在RegionServer打开HFile时直接加载到内存中,包括FileInfo、布隆过滤器MetaBlock、 Root Data Index和 Meta IndexBloc
Trailer部分:这部分主要记录了HFile的版本信息、其他各个部分的偏移值和寻址信息。
HFile文件由各种不同类型的Block(数据块)构成,虽然这些Block 的类型不同,但却拥有相同的数据结构。Block 的大小可以在创建表列簇的时候通过参数
blocksize = > '65535’指定,默认为64K。通常来讲,大号的Block有利于大规模的顺序扫描,而小号的Block更有利于随机查询。
(5)BlockCache
为了提升读取性能,HBase实现了一种读缓存结构——BlockCache。客户端读取某个Block,首先会检查该Block是否存在于Block Cache,如果存在就直接加载出来,如果不存在则去 HFile文件中加载,加载出来之后放到Block Cache中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的IO操作。
BlockCache是 RegionServer级别的,一个 RegionServer只有一个BlockCache,在RegionServer启动时完成BlockCache 的初始化工作。
1、LRUBlockCache
LRUBlockCache是 HBase目前默认的BlockCache机制。它使用一个ConcurrentHashMap管理BlockKey到Block的映射关系,缓存Block只需要将BlockKey和对应的Block放入该HashMap中,查询缓存就根据BlockKey 从 HashMap中获取即可。同时,该方案采用严格的LRU淘汰算法,当Block Cache总量达到一定阈值之后就会启动淘汰机制,最近最少使用的Block 会被置换出来。
Hbase的缓存分层策略
HBase采用了缓存分层设计,将整个BlockCache分为三个部分: single-access、multi-access和 in-memory,分别占到整个BlockCache大小的25%、50%、25%。
在一次随机读中,一个 Block 从 HDFS中加载出来之后首先放入single-access区,后续如果有多次请求访问到这个Block,就会将这个Block移到multi-access区。而 in-memory区表示数据可以常驻内存,一般用来存放访问频繁、量小的数据,比如元数据,用户可以在建表的时候设置列簇属性IN_MEMORY= true,设置之后该列簇的Block在从磁盘中加载出来之后会直接放入 in-memory 区。
需要注意的是,设置IN_MEMORY=true并不意味着数据在写入时就会被放到in-memory区,而是和其他BlockCache区一样,只有从磁盘中加载出 Block之后才会放入该区。另外,进入in-memory区的Block并不意味着会一直存在于该区,仍会基于LRU淘汰算法在空间不足的情况下淘汰最近最不活跃的一些 Block。因为HBase系统元数据( hbase:meta,hbase:namespace等表)都存放在 in-memory区,因此对于很多业务表来说,设置数据属性IN_MEMORY=true时需要非常谨慎,一定要确保此列簇数据量很小且访问频繁,否则可能会将hbase:meta等元数据挤出内存,严重影响所有业务性能。
LRU淘汰算法
在每次cache block时,系统将BlockKey和 Block放入HashMap后都会检查BlockCache总量是否达到阈值,如果达到阈值,就会唤醒淘汰线程对Map中的Block进
行淘汰。系统设置3个 MinMaxPriorityQueue,分别对应上述3个分层,每个队列中的元素按照最近最少被使用的规则排列,系统会优先取出最近最少使用的
Block,将其对应的内存释放。3个分层中的 Block会分别执行LRU淘汰算法进行淘汰。
LRUBlockCache的缺点
LRUBlockCache方案使用JVM提供的HashMap管理缓存,简单有效。但随着数据从single-access区晋升到multi-access区或长时间停留在single-access区,对应的内存对象会从young区晋升到old区,晋升到old区的Block被淘汰后会变为内存垃圾,最终由CMS回收 (Conccurent Mark Sweep,一种标记清除算法),显然这种算法会带来大量的内存碎片,碎片空间一直累计就会产生Full GC。尤其在大内存条件下,一次Full GC很可能会持续较长时间,甚至达到分钟级别。
Full GC会将整个进程暂停,称为stop-the-world暂停(STW),因此长时间Full GC必然会极大影响业务的正常读写请求。
2、SlabCache
为了解决LRUBlockCache方案中因JVM垃圾回收导致的服务中断问题,SlabCache方案提出使用Java NIO DirectByteBuffer技术实现堆外内存存储,不再由JVM管理数据内存。默认情况下,系统在初始化的时候会分配两个缓存区,分别占整个BlockCache大小的80%和20%,每个缓存区分别存储固定大小的Block,其中**前者主要存储小于等于64K的Block,后者存储小于等于128K的 Block,如果一个Block太大就会导致两个区都无法缓存。**和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法淘汰过期的Block。和LRUBlockCache不同的是,SlabCache淘汰 Block时只需要将对应的BufferByte标记为空闲,后续cache对其上的内存直接进行覆盖即可。
线上集群环境中,不同表不同列簇设置的BlockSize都可能不同,很显然,默认只能存储小于等于128KB Block 的 SlabCache方案不能满足部分用户场景。比如,用户设置BlockSize = 256K,简单使用SlabCache方案就不能达到缓存这部分Block 的目的。因此HBase在实际实现中,将SlabCache和 LRUBlockCache搭配使用,称为DoubleBlockCache。在一次随机读中,一个 Block 从 HDFS 中加载出来之后会在两个Cache 中分别存储一份。缓存读时首先在LRUBlockCache中查找,如果Cache Miss再在 SlabCache中查找,此时如果命中,则将该Block 放入LRUBlockCache中。
DoubleBlockCache方案有很多弊端。比如,SlabCache中固定大小内存设置会导致实际内存使用率比较低,而且使用LRUBlockCache缓存Block依然会因为JVMGC产生大量内存碎片。因此在 HBase 0.98版本之后,已经不建议使用该方案。
3、BucketCache
BucketCache通过不同配置方式可以工作在三种模式下: heap,offheap和 file。
-
heap模式表示这些Bucket是从JVM Heap中申请的;
-
offheap模式使用DirectByteBuffer技术实现.堆外内存存储管理;
-
file模式使用类似SSD的存储介质来缓存Data Block。
无论工作在哪种模式下,BucketCache都会申请许多带有固定大小标签的Bucket,和 SlabCache一样,一种Bucket存储一种指定BlockSize的Data Block,但和SlabCache不同的是,BucketCache会在初始化的时候申请14种不同大小的Bucket,而且如果某一种 Bucket空间不足,系统会从其他 Bucket空间借用内存使用,因此不会出现内存使用率低的情况。
实际实现中,HBase将BucketCache和 LRUBlockCache搭配使用,称为CombinedBlock-Cache。和 DoubleBlockCache不同,系统在LRUBlockCache中
**主要存储Index Block 和 BloomBlock,而将Data Block存储在BucketCache 中。**因此一次随机读需要先在LRUBlockCache中查到对应的 Index Block,然后再到 BucketCache查找对应Data Block。BucketCache通过更加合理的设计修正了SlabCache的弊端,极大降低了JVM GC对业务请求的实际影响,但其也存在一些问题。比如,使用堆外内存会存在拷贝内存的问题,在一定程度上会影响读写性能。当然,在之后的2.0版本中这个问题得到了解决。
相比LRUBlockCache,BucketCache实现相对比较复杂。它没有使用JVM内存管理算法来管理缓存,而是自己对内存进行管理,因此大大降低了因为出现大量内存碎片导致FullGC发生的风险。
更多推荐
所有评论(0)