HBase 读写数据流程
Hbase 读数据流程
-
首先从 zk 找到 meta 表的 region 位置,然后读取 meta 表中的数据,meta 表中存储了用户表的 region 信息
-
根据要查询的 namespace、表名和 rowkey 信息,找到写入数据对应的 region 信息
-
找到这个 region 对应的 regionServer,然后发送请求
-
查找对应的 region
-
先从 Memstore 查找数据,如果没有,再从 BlockCache 上读取。
Hbase 上 RegionServer 的内存分为两部分:
- 一部分作为 Memstore,主要用来写
- 另一部分作为 BlockCache,主要用来读数据
-
如果 BlockCache 中也没有,再到 StoreFile 上进行读取。从 StoreFile 中读到数据后,不是直接把结果返回给客户端,而是先把数据写入 BlockCache,然后再返回给客户端。
HBase 写数据流程
- 首先从 zk 找到 meta 表的 region 位置,然后读取 meta 表中的数据,meta 表中存储了用户表的 region 信息。
- 根据 namespace、表名和 rowkey 等信息,找到写入数据对应的 region
- 找到这个 region 对应的 regionServer,然后发送写入请求
- 把数据分别写入 HLog(Write ahead log)和 memStore
- memStore 达到阈值后把数据刷到磁盘,生成 storeFile
- 删除 HLog 中的历史数据
HBase 的 flush(刷写)和 compact(合并)机制
Flush 机制
- 当 memstore 的大小超过这个值的时候,会 flush 到磁盘,默认为 128M
<property>
<name>hbase.hregion.memstore.flush.sizename>
<value>134217728value>
property>
- 当 memstore 的数据超过 1 小时,会 flush 到磁盘
<property>
<name>hbase.regionserver.optionalcacheflushintervalname>
<value>3600000value>
- HregionServer 的全局 memstore 的大小,超过该大小会触发 flush 到磁盘的操作,默认是堆大小的 40%
<property>
<name>hbase.regionserver.global.memstore.sizename>
<value>0.4value>
property>
- 手动 flush
flush tableName
阻塞机制
以上介绍了数据刷写磁盘的标准,但是 HBase 是周期性的检查是否满足来进行刷写的,如果在下次检查到来之前,数据疯狂写入 Memstore,就会把内存撑爆。那怎么处理这种问题呢?
HBase 有阻塞机制,如果触发,就无法继续写入数据。
- 当 Memstore 数据达到 512MB
计算公式:hbase.hregion.memstore.flush.size*hbase.hregion.memstore..block.multiplier
- hbase.hregion.memstore.flush.size 刷写的阀值,默认是 134217728,即 128MB。
- hbase.hregion.memstore.block.multiplier 是一个倍数,默认是 4。
- RegionServer 全部 memstore 达到规定值
有时候集群的“写负载”非常高,写入量一直超过 flush 的量,这时,我们就希望 memstore 不要超过一定的安全设置。在这种情况下,写操作就要被阻塞一直到 memstore 恢复到一个“可管理”的大小, 这个大小就是默认值是堆大小 _ 0.4 _ 0.95。
- hbase.regionserver.global.memstore.size.lower.limit 是 0.95
- hbase.regionserver.global.memstore.size 是 0.4
Compact 合并机制
在 HBase 中主要存在两种类型的 compact 合并。
- Minor compact 小合并
将 Store 中的多个 HFile(StoreFile)合并为一个 HFile
这个过程中,删除和更新的数据仅仅只是做了标记,并没有物理移除,这种合并的触发频率很高。
minor compact 文件选择标准由以下几个参数共同决定:
<property>
<name>hbase.hstore.compaction.minname>
<value>3value>
property>
<property>
<name>hbase.hstore.compaction.maxname>
<value>10value>
property>
<property>
<name>hbase.hstore.compaction.min.sizename>
<value>134217728value>
property>
<property>
<name>hbase.hstore.compaction.max.sizename>
<value>9223372036854775807value>
property>
触发条件
- memstore flush
在进行 memstore flush 前后会进行判断是否触发 compact
- 定期检查线程
周期性检查是否需要进行 compaction 操作,周期性时间由参数:hbase.server.thread.wakefrequency 决定,默认值是 10000 millseconds。
- major compact 大合并
合并 Store 中所有的 HFile 为一个 HFile。
这个过程中有删除标记的数据会真正被移除,同时超过单元格 maxVersion 的版本记录也会被删除。合并频率比较低,默认 7 天执行一次,并且性能消耗非常大。建议生产关闭。在应用空闲时间手动触发。这样可以防止出现在业务高峰期进行 compact
触发条件
<property>
<name>hbase.hregion.majorcompactionname>
<value>604800000value>
property>
手动触发
major_compact tableName
Region 拆分机制
当 Region 中存储的是大量的 rowkey 数据,当 Region 中的数据条数过多的时候,直接影响查询效率。当 Region 过大的时候,HBase 就会拆分 Region。
拆分策略
HBase 的 Region Split 策略一共有以下几种:
- ConstantSizeRegionSplitPolicy
- 0.94 版本前默认切分策略
当 region 大小大于某个阈值(hbase.hregion.max.filesize=10G)之后就会触发切分,一个 region 等分为 2 个 region。
但是在生产线上这种切分策略却有相当大的弊端:切分策略对于大表和小表没有明显的区分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触
发分裂,极端情况下可能就 1 个,这对业务来说并不是什么好事。如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的 region,这对于集群的管理、资源使用、failover 来
说都不是一件好事
- IncreasingToUpperBoundRegionSplitPolicy
- 0.94 版本~2.0 版本默认切分策略
切分策略稍微有点复杂,总体看和 ConstantSizeRegionSplitPolicy 思路相同,一个 region 大小大于设置阈值就会触发切分。但是这个阈值并不像 ConstantSizeRegionSplitPolicy 是一个固定的值,而是会在一定条件下不断调整,调整规则和 region 所属表在当前 regionserver 上的 region 个数有关系.
region split 的计算公式是:
regioncount^3 _ 128M _ 2,当 region 达到该 size 的时候进行 split:
例如::
第一次 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 了
- SteppingSplitPolicy
- 2.0 版本默认切分策略
这种切分策略的切分阈值又发生了变化,相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些,依然和待分裂 region 所属表在当前 regionserver 上的 region 个数有关系,如果 region 个数等于 1,切分阈值为 flushsize*2,否则为 MaxRegionFileSize。
这种切分策略对于大集群中的大表、小表会比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小 region,而是适可而止。
- KeyPrefixRegionSplitPolicy
根据 rowKey 的前缀对数据进行分组,这里是指定 rowKey 的前多少位作为前缀,比如 rowKey 都是 16 位的,指定前 5 位是前缀,那么前 5 位相同的 rowKey 在进行 regionsplit 的时候会分到相同的 region 中。
- DelimitedKeyPrefixRegionSplitPolicy
保证相同前缀的数据在同一个 region 中,例如 rowKey 的格式为:userideventtype_eventid,指定的 delimiter 为,则 split 的的时候会确保 userid 相同的数据在同一个 region 中
- DisabledRegionSplitPolicy
不启用自动拆分, 需要指定手动拆分
拆分策略的应用
Region 拆分策略可以全局统一配置,也可以为单独的表指定拆分策略
- 通过 hbase-site.xml 全局统一配置,也可以为单独的表指定拆分策略
<property>
<name>hbase.regionserver.region.split.policyname>
<value>org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicyvalue>
property>
- 通过 Java API 为单独的表指定 Region 拆分策略
HTableDescriptor tableDesc = new HTableDescriptor("test1");
tableDesc.setValue(HTableDescriptor.SPLIT_POLICY, IncreasingToUpperBoundRegionSplitPolicy.class.getName());
tableDesc.addFamily(new HColumnDescriptor(Bytes.toBytes("cf1")));
admin.createTable(tableDesc);
- 通过 HBase Shell 为单个表指定 Region 拆分策略
> create 'test2', {METADATA => {'SPLIT_POLICY' =>
'org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicy'}},{NAME => 'cf1'}
HBase 表的预分区
- 为什么要预分区?
当一个 table 刚被创建的时候,HBase 默认分配一个 region 给 table。也就是说这个时候,所有的读写请求都会访问同一个 regionServer 的同一个 region 中,这个时候就达不到负载均衡的效果了,集群中其他的 regionServer 就可能会处于比较空闲的状态。解决这个问题可以用 pre-splitting。在创建 table 的时候就配置好,生成多个 region。
好处就是:
- 增加数据读写效率
- 负载均衡,防止数据倾斜
- 方便集群容灾调度 region
每个 region 维护着 startRow 与 endRowyKey,如果加入的数据符合某个 region 维护的 rowkey 范围,则该数据交给这个 region 来维护。
- 手动指定预分区
create 'person','info1','info2',SPLITS => ['1000','2000','3000']
也可以把分区规则创建于文件中。
create 'student','info',SPLITS_FILE => '/root/hbase/split.txt'
Region 合并
Region 的合并不是为了性能,而是出于维护的目的。
Region 合并的方式:
- 通过 Merge 类冷合并
需要先关闭 HBase 集群。
不需要进入 hbase shell,直接执行:
合并的信息可以从页面上获取
hbase org.apache.hadoop.hbase.util.Merge user user,,1662823434957.f971c62e76cdff90ea957c0709099bb5. user,1000,1662823434957.366fc3c5e6237a8993cc0e143109c229.
- 通过 online_merge 热合并 Region
不需要关闭 HBase 集群,在线进行合并
与冷合并不同的是,online_merge 的传参是 Region 的 hash 值,Region 的 hash 值就是 Region 名称的最后那段在两个“.”之间的字符串部分。
示例:
merge_region 'c8d1a1b7f709dfcd8b0c574b4121fdca','1d7e67e13a48b67d2d7867ca9717183c'
__EOF__