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重试操作。默认为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’,默认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的GC问题
MemStore是一块缓存,可以称为写缓存。大内存java系统总会面临GC问题,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功能默认是关闭的,需要配置参数"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,表示初始化时不申请内存。
BlockCache
BlockCache是读缓存,客户端读取某个Block,首先会检查该Block是否存在于Block Cache,如果存在就直接加载出来,如果不存在则去HFile文件中加载,加载出来之后放到Block Cache中,后续同一请求或者邻近数据查找请求可以直接从内存中获取,以避免昂贵的IO操作。
Block是HBase中最小的数据读取单元,即数据从HFile中读取都是以Block为最小单元执行的。
三种BlockCache方案
LRUBlockCache是最早的实现方案,第二种方案是SlabCache,第三种方案是BucketCache;
三种方案的不同之处主要在于内存管理模式
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。
无论工作在哪种模式下,BucketCache都会申请许多(14种)带有固定大小标签的Bucket,如果某一种Bucket空间不足,系统会从其他Bucket空间借用内存使用,因此不会出现内存使用率低的情况。
CombinedBlock-Cache:BucketCache和LRUBlockCache搭配使用。一次随机读需要先在LRUBlockCache中查到对应的Index Block,然后再到BucketCache查找对应Data Block。
相比heap模式,offheap模式因为内存是操作系统分配,降低了因为内存碎片导致Full GC的风险。
内存分配时,相比offheap直接从操作系统分配内存,heap模式需要首先从操作系统分配内存再拷贝到JVM heap,因此更耗时;
读取缓存时,heap模式可以从JVM heap中直接读取,而offheap模式则需要首先从操作系统拷贝到JVM heap再读取,因此更费时。
相比heap模式,offheap模式因为内存是操作系统分配,降低了因为内存碎片导致Full GC的风险。
内存分配时,相比offheap直接从操作系统分配内存,heap模式需要首先从操作系统分配内存再拷贝到JVM heap,因此更耗时;
读取缓存时,heap模式可以从JVM heap中直接读取,而offheap模式则需要首先从操作系统拷贝到JVM heap再读取,因此更费时。
HFile
|