文章目录
HBase RegionServer介绍
HBase客户端
Hbase客户端读写数据时,都要先根据hbase:meta表(元数据表)确定数据在哪个Region上,然后再根据Region的RegionServer信息,去对应的RegionServer上读取数据。hbase:meta表,专门用来存放整个集群所有的Region信息。
HBase保证hbase:meta表始终只有一个Region,这是为了确保meta表多次操作的原子性,因为HBase本质上只支持Region级别的事务。
hbase:meta表只有一个Region,如果所有的流量都先请求hbase:meta表找到Region,再请求Region所在的RegionServer,那么hbase:meta表的将承载巨大的压力,这个Region将马上成为热点Region,且根本无法承担数千万的流量。
HBase客户端缓存hbase:meta表的Region信息
HBase客户端有一个叫做MetaCache的缓存,在调用HBase API时,客户端会先去MetaCache中找到业务rowkey所在的Region,此时可能有三种情况:
- Region信息为空,MetaCache没有这个region的缓存数据;首先通过zookeeper获取hbase:meta表所在的RegionServer,然后在hbase:meta表找到业务rowkey所在的region,接着缓存信息到MetaCache;
- Region信息不为空,但是请求对应RegionServer后发现Region并不在这个RegionServer上,MetaCache信息过期;通过hbase:meta表找到正确的Region并缓存;
- Region信息不为空,但是请求对应RegionServer后发现Region是正确的
HBase常见的超时参数
-
hbase.rpc.timeout:表示单次RPC请求的超时时间,一旦单次RPC超过该时间,上层将收到TimeoutException。默认为60000ms。
-
hbase.client.retries.number:表示调用API时最多容许发生多少次RPC重试操作。2.x默认为15次。(1.x
35次)
-
hbase.client.pause:表示连续两次RPC重试之间的休眠时间,默认为100ms。注意,HBase的重试休眠时间是按照随机退避算法计算的,若hbase.client.pause=100,则第一次RPC重试前将休眠100ms左右,第二次RPC重试前将休眠200ms左右,第三次RPC重试前将休眠300ms左右,第四次重试前将休眠500ms左右,第五次重试前将休眠1000ms左右,第六次重试则将休眠2000ms左右……也就是重试次数越多,则休眠的时间会越长。因此,若按照默认的hbase.client.retries.number=35,则可能长期卡在休眠和重试两个步骤中。
-
hbase.client.operation.timeout:表示单次API的超时时间,默认值为1200000ms。注意,get/put/delete等表操作称为一次API操作,一次API可能会有多次RPC重试,这个operation.timeout限制的是API操作的总超时。
假设某业务要求单次HBase的读请求延迟不超过1s,那么该如何设置上述4个超时参数呢?
首先,hbase.client.operation.timeout应该设成1s。
其次,在SSD集群上,如果集群参数设置合适且集群服务正常,则基本可以保证p99延迟在100ms以内,因此hbase.rpc.timeout设成100ms。这里,hbase.client.pause用默认的100ms。
最后,在1s之内,第一次RPC耗时100ms,休眠100ms;第二次RPC耗时100ms,休眠200ms;第三次RPC耗时100ms,休眠300ms;第四次RPC耗时100ms,休眠500ms(不是完全线性递增的)。因此,在hbase.client.operation.timeout内,至少可执行4次RPC重试,实际中单次RPC耗时可能更短(因为有hbase.rpc.timeout保证了单次RPC最长耗时),所以hbase.client.retries.number可以稍微设大一点(保证在1s内有更多的重试,从而提高请求成功的概率),比如设成6次。
RegionServer
一个RegionServer由一个(或多个)HLog、一个BlockCache以及多个Region组成。
HLog用来保证数据写入的可靠性;
BlockCache可以将数据块缓存在内存中以提升数据读取性能;
Region是HBase中数据表的一个数据分片,一个RegionServer上通常会负责多个Region的数据读写。
一个Region由多个Store组成,每个Store存放对应列簇的数据,比如一个表中有两个列簇,这个表的所有Region就都会包含两个Store。
每个Store包含一个MemStore和多个HFile,用户数据写入时会将对应列簇数据写入相应的MemStore,一旦写入数据的内存大小超过设定阈值,系统就会将MemStore中的数据落盘形成HFile文件。
HFile存放在HDFS上,是一种定制化格式的数据存储文件,方便用户进行数据读取。
HLog
HBase中系统故障恢复以及主从复制都基于HLog实现。默认情况下,所有写入操作(写入、更新以及删除)的数据都先以追加形式写入HLog,再写入MemStore。大多数情况下,HLog并不会被读取,但如果RegionServer在某些异常情况下发生宕机,此时已经写入MemStore中但尚未flush到磁盘的数据就会丢失,需要回放(replay)HLog补救丢失的数据。
HBase主从复制需要主集群将HLog日志发送给从集群,从集群在本地执行回放操作,完成集群之间的数据复制。
HLog生命周期
HLog生命周期包含4个阶段:
- HLog构建:HBase的任何写入(写入、更新、删除)操作都会先将记录追加写入到HLog文件中。
- HLog滚动:HBase后台启动一个线程,每隔一段时间(由参数’hbase.regionserver.logroll.period’决定,默认1小时)进行日志滚动。日志滚动会新建一个新的日志文件,接收新的日志数据。
- HLog失效:写入数据一旦从MemStore中落盘,对应的日志数据就会失效。为了方便处理,HBase中日志失效删除总是以文件为单位执行。一旦日志文件失效,就会从WALs文件夹移动到oldWALs文件夹。注意此时HLog并没有被系统删除。
- HLog删除:Master后台会启动一个线程,每隔一段时间(参数’hbase.master.cleaner.interval’,2.x默认10分钟,1.x默认1分钟)检查一次文件夹oldWALs下的所有失效日志文件,确认是否可以删除,确认可以删除之后执行删除操作。确认条件主要有两个:
- 该HLog文件是否还在参与主从复制。
- 该HLog文件是否已经在OldWALs目录中存在10分钟。设置日志文件的TTL(参数’hbase.master.logcleaner.ttl’,默认10分钟)
MemStore
HBase系统中一张表会被水平切分成多个Region,每个Region负责自己区域的数据读写请求。水平切分意味着每个Region会包含所有的列簇数据,HBase将不同列簇的数据存储在不同的Store中,每个Store由一个MemStore和一系列HFile组成。
HBase基于LSM树模型实现,所有的数据写入操作首先会顺序写入日志HLog,再写入MemStore,当MemStore中数据大小超过阈值之后将数据批量写入磁盘,生成一个新的HFile文件。
-
将一次随机IO写入转换成一个顺序IO写入(HLog顺序写入)加上一次内存写入(MemStore写入),极大提升写入性能;
-
HFile中KeyValue数据是按照Key排序的,数据flush到HFile前已经在MemStore中排好序;
-
MemStore总是缓存着最近写入的数据。对于很多业务来说,最新写入的数据被读取的概率会更大;
-
MemStore采用跳跃表的数据结构,既保证高效的写入效率,又保证高效的多线程读取效率;
MemStore采用的数据结构是 ConcurrentSkipListMap,ConcurrentSkipListMap底层使用跳跃表来保证数据的有序性,并保证数据的写入、查找、删除操作都可以在O(logN)的时间复杂度完成。ConcurrentSkipListMap是线程安全的,底层采用了CAS原子性操作,避免了多线程访问条件下昂贵的锁开销,极大地提升了多线程访问场景下的读写性能。
MemStore由两个ConcurrentSkipListMap(称为A和B)实现,写入操作(包括更新删除操作)会将数据写入ConcurrentSkipListMap A,当ConcurrentSkipListMap A中数据量超过一定阈值之后会创建一个新的ConcurrentSkipListMap B来接收用户新的请求,之前已经写满的ConcurrentSkipListMap A会执行异步flush操作落盘形成HFile。
MemStore的GC问题
MemStore是一块缓存,可以称为写缓存。大内存java系统总会面临GC问题,MemStore本身会占用大量内存;
MemStore的工作模式会引起严重的内存碎片:因为一个RegionServer由多个Region构成,每个Region根据列簇的不同又包含多个MemStore,不同Region的数据写入对应的MemStore,因为共享内存,在JVM看来所有MemStore的数据都是混合在一起写入Heap的。
MSLAB内存管理方式
MemStore本地分配缓存(MemStore-Local Allocation Buffer,MSLAB)
MemStoreLAB会申请一个2M大小的Chunk数组,同时维护一个Chunk偏移量(初始为0);
数据data写入Chunk数组,Chunk偏移量移动data.length;
Chunk数组写满后,再申请一个新的Chunk数组;
MemStore Chunk Pool
MSLAB存在的问题,一个Chunk写满之后,系统会重新申请一个新的Chunk(新建Chunk对象会在JVM新生代申请新内存,如果申请比较频繁会导致JVM新生代Eden区满掉,触发YGC。)
MemStore Chunk Pool的核心思想:chunk循环利用,系统不需要申请新的Chunk
MSLAB相关配置
HBase中MSLAB功能默认是开启的,默认的ChunkSize是2M,可通过参数"hbase.hregion.memstore.mslab.chunksize"进行设置,建议保持默认值。
Chunk Pool功能2.x默认是开启的(1.x默认关闭),需要配置参数"hbase.hregion.memstore.chunkpool.maxsize"为大于0的值才能开启,该值默认是1。"hbase.hregion.memstore.chunkpool.maxsize"取值为[0,1],表示整个MemStore分配给Chunk Pool的总大小为hbase.hregion.memstore.chunkpool.maxsize * Memstore Size。
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 IndexBlock。
- Trailer部分:这部分主要记录了HFile的版本信息、其他各个部分的偏移值和寻址信息。
HFile文件由各种不同类型的Block(数据块)构成,类型不同,拥有相同的数据结构。
Block的大小可以在创建表列簇的时候通过参数blocksize=>'65535’指定,默认为64K。通常来讲,大号的Block有利于大规模的顺序扫描,而小号的Block更有利于随机查询。
BlockCache
BlockCache是读缓存,客户端读取某个Block,首先会检查该Block是否存在于BlockCache,如果存在就直接加载出来,如果不存在则去HFile文件中加载,加载出来之后放到Block Cache中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的IO操作。
Block是HBase中最小的数据读取单元,即数据从HFile中读取都是以Block为最小单元执行的。
三种BlockCache方案
LRUBlockCache是最早的实现方案,第二种方案是SlabCache,第三种方案是BucketCache;
三种方案的不同之处主要在于内存管理模式
-
LRUBlockCache是将所有数据都放入JVM Heap中,交给JVM进行管理;
-
后两种方案采用的机制允许将部分数据存储在堆外。这种演变本质上是因为LRUBlockCache方案中JVM垃圾回收机制经常导致程序长时间暂停,而采用堆外内存对数据进行管理可以有效缓解系统长时间GC。
LRUBlockCache
缓存Block是将BlockKey和对应的Block放入HashMap中,查询缓存是根据BlockKey从HashMap中获取,
采用了LRU淘汰算法。
实现细节关注点
-
缓存分层策略
HBase采用了缓存分层设计,将整个BlockCache分为三个部分:single-access、multi-access和in-memory,分别占到整个BlockCache大小的25%、50%、25%,3个分层中的Block会分别执行LRU淘汰算法进行数据淘汰。
在一次随机读中,一个Block从HDFS中加载出来之后首先放入single-access区,后续如果有多次请求访问到这个Block,就会将这个Block移到multi-access区。
in-memory区表示数据可以常驻内存,一般用来存放访问频繁、量小的数据,比如元数据,用户可以在建表的时候设置列簇属性IN_MEMORY=true,设置之后该列簇的Block在从磁盘中加载出来之后会直接放入in-memory区。
因为HBase系统元数据(hbase:meta,hbase:namespace等表)都存放在in-memory区,设置数据属性IN_MEMORY=true时需要非常谨慎,一定要确保此列簇数据量很小且访问频繁,否则可能会将hbase:meta等元数据挤出内存,严重影响所有业务性能。
-
LRUBlockCache方案优缺点
LRUBlockCache方案使用JVM提供的HashMap管理缓存,简单有效。但随着数据从single-access区晋升到multi-access区或长时间停留在single-access区,对应的内存对象会从young区晋升到old区,晋升到old区的Block被淘汰后会变为内存垃圾。Full GC会将整个进程暂停,称为stop-the-world暂停(STW),因此长时间Full GC必然会极大影响业务的正常读写请求。
SlabCache
为了解决LRUBlockCache方案中因JVM垃圾回收导致的服务中断问题,SlabCache使用堆外内存存储,不再由JVM管理数据内存。
默认情况下,系统在初始化的时候会分配两个缓存区,分别占整个BlockCache大小的80%和20%,每个缓存区分别存储固定大小的Block,其中前者主要存储小于等于64K的Block,后者存储小于等于128K的Block。如果一个Block太大就会导致两个区都无法缓存。
采用了LRU淘汰算法,和LRUBlockCache不同的是,将该内存空间标记为空闲,后续可以复用。
不同表不同列簇设置的BlockSize都可能不同,SlabCache Size无法确定;
DoubleBlockCache:SlabCache和LRUBlockCache搭配使用。在一次随机读中,一个Block从HDFS中加载出来之后会在两个Cache中分别存储一份。缓存读时首先在LRUBlockCache中查找,如果Cache Miss再在SlabCache中查找,此时如果命中,则将该Block放入LRUBlockCache中。
DoubleBlockCache方案弊端:SlabCache中固定大小内存设置会导致实际内存使用率比较低,而且使用LRUBlockCache缓存Block会因为JVM GC产生大量内存碎片。
BucketCache
BucketCache通过不同配置方式可以工作在三种模式下:heap,offheap和file。
-
heap模式表示Bucket是从JVM Heap中申请的;
-
offheap模式使用堆外内存存储;
-
file模式使用类似SSD的存储介质来缓存Data Block。
无论工作在哪种模式下,BucketCache都会申请许多(14种)带有固定大小标签的Bucket,如果某一种Bucket空间不足,系统会从其他Bucket空间借用内存,因此不会出现内存使用率低的情况。
BucketCache中Block缓存读写
读写流程主要包括5个模块:
- RAMCache是一个存储blockKey和Block对应关系的HashMap。
- WriteThead是整个Block写入的中心枢纽,主要负责异步地将Block写入到内存空间。
- BucketAllocator主要实现对Bucket的组织管理,为Block分配内存空间。
- IOEngine是具体的内存管理模块,将Block数据写入对应地址的内存空间。
- BackingMap是一个HashMap,用来存储blockKey与对应物理内存偏移量的映射关系,并且根据blockKey定位具体的Block
Block缓存写入流程如下:
-
将Block写入RAMCache。实际实现中,HBase设置了多个RAMCache,系统首先会根据blockKey进行hash,根据hash结果将Block分配到对应的RAMCache中。
-
WriteThead从RAMCache中取出所有的Block。HBase会同时启动多个WriteThead并发地执行异步写入,每个WriteThead对应一个RAMCache。
-
每个WriteThead会遍历RAMCache中所有Block,分别调用bucketAllocator为这些Block分配内存空间。
-
BucketAllocator会选择与Block大小对应的Bucket进行存放,并且返回对应的物理地址偏移量offset。
-
WriteThead将Block以及分配好的物理地址偏移量传给IOEngine模块,执行具体的内存写入操作。
-
写入成功后,将blockKey与对应物理内存偏移量的映射关系写入BackingMap中,方便后续查找时根据blockKey直接定位。
Block缓存读取流程如下:
- 首先从RAMCache中查找。对于还没有来得及写入Bucket的缓存Block,一定存储在RAMCache中。
- 如果在RAMCache中没有找到,再根据blockKey在BackingMap中找到对应的物理偏移地址量offset。
- 根据物理偏移地址offset直接从内存中查找对应的Block数据。
BucketCache工作模式
BucketCache默认有三种工作模式:heap、offheap和file,三者对应的最终存储介质有所不同。
- heap模式和offheap模式都使用内存作为最终存储介质
- heap模式,内存从JVM提供的heap区分配;有GC; 分配内存时需要将OS内存拷贝到JVM heap,更耗时;读取缓存时直接从JVM heap读取;(2.x不再支持该模式)
- offheap模式,内存从操作系统分配;无GC; 分配内存时OS直接分配内存;读取缓存时从OS拷贝到JVM heap再读取,更耗时;
- file模式,使用SSD等作为存储介质,相比昂贵的内存,可以提供更大的存储容量,因此可以极大地提升缓存命中率。