HBase进阶&优化

HBase进阶&优化

四、HBase进阶

4.1、Master 架构

Master 主要进程,具体实现类为HMaster,通常部署在namenode上。

1)Meta 表格介绍:(警告:不要去改这个表)
全称 hbase:meta,只是在 list 命令中被过滤掉了,本质上和 HBase 的其他表格一样。
RowKey:

([table],[region start key],[region id]) 即 表名,region 起始位置和 regionID。 

列:
info:regioninfo 为 region 信息,存储一个 HRegionInfo 对象。
info:server 当前 region 所处的 RegionServer 信息,包含端口号。
info:serverstartcode 当前 region 被分到 RegionServer 的起始时间。
如果一个表处于切分的过程中,即region切分,还会多出两列info:splitA和info:splitB,存储值也是 HRegionInfo 对象,拆分结束后,删除这两列。

注意:在客户端对元数据进行操作的时候才会连接 master,如果对数据进行读写,直接连接zookeeper 读取目录/hbase/meta-region-server 节点信息,会记录 meta 表格的位置。直接读取即可,不需要访问 master,这样可以减轻 master 的压力,相当于 master 专注 meta 表的写操作,客户端可直接读取 meta 表。

在 HBase 的 2.3 版本更新了一种新模式:Master Registry。客户端可以访问 master 来读取meta 表信息。加大了 master 的压力,减轻了 zookeeper 的压力。

4.2、RegionServer 架构

Region Server
主要进程,具体实现类为HRegionServer。通常部署在dataNode上。

除了主要的组件,还会启动多个线程监控一些必要的服务:
1、Region拆分
2、Region合并
3、MemStory刷写
4、WAL预写日志滚动

1)MemStore
写缓存,由于 HFile 中的数据要求是有序的,所以数据是先存储在 MemStore 中,排好序后,等到达刷写时机才会刷写到 HFile,每次刷写都会形成一个新的 HFile,写入到对应的文件夹 store 中。
2)WAL
由于数据要经 MemStore 排序后才能刷写到 HFile,但把数据保存在内存中会有很高的概率导致数据丢失,为了解决这个问题,数据会先写在一个叫做 Write-Ahead logfile 的文件中,然后再写入 MemStore 中。所以在系统出现故障的时候,数据可以通过这个日志文件重建。
3)BlockCache
读缓存,每次查询出的数据会缓存在 BlockCache 中,方便下次查询。

4.3、写流程

写流程:
写流程顺序正如 API 编写顺序,首先创建 HBase 的重量级连接
(1)首先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server
(2)访问对应的 Region Server,获取 hbase:meta 表,将其缓存到连接中,作为连接属性 MetaCache,由于 Meta 表格具有一定的数据量,导致了创建连接比较慢;
之后使用创建的连接获取 Table,这是一个轻量级的连接,只有在第一次创建的时候会检查表格是否存在访问 RegionServer,之后在获取 Table 时不会访问 RegionServer;
(3)调用Table的put方法写入数据,此时还需要解析RowKey,对照缓存的MetaCache,查看具体写入的位置有哪个 RegionServer;
(4)将数据顺序写入(追加)到 WAL,此处写入是直接落盘的,并设置专门的线程控制 WAL 预写日志的滚动(类似 Flume);
(5)根据写入命令的 RowKey 和 ColumnFamily 查看具体写入到哪个 MemStory,并且在 MemStory 中排序;
(6)向客户端发送 ack;
(7 )等达到 MemStore 的刷写时机后,将数据刷写到对应的 story 中。

4.4、MemStore Flush

MemStore 刷写由多个线程控制,条件互相独立:
主要的刷写规则是控制刷写文件的大小,在每一个刷写线程中都会进行监控
(1)当某个 memstroe 的大小达到了 hbase.hregion.memstore.flush.size(默认值 128M),其所在 region 的所有 memstore 都会刷写。
当 memstore 的大小达到了

hbase.hregion.memstore.flush.size(默认值 128M) 
hbase.hregion.memstore.block.multiplier(默认值 4) 

时,会刷写同时阻止继续往该 memstore 写数据(由于线程监控是周期性的,所有有可能面对数据洪峰,尽管可能性比较小)。

(2)由 HRegionServer 中的属性 MemStoreFlusher 内部线程 FlushHandler 控制。标准为LOWER_MARK(低水位线)和 HIGH_MARK(高水位线),意义在于避免写缓存使用过多的内存造成 OOM。
当 region server 中 memstore 的总大小达到低水位线:

java_heapsize 
*hbase.regionserver.global.memstore.size(默认值 0.4) 
*hbase.regionserver.global.memstore.size.lower.limit(默认值 0.95)

region 会按照其所有 memstore 的大小顺序(由大到小)依次进行刷写。直到 region server中所有 memstore 的总大小减小到上述值以下。
当 region server 中 memstore 的总大小达到高水位线:

java_heapsize 
*hbase.regionserver.global.memstore.size(默认值 0.4) 

时,会同时阻止继续往所有的 memstore 写数据。

(3)为了避免数据过长时间处于内存之中,到达自动刷写的时间,也会触发 memstore flush。由 HRegionServer 的属性 PeriodicMemStoreFlusher 控制进行,由于重要性比较低,5min才会执行一次。
自动刷新的时间间隔由该属性进行配置

hbase.regionserver.optionalcacheflushinterval(默认1 小时)。 

(4)当 WAL 文件的数量超过 hbase.regionserver.max.logs,region 会按照时间顺序依次进行刷写,直到 WAL 文件数量减小到 hbase.regionserver.max.log 以下(该属性名已经废弃,现无需手动设置,最大值为 32)。

4.5、读流程

4.5.1、HFile 结构

在了解读流程之前,需要先知道读取的数据是什么样子的。
HFile 是存储在 HDFS 上面每一个 store 文件夹下实际存储数据的文件。里面存储多种内容。包括数据本身(keyValue 键值对)、元数据记录、文件信息、数据索引、元数据索引和一个固定长度的尾部信息(记录文件的修改情况)。
键值对按照块大小(默认 64K)保存在文件中,数据索引按照块创建,块越多,索引越大。每一个 HFile 还会维护一个布隆过滤器(就像是一个很大的地图,文件中每有一种 key,就在对应的位置标记,读取时可以大致判断要 get 的 key 是否存在 HFile 中)。

KeyValue 内容如下:

rowlength -----------→ key 的长度 
row -----------------→ key 的值 
columnfamilylength --→ 列族长度 
columnfamily --------→ 列族 
columnqualifier -----→ 列名 
timestamp -----------→ 时间戳(默认系统时间) 
keytype -------------→ Put

由于 HFile 存储经过序列化,所以无法直接查看。可以通过 HBase 提供的命令来查看存储在 HDFS 上面的 HFile 元数据内容。

[atguigu@hadoop102 hbase]$ bin/hbase hfile -m  -f /hbase/data/命名 空间/表名/regionID/列族/HFile 名 

4.5.2、读流程


创建连接同写流程。
(1)创建 Table 对象发送 get 请求。
(2)优先访问 Block Cache,查找是否之前读取过,并且可以读取 HFile 的索引信息和布隆过滤器。
(3)不管读缓存中是否已经有数据了(可能已经过期了),都需要再次读取写缓存和store 中的文件。
(4)最终将所有读取到的数据合并版本,按照 get 的要求返回即可。

4.5.3、合并读取数据优化

每次读取数据都需要读取三个位置,最后进行版本的合并。效率会非常低,所有系统需要对此优化。
(1)HFile 带有索引文件,读取对应 RowKey 数据会比较快。
(2)Block Cache 会缓存之前读取的内容和元数据信息,如果 HFile 没有发生变化(记录在 HFile 尾信息中),则不需要再次读取。
(3)使用布隆过滤器能够快速过滤当前 HFile 不存在需要读取的 RowKey,从而避免读取文件。(布隆过滤器使用 HASH 算法,不是绝对准确的,出错会造成多扫描一个文件,对读取数据结果没有影响)

4.6、StoreFile Compaction

由于 memstore 每次刷写都会生成一个新的 HFile,文件过多读取不方便,所以会进行文件的合并,清理掉过期和删除的数据,会进行 StoreFile Compaction。

Compaction 分为两种,分别是 Minor CompactionMajor Compaction。Minor Compaction会将临近的若干个较小的 HFile 合并成一个较大的 HFile,并清理掉部分过期和删除的数据,有系统使用一组参数自动控制,Major Compaction 会将一个 Store 下的所有的 HFile 合并成一个大 HFile,并且会清理掉所有过期和删除的数据,由参数 hbase.hregion.majorcompaction控制,默认 7 天。

Minor Compaction 控制机制:
参与到小合并的文件需要通过参数计算得到,有效的参数有 5 个

(1)hbase.hstore.compaction.ratio(默认 1.2F)合并文件选择算法中使用的比率。    
(2)hbase.hstore.compaction.min(默认 3)  为 Minor Compaction 的最少文件个数。    
(3)hbase.hstore.compaction.max(默认 10) 为 Minor Compaction 最大文件个数。    
(4)hbase.hstore.compaction.min.size(默认 128M)为单个 Hfile 文件大小最小值,小于这个数会被合并。    
(5)hbase.hstore.compaction.max.size(默认 Long.MAX_VALUE)为单个 Hfile 文件大小最大值,高于这个数不会被合并。    

小合并机制为拉取整个 store 中的所有文件,做成一个集合。之后按照从旧到新的顺序遍历。
判断条件为:
1、过小合并,过大不合并。
2、文件大小/ hbase.hstore.compaction.ratio < (剩余文件大小和) 则参与压缩。所有把比值设置过大,如 10 会最终合并为 1 个特别大的文件,相反设置为 0.4,会最终产生 4 个 storeFile。不建议修改默认值
3、满足压缩条件的文件个数达不到个数要求(3 <= count <= 10)则不压缩。

4.7、Region Split

Region 切分分为两种,创建表格时候的预分区即自定义分区,同时系统默认还会启动一个切分规则,避免单个 Region 中的数据量太大。

4.7.1、预分区(自定义分区)

每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。

1)手动设定预分区

create 'staff1','info', SPLITS => ['1000','2000','3000','4000'] 

2)生成 16 进制序列预分区

create 'staff2','info',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'} 

3)按照文件中设置的规则预分区
(1)创建 splits.txt 文件内容如下:

aaaa
bbbb
cccc
dddd

(2)然后执行:

create 'staff3', 'info',SPLITS_FILE => 'splits.txt' 

4)使用 JavaAPI 创建预分区

4.7.2、系统拆分

Region 的拆分是由 HRegionServer 完成的,在操作之前需要通过 ZK 汇报 master,修改对应的 Meta 表信息添加两列 info:splitA 和 info:splitB 信息。之后需要操作 HDFS 上面对应的文件,按照拆分后的 Region 范围进行标记区分,实际操作为创建文件引用,不会挪动数据。刚完成拆分的时候,两个 Region 都由原先的 RegionServer 管理。之后汇报给 Master,由Master将修改后的信息写入到Meta表中。等待下一次触发负载均衡机制,才会修改Region的管理服务者,而数据要等到下一次压缩时,才会实际进行移动。

不管是否使用预分区,系统都会默认启动一套 Region 拆分规则。不同版本的拆分规则有差别。系统拆分策略的父类为 RegionSplitPolicy。
0.94 版本之前 => ConstantSizeRegionSplitPolicy
( 1 ) 当 1 个 region 中 的 某 个 Store 下 所 有 StoreFile 的总大小超过hbase.hregion.max.filesize (10G),该 Region 就会进行拆分。
0.94 版本之后,2.0 版本之前 => IncreasingToUpperBoundRegionSplitPolicy

( 2 ) 当1个 region中的某个 Store 下所有StoreFile 的总大小超过Min(initialSizeR^3 ,hbase.hregion.max.filesize"),该 Region 就会进行拆分。其中 initialSize 的默认值为 2hbase.hregion.memstore.flush.size,R 为当前 Region Server 中属于该 Table 的Region 个数(0.94 版本之后)。
具体的切分策略为:
第一次 split:1^3 * 256 = 256MB
第二次 split:2^3 * 256 = 2048MB
第三次 split:3^3 * 256 = 6912MB
第四次 split:4^3 * 256 = 16384MB > 10GB,因此取较小的值 10GB
后面每次 split 的 size 都是 10GB 了。
2.0 版本之后 => SteppingSplitPolicy

(3)Hbase 2.0 引入了新的 split 策略:如果当前 RegionServer 上该表只有一个 Region,按照 2 * hbase.hregion.memstore.flush.size 分裂,否则按照 hbase.hregion.max.filesize 分裂。

五、HBase 优化

5.1、RowKey 设计

一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于哪个一个预分区的区间内,设计rowkey的主要目的 ,就是让数据均匀的分布于所有的region中,在一定程度上防止数据倾斜。接下来谈一谈 rowkey 常用的设计方案。

1)生成随机数、hash、散列值
2)时间戳反转
3)字符串拼接

需求:使用 hbase 存储下列数据,要求能够通过 hbase 的 API 读取数据完成两个统计需求。
(1)统计张三在 2021 年 12 月份消费的总金额
(2)统计所有人在 2021 年 12 月份消费的总金额

5.1.1、实现需求 1

为了能够统计张三在 2021 年 12 月份消费的总金额,我们需要用 scan 命令能够得到张三在这个月消费的所有记录,之后在进行累加即可。Scan 需要填写 startRow 和 stopRow:

scan :  startRow ->   ^A^Azhangsan2021-12       
        endRow   ->   ^A^Azhangsan2021-12. 

注意点:
(1)避免扫描数据混乱,解决字段长度不一致的问题,可以使用相同阿斯卡码值的符号进行填充,框架底层填充使用的是阿斯卡码值为 1 的^A。
(2)最后的日期结尾处需要使用阿斯卡码略大于’-’的值
最终得到 rowKey 的设计为:

//注意 rowkey 相同的数据会视为相同数据覆盖掉之前的版本 
rowKey:  userdate(yyyy-MM-dd HH:mm:SS) 

5.1.2、实现需求 2

问题提出:按照需要 1 的 rowKey 设计,会发现对于需求 2,完全没有办法写 rowKey 的扫描范围。此处能够看出 hbase 设计 rowKey 使用的特点为:
适用性强 泛用性差 能够完美实现一个需求 但是不能同时完美实现多个需要。
如果想要同时完成两个需求,需要对 rowKey 出现字段的顺序进行调整。
调整的原则为:可枚举的放在前面。其中时间是可以枚举的,用户名称无法枚举,所以必须把时间放在前面。

最终满足 2 个需求的设计 可以穷举的写在前面即可 
rowKey 设计格式 => date(yyyy-MM)^A^Auserdate(-dd hh:mm:ss ms) 
 
(1)统计张三在 2021 年 12 月份消费的总金额 
scan: startRow => 2021-12^A^Azhangsan   
      stopRow  => 2021-12^A^Azhangsan. 
 
(2)统计所有人在 2021 年 12 月份消费的总金额 
scan: startRow => 2021-12   
      stopRow  => 2021-12. 

5.1.3、添加预分区优化

预分区的分区号同样需要遵守 rowKey的 scan 原则。所有必须添加在 rowKey的最前面,前缀为最简单的数字。同时使用 hash 算法将用户名和月份拼接决定分区号。(单独使用用户名会造成单一用户所有数据存储在一个分区)

添加预分区优化 
startKey stopKey    
            001 
001         002 
002         003 
... 
119         120 
 
分区号=> hash(user+date(MM)) % 120 
分区号填充  如果得到 1 => 001 
 
rowKey 设计格式 => 分区号 date(yyyy-MM)^A^Auserdate(-dd hh:mm:ss ms) 

缺点:实现需求 2 的时候,由于每个分区都有 12 月份的数据,需要扫描 120 个分区。
解决方法:提前将分区号和月份进行对应。

提前将月份和分区号对应一下 
000 到 009 分区  存储的都是 1 月份数据 
010 到 019 分区  存储的都是 2 月份数据 
... 
110 到 119 分区  存储的都是 12 月份数据 

是 9 月份的数据 
分区号=> hash(user+date(MM)) % 10 + 80 
分区号填充  如果得到 85 => 085 
 
得到 12 月份所有人的数据 
扫描 10 次 
scan: startRow => 1102021-12   
      stopRow  => 1102021-12.   
      ...   
      startRow => 1122021-12   
      stopRow  => 1122021-12.   
      ..   
      startRow => 1192021-12   
      stopRow  => 1192021-12. 5

5.2、参数优化

1)Zookeeper 会话超时时间
hbase-site.xml

属性:zookeeper.session.timeout 
解释:默认值为 90000 毫秒(90s)。当某个 RegionServer 挂掉,90s 之后 Master 才 能察觉到。可适当减小此值,尽可能快地检测 regionserver 故障,可调整至 20-30s。
 
看你能有都能忍耐超时,同时可以调整重试时间和重试次数 
hbase.client.pause(默认值 100ms) 
hbase.client.retries.number(默认 15 次) 

2)设置 RPC 监听数量
hbase-site.xml

属性:hbase.regionserver.handler.count 
解释:默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写 请求较多时,增加此值。 

3)手动控制 Major Compaction
hbase-site.xml

属性:hbase.hregion.majorcompaction 
解释:默认值:604800000 秒( 7 天), Major Compaction 的周期,若关闭自动 Major Compaction,可将其设为 0。如果关闭一定记得自己手动合并,因为大合并非常有意义 

4)优化 HStore 文件大小
hbase-site.xml

属性:hbase.hregion.max.filesize 
解释:默认值 10737418240(10GB),如果需要运行 HBase 的 MR 任务,可以减小此值, 因为一个 region 对应一个 map 任务,如果单个 region 过大,会导致 map 任务执行时间过长。该值的意思就是,如果 HFile 的大小达到这个数值,则这个 region 会被切分为两 个 Hfile。 

5)优化 HBase 客户端缓存

属性:hbase.client.write.buffer 
解释:默认值 2097152bytes(2M)用于指定 HBase 客户端缓存,增大该值可以减少 RPC 调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到 减少 RPC 次数的目的。 

6)指定 scan.next 扫描 HBase 所获取的行数
hbase-site.xml

属性:hbase.client.scanner.caching 
解释:用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大。 

7)BlockCache 占用 RegionServer 堆内存的比例
hbase-site.xml

属性:hfile.block.cache.size 
解释:默认 0.4,读请求比较多的情况下,可适当调大 

8)MemStore 占用 RegionServer 堆内存的比例
hbase-site.xml

属性:hbase.regionserver.global.memstore.size 
解释:默认 0.4,写请求较多的情况下,可适当调大 

Lars Hofhansl(拉斯·霍夫汉斯)大神推荐 Region 设置 20G,刷写大小设置 128M,其它默认。

5.3、JVM 调优

JVM 调优的思路有两部分:一是内存设置,二是垃圾回收器设置。
垃圾回收的修改是使用并发垃圾回收,默认 PO+PS 是并行垃圾回收,会有大量的暂停。理由是 HBsae 大量使用内存用于存储数据,容易遭遇数据洪峰造成 OOM,同时写缓存的数据是不能垃圾回收的,主要回收的就是读缓存,而读缓存垃圾回收不影响性能,所以最终设置的效果可以总结为:防患于未然,早洗早轻松。

1)设置使用 CMS 收集器:

-XX:+UseConcMarkSweepGC 

2)保持新生代尽量小,同时尽早开启 GC,例如:

//在内存占用到 70%的时候开启 GC 
-XX:CMSInitiatingOccupancyFraction=70 
//指定使用 70%,不让 JVM 动态调整 
-XX:+UseCMSInitiatingOccupancyOnly 
//新生代内存设置为 512m 
-Xmn512m 
//并行执行新生代垃圾回收 
-XX:+UseParNewGC 
// 设置 scanner扫描结果占用内存大小,在 hbase-site.xml中,设置 hbase.client.scanner.max.result.size(默认值为 2M)为 eden空间的 1/8 (大概在 64M) 
// 设置多个与 max.result.size * handler.count 相乘的结果小于 Survivor Space(新生代经过垃圾回收之后存活的对象) 

5.4、HBase 使用经验法则

官方给出了权威的使用法则:

(1)Region 大小控制 10-50G 
(2)cell 大小不超过 10M(性能对应小于 100K 的值有优化),如果使用 mob(Mediumsized Objects 一种特殊用法)则不超过 50M。 
(3)1 张表有 1 到 3 个列族,不要设计太多。最好就 1 个,如果使用多个尽量保证不会同时读取多个列族。 
(4)1 到 2 个列族的表格,设计 50-100 个 Region。 
(5)列族名称要尽量短,不要去模仿 RDBMS(关系型数据库)具有准确的名称和描述。 
(6)如果 RowKey 设计时间在最前面,会导致有大量的旧数据存储在不活跃的 Region中,使用的时候,仅仅会操作少数的活动 Region,此时建议增加更多的 Region 个数。 
(7)如果只有一个列族用于写入数据,分配内存资源的时候可以做出调整,即写缓存不会占用太多的内存。 

评论

暂无

添加新评论