HBase: 表结构设计优化
Posted 天戈朱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HBase: 表结构设计优化相关的知识,希望对你有一定的参考价值。
在 HBase(六): HBase体系结构剖析(上) 介绍过,Hbase创建表时,只需指定表名和至少一个列族,基于HBase表结构的设计优化主要是基于列族级别的属性配置,如下图:
目录:
- BLOOMFILTER
- BLOCKSIZE
- IN_MEMORY
- COMPRESSION/ENCODING
- VERSIONS
- TTL
BLOOMFILTER:
- Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合
- bloom filter的数据存在StoreFile的meta中,一旦写入无法更新,因为StoreFile是不可变的。Bloomfilter是一个列族(cf)级别的配置属性,如果在表中设置了Bloomfilter,那么HBase会在生成StoreFile时包含一份bloomfilter结构的数据,称其为MetaBlock;MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护。所以,开启bloomfilter会有一定的存储及内存cache开销
- 对于已经存在的表,可以使用alter表的方式修改表结构,但这种修改对于之前的数据不会生效,只针对修改后插入的数据
- 包含三种类型:NONE、ROW、ROWCOL
- ROW: 根据KeyValue中的row来过滤storefile,举例如下:假设有2个storefile文件sf1和sf2
- sf1包含kv1(r1 cf:q1 v)、kv2(r2 cf:q1 v)
- sf2包含kv3(r3 cf:q1 v)、kv4(r4 cf:q1 v)
- 如果设置了CF属性中的bloomfilter为ROW,那么get(r1)时就会过滤sf2,get(r3)就会过滤sf1
- ROWCOL:根据KeyValue中的row+qualifier来过滤storefile
- 如上例:若设置了CF属性中的bloomfilter为ROW,无论get(r1,q1)还是get(r1,q2),都会读取sf1+sf2;
- 而如果设置了CF属性中的bloomfilter为ROWCOL,那么get(r1,q1)就会过滤sf2,get(r1,q2)就会过滤sf1
- ROW: 根据KeyValue中的row来过滤storefile,举例如下:假设有2个storefile文件sf1和sf2
- region下的storefile数目越多,bloomfilter的效果越好
- region下的storefile数目越少,HBase读性能越好
BLOCKSIZE:
- 从上图可发现,默认的BlockSize 为 65536B (64KB),在 HBase(七): HBase体系结构剖析(下) 介绍HBase读原理,如果在blaock cache 、memostore中都没查到符合条件的数据,则循环遍历 storeFile 文件,而hbase读取磁盘文件是按其基本I/O单元(即 hbase block)读数据的,因此HFile块大小是影响性能的重要参数
- 参见Get\\Scan场景下测试不同BlockSize大小(16K,64K,128K)对性能的影响,如下图:对比结果参考:http://hbasefly.com/2016/07/02/hbase-pracise-cfsetting/
- 可见,如果业务请求以Get请求为主,可以考虑将块大小设置较小;如果以Scan请求为主,可以将块大小调大;默认的64K块大小是在Scan和Get之间取得的一个平衡
- 平均键值对规划,如下
[root@HDP0 bin]# hbase hfile -m -f /apps/hbase/data/data/default/PerTest/7685e6c39d1394d94e26cf5ddafb7f9f/d/3ef195ca65044eca93cfa147414b56c2 SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/usr/hdp/2.4.2.0-258/hadoop/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/usr/hdp/2.4.2.0-258/zookeeper/lib/slf4j-log4j12-1.6.1.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory] 2016-09-11 12:54:40,514 INFO [main] hfile.CacheConfig: CacheConfig:disabled Block index size as per heapsize: 6520 reader=/apps/hbase/data/data/default/PerTest/7685e6c39d1394d94e26cf5ddafb7f9f/d/3ef195ca65044eca93cfa147414b56c2, compression=none, cacheConf=CacheConfig:disabled, firstKey=00123ed7-5af8-49b1-bd13-9e086a5bd5f2/d:Action/1471406616120/Put, lastKey=fffbc8f7-55f2-4c49-804f-444f6ccbc903/d:UserID/1471406614464/Put, avgKeyLen=55, avgValueLen=10, entries=54180, length=4070738
-
从上面输出的信息可以看出,该HFile的平均键值对规模为55B + 10B = 65B,相对较小,在这种情况下可以适当将块大小调小(例如8KB或16KB)。这样可以使得一个block内不会有太多kv,kv太多会增大块内寻址的延迟时间,因为HBase在读数据时,一个block内部的查找是顺序查找
- 选择较小块的大小的目的是使随机读取更快,而付出的代价是块索引变大,会消耗更多的内存。相反,如果平均键值对规模很大,或者磁盘速度慢造成了性能瓶颈,那就应该选择一个较大的块大小,以便使一次磁盘IO能够读取更多的数据
- 思考:实际场景大部分是Scan读,但平均键值规划较小,如何设置BlockSize?
IN_MEMORY:
- Block Cache 包含三个级别的优先级队列:
- Single: 如果一个Block被第一次访问,则放在这一级的队列中
- Multi: 如果一个Block被多次访问,则从Single队列移动Multi队列
- In Memory: 它是静态指定的(在column family上设置),不会像其他两种cache会因访问频率而发生改变,这就决定了它的独立性,另外两种block访问次数再多也不会被放到in-memory的区段里去,in-memory的block不管是第几次访问,总是被放置到in-memory的区段中
- LRU(Least Recently Used)淘汰数据时,Single会被优先淘汰,其次是Multi, 最后是In Memory, 这三个队列分别占用 BlockCache的 25%、50%、25%
- 每load一个block到cache时,都会检查当前cache的size是否已经超过了“警戒线”,这个“警戒线”是一个规定的当前block cache总体积占额定体积的安全比例,默认该值是0.85,即当加载了一个block到cache后总大小超过了既定的85%就开始触发异步的evict操作了
- evict的逻辑是这样的:遍历cache中的所有block,根据它们所属的级别(single,multi,in-memory)分拨到三个优先级队列中,队头元素是最旧(最近访问日间值最小)的那个block。对这个三队列依次驱逐头元素,释放空间
- 注意: 标记 IN_MEMORY=>\'true\' 的column family的总体积最好不要超过in-memory cache的大小(in-memory cache = heap size * hfile.block.cache.size * 0.85 * 0.25),特别是当总体积远远大于了in-memory cache时,会在in-memory cache上发生严重的颠簸
- 换个角度再看,普遍提到的使用in-memory cache的场景是把元数据表的column family声明为IN_MEMORY=>\'true。实际上这里的潜台词是:元数据表都很小。其时我们也可以大胆地把一些需要经常访问的,总体积不会超过in-memory cache的column family都设为IN_MEMORY=>\'true\'从而更加充分地利用cache空间。普通的block永远是不会被放入in-memory cache的,只存放少量metadata是对in-memory cache资源的浪费
- 操作命令如下(建表时或alter已创建的表):
hbase(main):002:0> create \'Test\',{NAME=>\'d\',IN_MEMORY=>\'true\'} 0 row(s) in 4.4970 seconds => Hbase::Table - Test
hbase(main):003:0> describe \'Test\' Table Test is ENABLED Test COLUMN FAMILIES DESCRIPTION {NAME => \'d\', BLOOMFILTER => \'ROW\', VERSIONS => \'1\', IN_MEMORY => \'true\', KEEP_DELETED_CELLS => \'FALSE\', DATA_BLOCK_ENCODING => \'NONE\', TTL => \'FOREVER\', COMPRESSION => \'NONE\', MIN_VERSIONS => \'0\', BLOCKCACHE => \'true\', BLOCKSIZE => \'65536\', REPLICATION_SCOPE => \'0\'} 1 row(s) in 0.2530 seconds hbase(main):004:0> create \'Test1\',\'d\' 0 row(s) in 2.2400 seconds => Hbase::Table - Test1 hbase(main):005:0> disable \'Test1\' 0 row(s) in 2.2730 seconds hbase(main):006:0> alter \'Test1\',{NAME=>\'d\',IN_MEMORY=>\'true\'} Updating all regions with the new schema... 1/1 regions updated. Done. 0 row(s) in 2.4610 seconds hbase(main):007:0> enable \'Test1\' 0 row(s) in 1.3370 seconds hbase(main):008:0> describe \'Test1\' Table Test1 is ENABLED Test1 COLUMN FAMILIES DESCRIPTION {NAME => \'d\', BLOOMFILTER => \'ROW\', VERSIONS => \'1\', IN_MEMORY => \'true\', KEEP_DELETED_CELLS => \'FALSE\', DATA_BLOCK_ENCODING => \'NONE\', TTL => \'FOREVER\', COMPRESSION => \'NONE\', MIN_VERSIONS => \'0\', BLOCKCACHE => \'true\', BLOCKSIZE => \'65536\', REPLICATION_SCOPE => \'0\'} 1 row(s) in 0.0330 seconds hbase(main):009:0>
COMPRESSION/ENCODING:
- Compression就是在用CPU资源换取磁盘空间资源,对读写性能并不会有太大影响,HBase目前提供了三种常用的压缩方式:GZip | LZO | Snappy
- HBase在写入数据块到HDFS之前会首先对数据块进行压缩,再落盘,从而可以减少磁盘空间使用量
- 读数据的时候首先从HDFS中加载出block块之后进行解压缩,然后再缓存到BlockCache,最后返回给用户。写路径和读路径分别如下
- 结合上图,来看看数据压缩对资源使用情况以及读写性能的影响:
- 资源使用情况:压缩最直接、最重要的作用即是减少数据硬盘容量,理论上snappy压缩率可以达到5:1,但是根据测试数据不同,压缩率可能并没有理论上理想;压缩/解压缩无疑需要大量计算,需要大量CPU资源;根据读路径来看,数据读取到缓存之前block块会先被解压,缓存到内存中的block是解压后的,因此和不压缩情况相比,内存前后基本没有任何影响
- 读写性能:因为数据写入是先将kv数据值写到缓存,最后再统一flush的硬盘,而压缩是在flush这个阶段执行的,因此会影响flush的操作,对写性能本身并不会有太大影响;而数据读取如果是从HDFS中读取的话,首先需要解压缩,因此理论上读性能会有所下降;如果数据是从缓存中读取,因为缓存中的block块已经是解压后的,因此性能不会有任何影响;一般情况下大多数读都是热点读,缓存读占大部分比例,压缩并不会对读有太大影响
- 官方分别从压缩率,编解码速率三个方面对其进行对比如下图:
- 综合来看,Snappy的压缩率最低,但是编解码速率最高,对CPU的消耗也最小,目前一般建议使用Snappy
- 从上图看数据编码对资源使用情况以及读写性能的影响:
- 资源使用情况:和压缩一样,编码最直接、最重要的作用也是减少数据硬盘容量,但是数据编码压缩率一般没有数据压缩的压缩率高,理论上只有5:2;编码/解码一般也需要大量计算,需要大量CPU资源;根据读路径来看,数据读取到缓存之前block块并没有被解码,缓存到内存中的block是编码后的,因此和不编码情况相比,相同数据block快占用内存更少,即内存利用率更高
- 读写性能:和数据压缩相同,数据编码也是在数据flush到hdfs阶段执行的,因此并不会直接影响写入过程;前面讲到,数据块是以编码形式缓存到blockcache中的,因此同样大小的blockcache可以缓存更多的数据块,这有利于读性能。另一方面,用户从缓存中加载出来数据块之后并不能直接获取KV,而需要先解码,这却不利于读性能。可见,数据编码在内存充足的情况下会降低读性能,而在内存不足的情况下需要经过测试才能得出具体结论
- HBase目前提供了四种常用的编码方式:Prefix | Diff | Fast_Diff | Prefix_Tree。
- 压缩与编码使用测试结果示例,来源于:http://hbasefly.com/2016/07/02/hbase-pracise-cfsetting/
- 结果分析:
- 数据压缩率并没有理论上0.2那么高,只有0.7左右,这和数据结构有关系。其中压缩、编码、压缩+编码三种方式的压缩率基本相当
- 随机读场景:和默认配置相比,snappy压缩在性能上没有提升,CPU开销却上升了38%;prefix_tree性能上没有提升,CPU利用率也基本相当;snappy+prefix_tree性能没有提升,CPU开销上升了38%
- 区间扫描场景:和默认配置相比,snappy压缩在性能上略有10%的提升,但是CPU开销却上升了23%;prefix_tree性能上略有4%左右的下降,但是CPU开销也下降了5%,snappy+prefix_tree在性能上基本没有提升,CPU开销却上升了23%
- 设计原则:
- 在任何场景下开启prefix_tree编码都是安全的
- 在任何场景下都不要同时开启snappy压缩和prefix_tree编码
- 通常情况下snappy压缩并不能比prefix_tree编码获得更好的优化结果,如果需要使用snappy需要针对业务数据进行实际测试
VERSIONS:
- 用于定义某列族所能记录的最多的版本数量,默认值是3,即每个单元格的最大版本数量是3
- 对于更新频繁的应用,建设设置为1,可以快速淘汰无用的数据,节省存储空间同时还能提升查询效率
- 同样道理,可在建表时指定或通过alter修改表结构实现
TTL:
- TTL:Time To Live 用于定义列族中单元格存活时间,过期数据自动删除
- TTL属性特性:
- 单位是秒,默认值:FOREVEN (永不过期)
- 当一行所有列都过期后,RowKey也会被删除
- 若TTL设置为两个月,则时间戮为2个月之前的数据不能插入
- 同理,在建表时指定或通过alter修改表结构设置
以上是关于HBase: 表结构设计优化的主要内容,如果未能解决你的问题,请参考以下文章