本篇主要介绍:
① Region的拆分和合并,及相关经验总结。
② Region自动均衡
③ HFile的合并(compaction)
④ BlockCache 和 BloomFilter
一、Region拆分与合并
一个Region就是一个表 的一段Rowkey的数据集合。当Region太大的时候HBase会拆分它。这么做的原因是:当某个Region太大的时候读取效率太低了。Region的拆分分为自动拆分和手动拆分。自动拆分可以采用不同的策略。
1.1 Region自动拆分
Hbase的Region自动拆分有几种策略。这里简单介绍一下概念,拆分策略的具体内容读者可以下去自行深入研究。
- ConstantSizeRegionSplitPolicy
早在0.94版本的时候HBase只有一种拆分策略,这个策 略就是按照固定大小来拆分Region。它唯一用到的参数是:
hbase.hregion.max.filesize: region最大大小,默认10GB
当单个Region大小超过了10GB,就会被HBase拆分成为2个Region。
- IncreasingToUpperBoundRegionSplitPolicy(默认)
0.94版本之后,有了IncreasingToUpperBoundRegionSplitPolicy 策略。这种策略从名字上就可以看出是限制不断增长的文件尺寸的策略用来防止Region过大。
- KeyPrefixRegionSplitPolicy
除了简单粗暴地根据大小来拆分,我们还可以自己定义拆分点。
KeyPrefixRegionSplitPolicy 是 IncreasingToUpperBoundRegionSplitPolicy的子类,在前者的基础上 增加了对拆分点(splitPoint,拆分点就是Region被拆分处的rowkey) 的定义。它保证了有相同前缀的rowkey不会被拆分到两个不同的Region 里面。
- DelimitedKeyPrefixRegionSplitPolicy
该策略也是继承自IncreasingToUpperBoundRegionSplitPolicy, 它也是根据你的rowkey前缀来进行切分的。唯一的不同就是: KeyPrefixRegionSplitPolicy是根据rowkey的固定前几位字符来进行判 断,而DelimitedKeyPrefixRegionSplitPolicy是根据分隔符来判断的。比如你定义了前缀分隔符为_,那么host1_001和host12_999的前缀 就分别是host1和host12。
- BusyRegionSplitPolicy
此前的拆分策略都没有考虑热点问题。所谓热点问题就是数据库中 的Region被访问的频率并不一样,某些Region在短时间内被访问的很频 繁,承载了很大的压力,这些Region就是热点Region。如果你的系统常常会出现热点Region,而你对性能有很高的追求, 那么这种策略可能会比较适合你。它会通过拆分热点Region来缓解热点 Region的压力,但是根据热点来拆分Region也会带来很多不确定性因 素,因为你也不知道下一个被拆分的Region是哪个。
- DisabledRegionSplitPolicy
这种策略其实不是一种策略。如果你看这个策略的源码会发现就一 个方法shouldSplit,并且永远返回false。设置成这种策略就是Region永不自动拆分。 如果使用DisabledRegionSplitPolicy让Region永不自动拆分之 后,你依然可以通过手动拆分来拆分Region。
1.2 Region手动拆分
无论你设置了哪种拆分策略,一开始数据进入Hbase的时候都只会 往一个Region塞数据。必须要等到一个Region的大小膨胀到某个阀值的 时候才会根据拆分策略来进行拆分。但是当大量的数据涌入的时候,可 能会出现一边拆分一边写入大量数据的情况,由于拆分要占用大量IO, 有可能对数据库造成一定的压力。如果你事先就知道这个Table应该按 怎样的策略来拆分Region的话,你也可以事先定义拆分点 (SplitPoint)。所谓拆分点就是拆分处的rowkey,比如你可以按26个 字母来定义25个拆分点,这样数据一到HBase就会被分配到各自所属的 Region里面。这时候我们就可以把自动拆分关掉,只用手动拆分。 手动拆分有两种情况:预拆分(pre-splitting)和强制拆分 (forced splits)。
1.2.1 Region预拆分
预拆分(pre-splitting)就是在建表的时候就定义好了拆分点的算法,所以叫预拆分。
默认的拆分点算法有两个:UniformSplit和HexStringSplit
假设要拆分的Region的数量是n,HexStringSplit把数据从“00000000”到“FFFFFFFF”之间的数据 长度按照n等分之后算出每一段的起始rowkey和结束rowkey,以此作为 拆分点。
UniformSplit有点像HexStringSplit的byte版,不管传参还是n, 唯一不一样的是起始和结束不是String,而是byte[]。
举个例子:
hbase org.apache.hadoop.hbase.util.RegionSplitter my_split_table HexStringSplit -c 10 -f mycf
执行如上命令的结果:新建了一张表,并且规定了该表的Region数量永远只有10个。
-c:要拆分的Region数量。
-f:要建立的列族名称。
还可以手动指定拆分点,在建表的时候加上SPLITS参数
create 'test_split2', 'mycf2', SPLITS=['aaa', 'bbb', 'ccc']
1.2.2 Region的强制拆分
除了预拆分和自动拆分以外,你还可以对运行了一段时间的Region 进行强制地手动拆分(forced splits)。方法是调用hbase shell的 split方法,split的调用方式如下:
split 'regionName' # format: 'tableName,startKey,id'
比如:
split 'test_table1,c,1476406588669.96dd8c68396fda69'
这个就是把test_table1,c,1476406588669.96dd8c68396fda69这个 Region从新的拆分点999处拆成2个Region。
1.3 Region拆分推荐的方案
一开始可以先定义拆分点,但是当数据开始工作起来后会出现热点 不均的情况,所以推荐的方法是:
(1)用预拆分导入初始数据。
(2)然后用自动拆分来让HBase来自动管理Region。
建议:不要关闭自动拆分
1.4 Region的合并
Region可以被拆分,也可以被合并。不过Region的合并(merge) 并不是为了性能考虑的,而更多地是出于维护的目的被创造出来的。
什么时候会用到Merger呢?比如你删了大量的数据,每个Region都变小了,这个时候分成这么 多个Region就有点浪费了,可以把Region合并起来,然后可以减少一些 RegionServer服务器来节省成本。
1.4.1 通过Merger类合并Region
合并通过使用org.apache.hadoop.hbase.util.Merge类来实现。
举个例子,比如我想把以下两个Region合并:
test_table1,b,1476406588669.39eecae03539ba0a63264c24130c2cb1. test_table1,c,1476406588669.96dd8c68396fda694ab9b0423a60a4d9.
就需要在Linux下(不需要进入hbase shell)执行以下命令:
hbase org.apache.hadoop.hbase.util.Merge test_table1
test_table1,b,1476406588669.39eecae03539ba0a63264c24130c2cb1. test_table1,c,1476406588669.96dd8c68396fda694ab9b0423a60a4d9.
但是这种方法在执行命令前得先把集群给下线了才行,所以我们需要先把HMaster和所有的 HRegionServer全部都停掉,不过每次merge都要关闭整个HBase太麻烦了,所以后来HBase 又增加了online_merge(热合并)。
1.4.2 热合并
hbase shell提供了一个命令叫online_merge,通过这个方法可以 进行热合并(online_merge)。举个例子:
假设要合并以下两个Region: test_table1,a,1476406588669.d1f84781ec2b93224528cbb79107ce12. test_table1,b,1476408648520.d129fb5306f604b850ee4dc7aa2eed36. online_merge的传参是Region的hash值,Region的hash值就是 Region名最后那段在两个.号之间的字符串。
需要在hbase shell 中执行以下命令:
merge_region
'd1f84781ec2b93224528cbb79107ce12', 'd129fb5306f604b850ee4dc7aa2eed36'
二、 Region的自动平衡
HBase 的 Region 分配和自动均衡是由 Master 节点控制的,在初始化表时会先分配一个Region,然后指定给某个Region Server。 如果使用预分区,那么Master 会按照轮询的方式平均分配到每个 Region Server。此后,随着Region不断的增大和裂变,Region Server 上的 Region 数量开始变得不均衡。如果开启了自动均衡开关,Master 会通过定时器来检查集群中的Regions在各个RegionServer之间的负载是否是均衡。一旦检测到不均衡的情况,就会生成相应的Region迁移计划。
关于均衡的方式,HBase 提供以下两种策略:
- DefaultLoadBalancer 默认的策略,根据 Region 个数来进行均衡
- StochasticLoadBalancer 根据读写压力评估来进行均衡
由于HBase的的数据(包括HLog、StoreFile等)都是写入到HDFS文件系统中的, 因此 HBase 的 Region 迁移是非常轻量级的。在做Region迁移时,Region所对应的HDFS文件是不变的,此时只需要将 Region 的元数据重新分配到目标 Region Server 就可以了。迁移过程的步骤包含:
- 创建Region 迁移计划,指定 RegionID、源 Region Server 和目标 Region Server;
- 源 Region Server 解绑,此时会关闭 Region;
- 目标 Region Server 绑定,重新打开 Region;
三、 HFile的合并
上一篇文章中,我们从RegionServer一直介绍到了HFile的格式,稍微复习一下:
在结尾的数据块中包含了数据相关的索引信息,系统也是通过结尾的索引信息找到 HFile 中的数据。HFile 中的数据块大小默认为 64KB。如果访问 HBase 数据库的场景多为有序的访问,那么建议将该值设置的大一些。如果场景多为随机访问,那么建议将该值设置的小一些。一般情况下,通过调整该值可以提高 HBase 的性能。
好,那我们接下来看HFile的compaction(整理合并)。HFile(StoreFile是HFile的Java抽象对象)是会经常被合并和拆分的。为什么要合并?每次memstore的刷写都会产生一 个新的HFile,而HFile毕竟是存储在硬盘上的东西,凡是读取存储在硬 盘上的东西都涉及一个操作:寻址,如果是传统硬盘那就是磁头的移动 寻址,这是一个很慢的动作。当HFile一多,你每次读取数据的时候寻 址的动作就多了,效率就低了。所以为了防止寻址的动作过多,我们要 适当地减少碎片文件,所以需要合并操作。HFile合并操作就是在一个Store里面找到需要合并的HFile,然后把他们合并起来,最后把之前的碎文件移除。那么问题就来了:哪些文 件需要被合并?
3.1 合并的策略
HFile的合并有很多种策略,不同策略之间的区别主要就是对于要
合并的文件集合的定义方法。如果你去搜索hbase compaction,首先会看到的就是两个概念:Minor Compaction和M ajor Compaction:
Minor Compaction:将Store中多个HFile合并为一个HFile。在这个过程中达到TTL的数据会被移除,但是被手动删除的数据不会被移除。这种合并触发频率较高。
Major Compaction:合并Store中的所有HFile为一个HFile。在 这个过程中被手动删除的数据会被真正地移除。同时被删除的还有单元格内超过MaxVersions的版本数据。这种合并触发频率较 低,默认为7天一次。不过由于Major Compaction消耗的性能较 大,你不会想让它发生在业务高峰期,建议手动控制Major Compaction的时机。
Major Compaction是把一个Store中的HFile合并为一个HFile。很多 网上的资料说Major Compaction是把一个Region中的所有HFile合并 成一个文件,这是错的。
在0.96版本之前的Minor Compaction策略是 RatioBasedCompactionPolicy。0.96版本之后,出现了多种Minor Compaction策略。
①RatioBasedCompactionPolicy
从旧到 新地扫描HFile文件,当扫描到某个文件,该文件满足以下条件:
该文件的大小< 比它更新的所有文件的大小总和 * hbase.store.compaction.ratio
满足条件以后把该HFile和比它更新的所有HFile合并成一个 HFile。
② ExploringCompactionPolicy
0.96版本之后提出了ExploringCompactionPolicy算法,并且把该算法作为了默认算法。这个算法主要做了如下改动:
不再武断地认为,某个文件满足条件就把更新的文件全部合并进去。确切地说,现在的遍历不强调顺序性了,是把所有的文件都遍历一 遍之后每一个文件都去考虑。符合条件而进入待合并列表的文件由新的条件判断:
该文件< (所有文件大小总和 - 该文件大小) * 比例因子
③ FIFOCompactionPolicy
④ StripeCompactionPolicy
这些策略的详细过程可以参阅《HBase不睡觉书》以及HBase源码。读者可以自己进行拓展。
3.2 合并时的步骤
合并经历了以下几个具体步骤:
(1)获取需要合并的HFile列表。
(2)由列表创建出StoreFileScanner。
HRegion会创建出一个Scanner,用这个Scanner来读取本次要合并 的所有StoreFile上的数据。
(3)把数据从这些HFile中读出,并放到tmp目录(临时文件 夹)。
HBase会在临时目录中创建新的HFile,并使用之前建立的Scanner 从旧HFile上读取数据,放入新HFile。以下两种数据不会被读取出来:
- 如果数据过期了(达到TTL所规定的时间),那么这些数据不会 被读取出来。
- 如果是majorCompaction,那么数据带了墓碑标记也不会被读取 出来。
(4)用合并后的HFile来替换合并前的那些HFile。
最后用临时文件夹内合并后的新HFile来替换掉之前的那些HFile文 件。过期的数据由于没有被读取出来,所以就永远地消失了。如果本次 合并是Major Compaction,那么带有墓碑标记的文件也因为没有被读取 出来,就真正地被删除掉了。
3.3 Major Compaction
Major Compaction的目的:
Minor Compaction的目的是增加读性能,而major Compaction在 minorCompaction的目的之上还增加了一点:真正地从磁盘上把用户删除的数据(带墓碑标记的数据)删除掉。为什么只有major Compaction可以真正删除数据?
其实HBase一直拖到major Compaction的时候才真正把带墓碑标记的 数据删掉,并不是因为性能要求,而是之前真的做不到。之前提到过 HBase是建立在HDFS这种只有增加删除而没有修改的文件系统之上的, 所以就连用户删除这个动作,在底层都是由新增实现的:用户增加一条数据就在HFile上增加一条KeyValue,类型是PUT。
用户删除一条数据还是在HFile上增加一条KeyValue,类型是 DELETE,这就是墓碑标记。
现在会遇到一个问题:当用户删除数据的时候之前的数据已经被刷 写到磁盘上的另外一个HFile了。这种情况很常见,也就是说,墓碑标记和原始数据这两个KeyValue压根就不在同一个HFile上,如下图所示。
在查询的时候Scan指针其实是把所有的HFile都看过了一遍,它知道了有这条数据,也知道它有墓碑标记,而在返回数据的时候选择不把
数据返回给用户,这样在用户的Scan操作看来这条数据就是被删掉了。 如果你可以带上RAW=>true参数来Scan,你就可以查询到这条被打上墓碑标记的数据。
- 为什么达到TTL的数据可以被Minor Compaction删除?
这是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来 记录。合并时创建的Scanner在查询数据的时候,根据以下公式来判断cell 是否过期:
当前时间now - cell的时间戳 > TTL
如果过期了就不返回这条数据。这样当合并完成后,过期的数据因为没有被写入新文件,自然就消失了。
- Major Compaction是怎么产生的?
它其实就是Minor Compaction升级而来的。如果本次Minor Compaction包含所有文 件,并且达到了足够的时间间隔,则会被升级为Major Compaction。判 断是否包含所有文件比较简单,判断是否达到了足够的时间间隔则需要 根据以下两个配置项综合考虑:
- hbase.hregion.majorcompaction:major Compaction发生的周期,单位是毫秒,默认值是7天。
- hbase.hregion.majorcompaction.jitter majorCompaction:周期抖动参数,0~1.0的一个指数。调整这个参数可以让Major Compaction的发生时间更灵活,默认值是0.5。
提示:
虽然有以上机制控制Major Compaction的发生时机,但是由于Major Compaction时对系统的压力还是很大的,所以建议关闭自动Major Compaction,采用手动触发的方式,定期进行Major Compaction。
四、BlockCache 和 布隆过滤器
4.1 BlockCache
一个RegionServer只有一个 BlockCache。之前画的架构图上是没有标出BlockCache的,这是因为之 前的图上出现的都是数据存储必需的组成部分,而BlockCache不是数据 存储的必须组成部分,他只是用来优化读取性能的。如果加上 BlockCache,之前的架构就变成如下图所示:
BlockCache名称中的Block指的是HBase的Block。BlockCache的工作原理跟其他缓存一样:
读请求到HBase之 后先尝试查询BlockCache,如果获取不到就去HFile(StoreFile)和 Memstore中去获取。如果获取到了则在返回数据的同时把Block块缓存 到BlockCache中。BlockCache默认是开启的。
接下来看看有哪几种BlockCache的实现方案:
①LRUBlock Cache
近期最少使用算法。读出来的block会被放到BlockCache中待 下次查询使用。当缓存满了的时候,会根据LRU的算法来淘汰block。
②SlabCache
这是一种堆外内存的解决方案。
堆外内存(off-heap memory)是 不属于JVM管理的内存范围,说白了,就是原始的内存区域了。堆外内 存的大小可以通过-XX:MaxDirectMe morySize=60MB这样来设置。
使用堆外内存最大的好处就是:回收堆外内存的时候JVM几乎不会停顿,这样再也不用怕回收的时候业务系统卡住了。既然堆外内存回收的时候不会卡,为什么大家不都去用它呀?这是因为堆外 内存的缺点几乎比它带来的好处还大:
- 因为在堆外内存存储的数据都是很原始的数据,如果是一个对 象,比如先序列化之后才能存储,所以不能存储太复杂的对象。
- 堆外内存并不是在JVM的管理范围,所以当内存泄露的时候很不 好排查问题。
- 堆外内存由于用的是系统内存,当你用的太大的时候,物理内存有可能爆掉,或者直接开启了虚拟内存,也就是直接影响到了硬盘的使用。
再来说下SlabCache的具体实现。SlabCache调用了nio的 DirectByteBuffers。SlabCahce把堆外内存按照80%和20%的比例划分为 两个区域:
(1)存放大小约等于1个BlockSize默认值的Block。
(2)存放大小约等于2个BlockSize默认值的Block。
③Bucket Cache
BucketCache借鉴了SlabCache的创意,也用上了堆外内存。不过它有以下自己的特点:
相比起只有2个区域的SlabeCache,BucketCache一上来就分配了 14种区域。这 14种区域分别放的是大小为4KB、8KB、16KB、32KB、40KB、 48KB、56KB、64KB、96KB、128KB、192KB、256KB、384KB、 512KB的Block。而且这个种类列表还是可以手动通过设置 hbase.bucketcache.bucket.sizes属性来定义
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。
④组合模式
具体地说就是把不同类型的Block分别放到 LRUCache和BucketCache中。
Index Block和Bloom Block会被放到LRUCache中。Data Block被直 接放到BucketCache中,所以数据会去LRUCache查询一下,然后再去 BucketCache中查询真正的数据。其实这种实现是一种更合理的二级缓 存,数据从一级缓存到二级缓存最后到硬盘,数据是从小到大,存储介 质也是由快到慢。考虑到成本和性能的组合,比较合理的介质是: LRUCache使用内存->BuckectCache使用SSD->HFile使用机械硬盘。
4.2 BloomFilter
布隆过滤器是hbase中的高级功能,它能够减少特定访问模式(get/scan)下的查询时间。不过由于这种模式增加了内存和存储的负担,所以被默认为关闭状态。
hbase支持如下类型的布隆过滤器:
1、NONE 不使用布隆过滤器
2、ROW 行键使用布隆过滤器
3、ROWCOL 列键使用布隆过滤器
其中ROWCOL是粒度更细的模式。
- 为什么要引入布隆过滤器呢?
如果用户随机查找一个行键,则这个行键很可能位于两个开始键(即索引)之间的位置。对于hbase来说,它判断这个行键是否真实存在的唯一方法就是加载这个数据块,并且扫描它是否包含这个键。
当我们随机读get数据时,如果采用hbase的块索引机制,hbase会加载很多块文件。
如果采用布隆过滤器后,它能够准确判断该HFile的所有数据块中,是否含有我们查询的数据,从而大大减少不必要的块加载,从而增加hbase集群的吞吐率。
1、布隆过滤器的存储在哪?
对于hbase而言,当我们选择采用布隆过滤器之后,HBase会在生成StoreFile(HFile)时包含一份布隆过滤器结构的数据,称其为MetaBlock;MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护。所以,开启bloomfilter会有一定的存储及内存cache开销。但是在大多数情况下,这些负担相对于布隆过滤器带来的好处是可以接受的。
2、采用布隆过滤器后,hbase如何get数据?
在读取数据时,hbase会首先在布隆过滤器中查询,根据布隆过滤器的结果,再在MemStore中查询,最后再在对应的HFile中查询。
3、采用ROW还是ROWCOL布隆过滤器?
这取决于用户的使用模式。如果用户只做行扫描,使用更加细粒度的行加列布隆过滤器不会有任何的帮助,这种场景就应该使用行级布隆过滤器。当用户不能批量更新特定的一行,并且最后的使用存储文件都含有改行的一部分时,行加列级的布隆过滤器更加有用。
参考
《HBase不睡觉书》
https://blog.csdn.net/qq_38180223/article/details/80922114