客户端高阶API用法
过滤器
略
协处理器
用来实现存储过程功能的终端程序(EndPoint)和用来实现触发器功能的观察者(Observers)
协处理器家族的关系如下图所示:
观察者(Observers):
- RegionObserver:针对Region的观察者,可以监听关于Region的操作
- RegionServerObserver:针对RegionServer的观察者,可以监听整个RegionServer的操作
- MasterObserver:针对Master的观察者,可以监听Master进行的DDL操作
- WALObserver:针对WAL的观察者,可以监听WAL的所有读写操作。
- BulkLoadObserver:BulkLoad是采用MapReduce将大量数据快速地导入HBase的一种方式。BulkLoadObserver可以监听BulkLoad行为
- EndpointObserver:可以监听Endpoint的执行过程。
在此我并没有列出这些接口的所有基本实现类,只列出最常用的两种基本实现类:
- BaseRegionObserver:实现了RegionObserver接口的所有需要实现的方法,并给出了 最简单的实现
- BaseMasterObserver:实现了MasterObserver接口的所有需要实现的方法,并给出了最简单的实现
终端程序(EndPoint):
只有一个接口CoprocessorService,并且没有提供基本的实现类。该接口只有一个方法需要实现:getService,该方法需要返回ProtocolBuffers的Service实例。
性能优化
JVM调优
调大堆内存
Hbase 默认的最大RegionServer内存为1GB,而Memstore默认是40%,可以修改$HBASE_HOME/conf/hbase-env.sh
export HBASE_HEAPSIZE=8G
这个参数会影响所有HBase实例,包括Master和Region。这样的话Master和RegionServer都会占用8GB,也可以单独设置Master和RegionServer的大小。
export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS" -Xms 4g -Xmx 4g"
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS" -Xms 8g -Xmx 8g"
如何设置内存:
如果同时运行MapReduce的话,RegionServer将是除了MapReduce以外使用内存最大的服务。如果没有MapReduce的话,RegionServer可以调整到大概一半的服务器内存
防止Full GC
发生full GC的原因:
- 同步模式失败(concurrent mode failure):在CMS还没有把垃圾收集完的时候空间还没有完全释放,而这个时候如果新生代的对象过快地转化为老生代的对象时发现老生代的可用空间不够了。解决方法:设置-
XX:CMSInitiatingOccupancyFraction=N来缓解,设置的越小,JVM越早启动垃圾回收进程,一般设置为70。 - 由于碎片化造成的失败(Promotion Failure due to Fragmentation):当前要从新生代提升到老年代的对象比老年代的所
有可以使用的连续的内存空间都大
full GC会造成zookeeper认为RS假死,从而造成朱丽叶暂停。
垃圾回收策略选择:
- 如果你的RegionServer内存小于4GB,就不需要考虑G1GC策略了,直接用-XX:+UseParNewGC-XX:+UseConcMarkSweepGC。
- 如果你的RegionServer内存大于32GB,建议使用G1GC策略
Memstore的专属JVM策略MSLAB
上文所述,为防止内存碎片化,JVM有一个基于线程的解决方案,叫TLAB(Thread-Local allocation buffer)。当你使用TLAB的时候,每一个线程都会分配一个固定大小的内存空间,专门给这个线程使用,当线程用完这个空间后再新申请的空间还是这么大,这样下来就不会出现。特别小的碎片空间,基本所有的对象都可以有地方放。缺点就是无论你的线程里面有没有对象都需要占用这么大的内存,其中有很大一部分空间是闲置的,内存空间利用率会降低。
但是HBase不能直接使用这个方案,因为在HBase中多个Region是被一个线程管理的,多个Memstore占用的空间还是无法合理地分开。于是HBase就自己实现了一套以Memstore为最小单元的内存管理机制,称为MSLAB(Memstore-Local Allocation Buffers)。
MSLAB具体实现如下:
- 引入chunk的概念,所谓的chunk就是一块内存,大小默认为2MB。
- RegionServer中维护着一个全局的MemStoreChunkPool实例,从名字很容看出,是一个chunk池。
- 每个MemStore实例里面有一个MemStoreLAB实例。
- 当MemStore接收到KeyValue数据的时候先从ChunkPool中申请一个chunk,然后放到这个chunk里面。
- 如果这个chunk放满了,就新申请一个chunk。
- 如果MemStore因为刷写而释放内存,则按chunk来清空内存。
跟MSLAB相关的参数是:
- hbase.hregion.memstore.mslab.enabled:设置为true,即打开MSLAB,默认为true。
- hbase.hregion.memstore.mslab.chunksize:每个chunk的大小,默认为2048 * 1024 即2MB。
- hbase.hregion.memstore.mslab.max.allocation:能放入chunk的最大单元格大小,默认为256KB,已经很大了。
- hbase.hregion.memstore.chunkpool.maxsize:在整个memstore可以占用的堆内存中,chunkPool占用的比例。该值为一个百分比,取值范围为0.0~1.0。默认值为0.0。
- hbase.hregion.memstore.chunkpool.initialsize:该值代表了预分配的chunk占总的chunkPool的比例。该值为一个百分比,取值范围为0.0~1.0,默认值为0.0。
Region的自动拆分
Region的拆分分为自动拆分和手动拆分。自动拆分可以采用不同的策略。
ConstantSizeRegionSplitPolicy
从名字上就可以看出这个策略就是按照固定大小来拆分Region。
hbase.hregion.max.filesize: region最大大小,默认值10G
IncreasingToUpperBoundRegionSplitPolicy(默认)
这种策略从名字上就可以看出是限制不断增长的文件尺寸的策略。
默认尺寸:256MB,2048MB,6912MB,10GB
KeyPrefixRegionSplitPolicy
我们还可以自己定义拆分点。KeyPrefixRegionSplitPolicy是IncreasingToUpperBoundRegionSplitPolicy的子类,在前者的基础上
增加了对拆分点(splitPoint,拆分点就是Region被拆分处的rowkey)的定义。它保证了有相同前缀的rowkey不会被拆分到两个不同的Region里面。
该策略会根据KeyPrefixRegionSplitPolicy.prefix_length所定义的长度来截取rowkey作为分组的依据。
所以这个策略适用的场景是:
- 数据有多种前缀。
- 查询多是针对前缀,比较少跨越多个前缀来查询数据。
下图为用默认策略和用KeyPrefixRegionSplitPolicy策略的区别。
DelimitedKeyPrefixRegionSplitPolicy
该策略也是继承自IncreasingToUpperBoundRegionSplitPolicy,它也是根据你的rowkey前缀来进行切分的。唯一的不同就是:
KeyPrefixRegionSplitPolicy是根据rowkey的固定前几位字符来进行判断,而DelimitedKeyPrefixRegionSplitPolicy是根据分隔符来判断的。在有些系统中rowkey的前缀可能不一定都是定长的。
DelimitedKeyPrefixRegionSplitPolicy.delimiter:前缀分隔符
比如你定义了前缀分隔符为_,那么host1_001和host12_999的前缀就分别是host1和host12
BusyRegionSplitPolicy
上述策略没有考虑热点问题。如何判断哪个Region是热点:
先介绍以下参数:
- hbase.busy.policy.blockedRequests:请求阻塞率,即请求被阻塞的严重程度。取值范围是0.0~1.0,默认是0.2,即20%的请求被阻塞的意思。
- hbase.busy.policy.minAge:拆分最小年龄,当Region的年龄比这个小的时候不拆分,这是为了防止在判断是否要拆分的时候出现了短时间的访问频率波峰,结果没必要拆分的Region被拆分了,因为短时间的波峰会很快地降回到正常水平。单位毫秒,默认值是600000,即10分钟。
- hbase.busy.policy.aggWindow:计算是否繁忙的时间窗口,单位毫秒,默认值是300000,即5分钟。用以控制计算的频率。
计算热点Region的公式如下:
如果“当前时间–上次检测时间>=hbase.busy.policy.aggWindow”,则进行如下计算:这段时间被阻塞的请求/这段时间的总请求 = 请求的被阻塞率(aggBlockedRate)。
如果“aggBlockedRate > hbase.busy.policy.blockedRequests”,则判断该Region为繁忙。
此策略会通过拆分热点Region来缓解热点Region的压力,但是根据热点来拆分Region也会带来很多不确定性因
素,因为你也不知道下一个被拆分的Region是哪个。
DisabledRegionSplitPolicy
这种策略其实不是一种策略。如果你看这个策略的源码会发现就一个方法shouldSplit,并且永远返回false。设置成这种策略就是Region永不自动拆分。
如果你事先就知道这个Table应该按怎样的策略来拆分Region的话,你也可以事先定义拆分点(SplitPoint)。所谓拆分点就是拆分处的rowkey,比如你可以按26个字母来定义25个拆分点,这样数据一到HBase就会被分配到各自所属的Region里面。这时候我们就可以把自动拆分关掉,只用手动拆分。
手动拆分有两种情况:预拆分(pre-splitting)和强制拆分(forced splits)。
预拆分(pre-splitting)
预拆分(pre-splitting)就是在建表的时候就定义好了拆分点的算法。以下给个例子:
hbase org.apache.hadoop.hbase.util.RegionSplitter my_split_table
HexStringSplit -c 10 -f mycf
- my_split_table:我们指定要新建的表名。
- HexStringSplit:指定的拆分点算法为HexStringSplit。
- -c:要拆分的Region数量。
- -f:要建立的列族名称。
拆分算法包括:
- HexStringSplit把数据从“00000000”到“FFFFFFFF”之间的数据长度按照n等分之后算出每一段的起始rowkey和结束rowkey,以此作为拆分点
- UniformSplit有点像HexStringSplit的byte版,不管传参还是n,唯一不一样的是起始和结束不是String,而是byte[]
- 你还可以通过实现SplitAlgorithm接口实现自己的拆分算法
- 也可以手动指定拆分点:在建表的时候跟上SPLITS参数,如:
create 'test_split2', 'mycf2' SPLITS=>['aaa','bbb','ccc','ddd','eee','fff']
强制拆分(forced splits)
你还可以对运行了一段时间的Region进行强制地手动拆分(forced splits)。例:
split 'test_table1,c,1476406588669.96dd8c68396fda69', '999'
这个就是把test_table1,c,1476406588669.96dd8c68396fda69这个Region从新的拆分点999处拆成2个Region。
推荐方案
一开始可以先定义拆分点,但是当数据开始工作起来后会出现热点不均的情况,所以推荐的方法是:
- 用预拆分导入初始数据。
- 然后用自动拆分来让HBase来自动管理Region。
- 建议:不要关闭自动拆分。
Region的合并
比如你删了大量的数据,每个Region都变小了,这个时候分成这么多个Region就有点浪费了,可以把Region合并起来,然后可以减少一些RegionServer服务器来节省成本。
热合并
每次merge都要关闭整个HBase这也太麻烦了,好在后来HBase又增加了online_merge。我管通过Merge类来合并叫冷合并,
oneline_merge叫热合并。
WAL的优化
一个Region只有一个WAL实例。WAL实例启动后在内存中维护了一个ConcurrentNavigableMap。这是一个线程安全的并发集
合。这个ConcurrentNavigableMap包含了很多个WAL文件的引用。当一个文件写满了就会开始下一个文件。当WAL工作的时候WAL文件数量会不断增长直到达到一个阈值后开始滚动。
跟WAL优化的参数包括:
- hbase.regionserver.maxlogs:Region中的最大WAL文件数量,默认值是32。
- hbase.regionserver.hlog.blocksize:HDFS块大小,没有默认值,如果你不设定该值,HBase就会直接调用HDFS的API去获取。
- hbase.regionserver.logroll.multiplier:WAL文件大小因子。每一个WAL文件所占的大小通过HDFS块大小*WAL文件大小因子得出。默认值是0.95。
其实Region级别的优化对性能的提高效果并不是很大,而对于Store的优化比对Region的优化更重要。
BlockCache的优化
BlockCache名称中的Block指的是HBase的Block。之前介绍过HBase,目前有很多种Block:
- DATA
- ENCODED_DATA
- LEAF_INDEX
- BLOOM_CHUNK
- META
- INTERMEDIATE_INDEX
- ROOT_INDEX
- FILE_INFO
- GENERAL_BLOOM_META
- DELETE_FAMILY_BLOOM_META
- TRAILER
- INDEX_V1
通常有几种BlockCache的实现方案
LRUBlock Cache
读出来的block会被放到BlockCache中待下次查询使用。当缓存满了的时候,会根据LRU的算法来淘汰block。
LRUBlockCache被分为三个区域,整个策略很像JVM的新生代、年老代、永久代。如下表:
一般的Block被第一次读出后是放到single-access的,只有当被访问多次后才会放到multi-access,而带有IN-MEMORY属性的列族中的Block一开始就被放到in-memory区域。这个区域的缓存有最高的存活时间,在需要淘汰Block的时候,这个区域的Block是最后被考虑到的,所以这个属性仅仅是为了BlockCache而创造的。
设置hfile.block.cache.size的时候要注意在HBase的内存使用上有一个规则那就是Memstore + BlockCache的内存占用比例不能超过0.8(即80%),否则就要报错。因为必须要留20%作为机动空间。
这两个配置项的默认值都是0.4,也就是说默认项总和就已经达到了他们俩可以占用的内存比例上限了,所以基本没事
就不用去加大这两个配置项,你调大哪一个,都必须相应地调小另外一个。
但是LRUBlockCache有什么坏处呢?完全基于JVM Heap的缓存,势必带来一个后果:随着内存中对象越来越多,每隔一段时间都会引发一次Full GC
SlabCache
SlabCache是对堆外内存尝试的方案。SlabCache的具体实现调用了nio的DirectByteBuffers。SlabCahce把堆外内存按照80%和20%的比例划分为两个区域:
- 存放大小约等于1个BlockSize默认值的Block。
- 存放大小约等于2个BlockSize默认值的Block。
SlabCache允许的Block值定的太死,只有2种,造成利用率很低,而大部分的请求还只用到了LRUCache。所以SlabCache实际测试起来对Full GC的改善很小
Bucket Cache
相比起只有2个区域的SlabeCache,BucketCache一上来就分配了14种区域:
- 这14种区域分别放的是大小为4KB、8KB、16KB、32KB、40KB、48KB、56KB、64KB、96KB、128KB、192KB、256KB、384KB、512KB的Block。而且这个种类列表还是可以手动通过设置hbase.bucketcache.bucket.sizes属性来定义(种类之间用逗号分隔,想配几个配几个,不一定是14个!)
- BucketCache的存储不一定要使用堆外内存,是可以自由在3种存储介质直接选择:堆(heap)、堆外(offheap)、文件(file)。通过设置hbase.bucketcache.ioengine为heap、offfheap或者file来配置
- 每个Bucket的大小上限为最大尺寸的block * 4,比如可以容纳的最大的Block类型是512KB,那么每个Bucket的大小就是512KB* 4 = 2048KB。
- 系统一启动BucketCache就会把可用的存储空间按照每个Bucket的大小上限均分为多个Bucket。如果划分完的数量比你的种类还少,比如比14(默认的种类数量)少,就会直接报错,因为每一种类型的Bucket至少要有一个Bucket。
BucketCache实现起来的样子就像如下图所示:
BucketCache还有一个特别的长处,那就是它自己来划分内存空间、自己来管理内存空间,Block放进去的时候是考虑到offset偏移量的(具体可以看源码的BucketAllocator),所以内存碎片少,发生GC的时间很短。
组合模式
组合模式把不同类型的Block分别放到LRUCache和BucketCache中。
Index Block和Bloom Block会被放到LRUCache中。Data Block被直接放到BucketCache中,所以数据会去LRUCache查询一下,然后再去BucketCache中查询真正的数据。其实这种实现是一种更合理的二级缓存,数据从一级缓存到二级缓存最后到硬盘,数据是从小到大,存储介质也是由快到慢。考虑到成本和性能的组合,比较合理的介质是:LRUCache使用内存->BuckectCache使用SSD->HFile使用机械硬盘。
Memstore的优化
Memstore的优化核心就在于理解Memstore的刷写(flush),因为大部分人遇到的性能问题都是写操作被block了,无法写入Hbase了。
Memstore在以下5种情况下会触发刷写。
大小达到刷写阀值
当Memstore占用的内存大小达到hbase.hregion.memstore.flush.size的配置值的时候就会触发一次刷
写,生成一个HFile。
如果你的数据增长得太快了,在还未到达检查时间之前,数据就达到了hbase.hregion.memstore.flush.size的好
几倍,那么会触发阻塞机制,此时无法写入数据到Memstore。
hbase.hregion.memstore.flush.size(默认128MB)*hbase.hregion.memstore.block.multiplier(默认是4)
所以默认的阻塞机制阈值时512MB。如果在下一次刷写检查到来之前达到了这个阀值,会立即触发一次刷写。HBase还会在刷写的时候同时阻塞所有写入该Store的写请求。不过解决这个问题的最好方案并不是一味地调大阻塞阀值,而是要同时考
虑HFile的相关参数设置,后续章节讲此部分。
整个RegionServer的memstore总和达到阀值
hbase.regionserver.global.memstore.size.lower.limit。该配置项是一个百分比,所以取值范围在0.0~1.0,默认为0.95。
globalMemStoreSize:全局的memstore容量。对于这个值,我们已经在前面多次提到了。它的计算方式为:
hbase_heapsize(RegionServer占用的堆内存大小)* hbase.regionserver.global.memstore.size,其中
hbase.regionserver.global.memstore.size默认值为0.4。一旦达到这个阀值,就会触发一次强制的刷写。
举个例子,比如你配置的
hbase.regionserver.global.memstore.size.lower.limit是0.95,
hbase.regionserver.global.memstore.size是0.4,堆内存总共是
16G,那么触发刷写的阈值:16 * 0.4 * 0.95 = 6.08GB
触发阻塞:16 * 0.4 = 6.4GB
WAL的数量大于maxLogs
当WAL文件的数量大于maxLogs的时候,也会触发一次刷写。不过这个时候WAL会报警一下,不过不会阻塞写入。
Memstore达到刷写时间间隔
时间间隔的配置项是:hbase.regionserver.optionalcacheflushinterval:memstore刷
写间隔,默认值为3600000,即1个小时。说如果以上的所有条件都没有被触发到的话,memstore还是
会每隔一个小时刷写一次,并生成一个HFile。
手动触发flush
Admin接口也提供了方法来手动触发Memstore的刷写:
- flush(TableName tableName):对单个表进行刷写。
- flushRegion(byte[] regionName):对单个Region进行刷写。
HFile的合并
当然很多时候发生阻塞有可能并不是简单的Memstore内存不够大,而是HFile的合并(compaction)出了问题。
每次memstore的刷写都会产生一个新的HFile,而HFile毕竟是存储在硬盘上的东西,凡是读取存储在硬盘上的东西都涉及一个操作:寻址,如果是传统硬盘那就是磁头的移动寻址,这是一个很慢的动作。当HFile一多,你每次读取数据的时候寻址的动作就多了,效率就低了。所以为了防止寻址的动作过多,我们要适当地减少碎片文件,所以需要继续合并操作。
合并分为两种操作:
- Minor Compaction:将Store中多个HFile合并为一个HFile(注意不是region)。在这个过程中达到TTL的数据会被移除,但是被手动删除的数据不会被移除。这种合并触发频率较高。
- Major Compaction:合并Store中的所有HFile为一个HFile(注意不是region)。在这个过程中被手动删除的数据会被真正地移除。同时被删除的还有单元格内超过MaxVersions的版本数据。这种合并触发频率较低,默认为7天一次。不过由于Major Compaction消耗的性能较大,你不会想让它发生在业务高峰期,建议手动控制MajorCompaction的时机。
ExploringCompactionPolicy算法
ExploringCompactionPolicy算法是0.96版本之后的默认算法(Minor Compaction)这里举个例子,如下图所示,某个Store里面有这些HFile:
当前的条件参数:
- hbase.store.compaction.ratio = 1.2(默认值)。
- hbase.hstore.compaction.min = 3(默认值)。
- hbase.hstore.compaction.max = 4(默认值是5,但是为了让例子简单些,我把数量调小了)
- minCompactSize = 10
以下是手动执行算法的步骤:
- 序列号为2的HFile大小只有3,比minCompactSize=10小,所以序列号为2的HFile怎样都会被安排进组合
- 根据待比较文件< (所有文件大小总和-该文件大小)*比例因子的算法又可以把序列号为1的HFile排除掉,因为99 >
((3+12+33+10+11) * 1.2)。 - 得到待选择的HFile之后就开始穷举组合,组合的要求是个数必须大于等于hbase.hstore.compaction.min=3又小于等于
hbase.hstore.compaction.max=4,所以我们可以穷举出以下5个组
合,分别用英文字母表示::
我们来比较一下各个组合的个数,显然b和d组都有4个元素,别的组元素数量都没有他们多。淘汰了其他的组后,b组合d组打了个平手,平局的情况下就要比文件尺寸大小了,他们的文件尺寸总和分别是:
d组的文件总大小更小,所以d组最终胜出。合并后的结果如下图所示:
FIFOCompactionPolicy
FIFOCompactionPolicy策略在合并时会跳过含有未过期数据的HFile,直接删除所有单元格都过期的块。最终的效果是:
- 过期的块被整个删除掉了。
- 没过期的块完全没有操作。
DateTieredCompactionPolicy
这种策略是参考自Cassandra的。DateTieredCompactionPolicy解决的是一个基本的问题:最新的数据最
有可能被读到。
假设这样一种场景,用户发表的朋友圈动态在第一个小时之内被阅读的数量是最多的 。
由于朋友圈动态只要用户不手动删,我们是不能删的,TTL几乎是无限大,所以FIFO显然不合适。
不能单纯地看大小来合并,万一旧的文件跟新的文件合一块了,新的文件又没有合并,会极大地降低新文件的读取性能,所以
Exploring也不合适。
StripeCompactionPolicy
这个策略最早是借鉴自levelDB的compaction策略。数据从Memstore刷写到HFile上后先落在level 0,然后随着时间的推移,当level 0大小超过一定的阀值的时候就会引发一次合并。这次合并会把KeyValue从level 0读出来,然后插入到level 1的HFile中去,而level 1的块是根据键位范围(KeyRange)来划分的。如果我们根据rowkey的首字母来划
分键位范围,那么合并过程如下图所示:
如果level 1的HFile文件大过一定的阀值,就继续向下,往level2合并。如果又超过阀值就继续往level 3合并,以此类推。
把这个结构在HBase实现之后,开发者发现这个结构太复杂了,划分的块太多了,导致compaction的次数增多了,反而降低了IO利用率。
于是改进了层级设计,把level 1~N合并成一个层叫Strips层,level 0还是保留,名字改叫L0。如下图所示:
使用这种策略究竟能解决什么问题:
- 很明显,这种策略通过增加了L0层,等于是给合并操作加了一层缓冲,让合并操作更缓和。
- 严格按照键位来划分Strips,对于读取虽然不能说提高多少速度,但是可以提高查询速度的稳定性。当你执行scan的时候,跨越的HFile数量保持在了一个比较稳定的数值。
- 本来要牵涉全部HFile才能执行的Major Compaction,现在可以分Strip执行了,比如a-f这个strip就可以独立执行MajorCompaction而不牵涉g-m。执行了Major Compaction就可以真正删除掉被打上墓碑标记的数据了,否则数据一直无法被删除掉,这也是我们需要major compaction的最大原因。MajorCompaction一直以来都有牵涉的HFile文件过多造成的IO不稳定的缺点。在这个策略中因为一次只牵涉一个Strip中的文件,所以克服了IO不稳定的缺点。
那么什么场景适合用StripeCompactionPolicy:
- Region要够大:这种策略实际上就是把Region给细分成一个个Stripe。Stripe可以看做是小Region,我们可以管它叫subregion。所以如果Region不大,没必要用Stripe策略。小Region用Stripe反而增加IO负担。多大才算大?作者建议如果Region大小小于2GB,就不适合用StripeCompactionPolicy。
- Rowkey要具有统一格式,能够均匀分布。由于要划分KeyRange,所以key的分布必须得均匀,比如用26个字母打头来命名rowkey,就可以保证数据的均匀分布。如果使用timestamp来做rowkey,那么数据就没法均匀分布了,肯定就不适合使用这个策略。
compaction的吞吐量限制参数
hbase.hstore.blockingStoreFiles
当Store中的HFile数量达到这个数量的时候阻塞Memstore的刷写(flush)。默认值是7,也就是说当你的Store中的HFile的数量达到7的时候,这个Store的Memstore的刷写会被阻止
上文提到,Memstore达到阻塞阈值的时候别急着去调大Memstore的阻塞阀值,而要综合考虑HFile的阻
塞值,就是这个原因。
老实说这个默认值设定得有点小,大家可以适当地调大这个数值,比如调到20、30、50 都不算多。HFile多,只是读取性能下降而已,但是达到阻塞值可就不只是慢的问题了,是直接写不进去了。
合并/刷写吞吐量限制机制
HBase会计算合并/刷写时占用的吞吐量,然后当占用吞吐量过大的时候适当地休眠。之所以写合并/刷写是因为这两个参数既会限制合并时占用的吞吐量,也会限制刷写时占用的吞吐量。限制是区分高峰时段和非高峰时段的。在非高峰期是不限速的,只有在高峰期当合并/刷写占用了太大的吞吐量才会休眠。决定是否要休眠是看当时占用的流量是否达到休眠吞吐量阀值。
压力比
压力比(pressureRatio)越大,代表HFile堆积得越多,或者即将产生越多的HFile。一旦HFile达到阻塞阈值,则无法写入任何数据,系统就不可用了。
压力比,分为合并压力(compactionPressure)和刷写压力(flushPressure)两种。
合并压力:
compactionPressure = (storefileCount - minFilesToCompact) / (blockingFileCount
- minFilesToCompact)
- storefileCount:当前StoreFile数量。
- minFilesToCompact:单次合并文件数量下限,即hbase.hstore.compaction.min。
- blockingFileCount:hbase.hstore.blockingStoreFiles
因此通过这个公式我们可以看出,当前的StoreFile越大,或者阻塞上限越小,那么合并的压力就越大,因为更有可能发生阻塞
刷写压力:
flushPressure = globalMemstoreSize / memstoreLowerLimitSize
- globalMemstoreSize:当前的Memstore大小。
- memstoreLowerLimitSize:Memstore刷写的下限,当全局memstore达到这个内存占用数量的时候就会开始刷写。
Memstore占用的内存越大,或者刷写的触发条件越小,越有可能引发刷写。发生刷写后,HFile的数量就会增
多,即越有可能因为HFile过多触发阻塞。
合并步骤
- 获取需要合并的HFile列表:获取列表的时候需要排除掉带锁的HFile。锁分两种:写锁(write
lock)和读锁(read lock)。当HFile正在进行以下操作的时候会上
锁:用户正在scan查询:上Region读锁(region read lock)。
Region正在切分(split):此时Region会先关闭,然后上
Region写锁(region write lock)。
Region关闭:上Region写锁(region write lock)。
Region批量导入:上Region写锁(region write lock)。 - 由列表创建出StoreFileScanner:HRegion会创建出一个Scanner,用这个Scanner来读取本次要合并
的所有StoreFile上的数据。 - 把数据从这些HFile中读出,并放到tmp目录(临时文件夹)
- 用合并后的HFile来替换合并前的那些HFile
Major Compaction
Minor Compaction的目的是增加读性能,而majorCompaction在minorCompaction的目的之上还增加了1点:真正地从磁盘上把用户删除的数据(带墓碑标记的数据)删除掉。
为什么达到TTL的数据可以被Minor Compaction删除:这是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来
记录。只需当前时间-cell的时间戳 > TTL
判断是否达到了足够的时间间隔则需要根据以下两个配置项综合考虑:
- hbase.hregion.majorcompaction:majorCompaction发生的周期,单位是毫秒,默认值是7天。
- hbase.hregion.majorcompaction.jitter majorCompaction:周期抖动参数,0~1.0的一个指数。调整这个参数可以让MajorCompaction的发生时间更灵活,默认值是0.5。
由于Major Compaction会占用大量的磁盘和网络IO,会极大地影响集群的性能。如果你 发现Major Compaction总是发生在高峰期,建议关闭自动Major Compaction。关闭的方式就是把Hbase.hregion.majorcompaction设置为0,然后自己定义一些定时任务
来让HBase在非业务高峰期来手动调用Major Compaction。完全不进行majorCompaction对集群非常不利。
总结
- 如果你的数据有固定的TTL,并且越新的数据越容易被读到,那么DateTieredCompaction一般是比较适合你的。
- 如果你的数据没有TTL或者TTL较大,那么选择StripeCompaction会比默认的策略更稳定。
- FIFOCompaction一般不会用到,这只是一种极端情况,比如用于生存时间特别短的数据。如果你想用FIFOCompaction,可以考虑使用DateTieredCompaction。
诊断手册
写入阻塞
- RegionServer内存设置得太小:由于默认值是1GB,那么Memstore默认是占40%,才只有400MB,那很容易阻塞的
- HFile达到允许的最大数量:单个Store中的HFile达到hbase.hstore.blockingStoreFiles最大数量的时候,Memstore就不能刷写数据到HDFS了,这是第一层的阻塞。它会引发下一层的阻塞,也就是Memstore的阻塞。
- Memstore大小达到阈值:当单个Memstore的大小达到hbase.hregion.memstore.flush.size*hbase.hregion.memstore.block.multiplier的时候,写请求就无法写入Memstore
- RegionServer上的Memstore总大小达到阈值:可以略微地调大这个百分比,但是由于整个
RegionServer中的Memstore + BlockCache不能大于80%,而默认的RegionServer大小是0.4,默认的BlockCache是0.4,加起来就0.8了,调大这个参数,BlockCache占的内存(hfile.block.cache.size)就必须调小,否则启动都启动不起来。
朱丽叶暂停
- 调大RS内存
- 调大ZK超时时间
- 优化GC策略
- 启用MSLAB:调整hbase.hregion.memstore.chunkpool.maxsize值
读取性能调优
- 使用过滤器:灵活地运用比如前缀过滤器(PrefiexFilter)、分页过滤器(PageFilter)等可以减少不必要遍历次数的过滤器
- 增加BlockCache:理论上增加BlockCache可以增加读性能,但是增加了是否有效,还需要看你实际的Cache命中率,如果命中率低的话,可以考虑增加命中率
- 调整HFile合并策略:调整你的HFile合并(Compaction)策略,让HFile的数量尽量减小,以减少每次Scan的跨HFile的次数。但同时又要保证该合并策略适用于你的场景,并且不要太频繁