海量列式非关系数据库HBase 原理深入
前置知识(上一篇):海量列式非关系数据库HBase 架构,shell与API
HBase读数据流程:
前置关键词描述:
- Block Cache :读缓存,缓存上一次读的数据,整个ReginServer只有一个
- MemStore :写缓存,缓存上一次写的数据,每个Store有一个
- WAL: 预写入日志
读取数据流程:
- 1.请求zk 查询meta表的地址
- 2.根据meta表的地址查询rowkey属于哪个reginserver的哪个regin,元数据缓存到MetaCache
- 3.先去BlockCache 和MemStore查找,找不到才去storeFile找,如果在storeFile 查询到,就缓存到BlockCache里
HBase写数据流程:
写数据流程:
- 1.请求zk 查询meta表的地址
- 2.根据meta表的地址查询rowkey属于哪个reginserver的哪个regin,元数据缓存到MetaCache
- 3.先写WAL,再写MemStore,写入MemStore就返回了,
- 如果MemStore内存不够,会flush storeFile文件,然后合并多个storeFile
注: Hbase的写流程比读流程效率高,因为写流程只需要写入内存,读流程先读内存,如果读不到,还需要读磁盘文件。
HBase的flush(刷写) 机制:
刷写条件:
- 1.MemStore大小达到128M
- 2.时间超过1小时
- 3.Reginserver的所有Memstore大小达到reginserver占用的堆内存大小的40%
注: 上述条件默认每10s检查一次
为防止检查之前达到刷写条件,会触发阻塞机制.
阻塞机制触发条件:
- Memstore达到512M
- Reginserver的所有Memstore大小达到堆内存的0.95*0.4
避免阻塞机制的解决方案:
如果出现这种情况,可以增大memstore大小,增大reginserver的堆内存大小。
Compact合并机制:
minor compact 小合并:
文件被选中条件:
- 1. 待合并文件数量大于3
- 2.待合并文件数量 小于10
- 3.文件大小小于128M的文件一定会加入
- 4.排除特别大的文件
合并触发条件:
- 1.menstore flush
- 2.定期检查,默认10s
Major compact:
- 合并所有的HFile,默认7天执行一次,生产中默认关闭
- 手动:major_compact 表名
注意:真正的删除是在这一步进行
Region 拆分机制:
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, 切分阈值为flush size(128M) * 2,否则为MaxRegionFileSize(10GB)。这种切分策略对于大集群中的大表、小表会 比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小region,而是 适可而止。
Hbase 预分区:
为了负载均衡,提高读写效率,否则刚开始读写都在一个机器上进行。
通常解决负载均衡问题,还有以下解决方案:
- 给row key 加前缀
- 对row key 进行hash
- 反转
Region 合并:
Region的合并不是为了性能,而是出于维护的目的。
通过Merge类冷合并Region:
- 需要先关闭hbase集群
- 需求:需要把student表中的2个region数据进行合并:
student,,1593244870695.10c2df60e567e73523a633f20866b4b5.
student,1000,1593244870695.0a4c3ff30a98f79ff6c1e4cc927b3d0d.
这里通过org.apache.hadoop.hbase.util.Merge类来实现,不需要进入hbase shell,直接执行(需要 先关闭hbase集群):
hbase org.apache.hadoop.hbase.util.Merge student \ student,,1595256696737.fc3eff4765709e66a8524d3c3ab42d59. \ student,aaa,1595256696737.1d53d6c1ce0c1bed269b16b6514131d0.
通过online_merge热合并Region:
- 不需要关闭hbase集群,在线进行合并。
与冷合并不同的是,online_merge的传参是Region的hash值,而Region的hash值就是Region名称的最 后那段在两个.之间的字符串部分。 需求:需要把lagou_s表中的2个region数据进行合并: student,,1587392159085.9ca8689901008946793b8d5fa5898e06. \ student,aaa,1587392159085.601d5741608cedb677634f8f7257e000. 需要进入hbase shell: merge_region 'c8bc666507d9e45523aebaffa88ffdd6','02a9dfdf6ff42ae9f0524a3d8f4c7777'
RowKey 设计:
- RowKey长度原则
- rowkey是一个二进制码流,可以是任意字符串,最大长度64kb,实际应用中一般为10-100bytes, 以byte[]形式保存,一般设计成定长。
- 建议越短越好,不要超过16个字节 设计过长会降低memstore内存的利用率和HFile存储数据的效率。
- RowKey散列原则
- 建议将rowkey的高位作为散列字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均 衡的几率。
- RowKey唯一原则
- 必须在设计上保证其唯一性
- RowKey排序原则
- HBase的Rowkey是按照ASCII有序设计的,我们在设计Rowkey时要充分利用这点
scan使用的时候注意:setStartRow,setEndRow 限定范围, 范围越小,性能越高。
Hbase 协处理器:
协处理器类型:
Observer:
协处理器与触发器(trigger)类似:在一些特定事件发生时回调函数(也被称作钩子函数,hook)被执 行。这些事件包括一些用户产生的事件,也包括服务器端内部自动产生的事件。
协处理器框架提供的接口如下:
- RegionObserver:用户可以用这种的处理器处理数据修改事件,它们与表的region联系紧密。
- MasterObserver:可以被用作管理或DDL类型的操作,这些是集群级事件。
- WALObserver:提供控制WAL的钩子函数
Endpoint:
这类协处理器类似传统数据库中的存储过程,客户端可以调用这些 Endpoint 协处理器在Regionserver 中执行一段代码,并将 RegionServer 端执行结果返回给客户端进一步处理。
Endpoint常见用途:
聚合操作 :
假设需要找出一张表中的最大数据,即 max 聚合操作,普通做法就是必须进行全表扫描,然后Client 代码内遍历扫描结果,并执行求最大值的操作。这种方式存在的弊端是无法利用底层集群的并发运算能 力,把所有计算都集中到 Client 端执行,效率低下。
使用Endpoint Coprocessor,用户可以将求最大值的代码部署到 HBase RegionServer 端,HBase 会利用集群中多个节点的优势来并发执行求最大值的操作。也就是在每个 Region 范围内执行求最大值 的代码,将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给Client。在 Client进一步将多个 Region 的最大值汇总进一步找到全局的最大值。
Endpoint Coprocessor的应用我们后续可以借助于Phoenix非常容易就能实现。针对Hbase数据集进行 聚合运算直接使用SQL语句就能搞定。
Observer 案例:
需求: 通过协处理器Observer实现Hbase当中t1表插入数据,指定的另一张表t2也需要插入相对应的数据。
create 't1','info' create 't2','info'
实现思路:
通过Observer协处理器捕捉到t1插入数据时,将数据复制一份并保存到t2表中
java 实现:
<!-- https://mvnrepository.com/artifact/org.apache.hbase/hbase-server --> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-server</artifactId> <version>1.3.1</version> </dependency>
package com.lagou.hbase.processor; import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Durability; import org.apache.hadoop.hbase.client.HTable; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver; import org.apache.hadoop.hbase.coprocessor.ObserverContext; import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment; import org.apache.hadoop.hbase.regionserver.wal.WALEdit; import org.apache.hadoop.hbase.util.Bytes; import java.io.IOException; import java.util.List; //重写prePut方法,监听到向t1表插入数据时,执行向t2表插入数据的代码 public class MyProcessor extends BaseRegionObserver { @Override public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability) throws IOException { //把自己需要执行的逻辑定义在此处,向t2表插入数据,数据具体是什么内容与Put一样 //获取t2表table对象 final HTable t2 = (HTable) e.getEnvironment().getTable(TableName.valueOf("t2")); //解析t1表的插入对象put final Cell cell = put.get(Bytes.toBytes("info"), Bytes.toBytes("name")).get(0); //table对象.put final Put put1 = new Put(put.getRow()); put1.add(cell); t2.put(put1); //执行向t2表插入数据 t2.close(); } }
打成Jar包,上传HDFS:
cd /opt/lagou/softwares mv original-hbaseStudy-1.0-SNAPSHOT.jar processor.jar hdfs dfs -mkdir -p /processor hdfs dfs -put processor.jar /processor
挂载协处理器:
hbase(main):056:0> describe 't1' hbase(main):055:0> alter 't1',METHOD => 'table_att','Coprocessor'=>'hdfs://linux121:9000/processor/processor.jar|com .lagou.hbase.processor.MyProcessor|1001|' #再次查看't1'表, hbase(main):043:0> describe 't1'
验证协处理器:
向t1表中插入数据(shell方式验证)
put 't1','rk1','info:name','lisi'
卸载协处理器:
disable 't1' alter 't1',METHOD=>'table_att_unset',NAME=>'coprocessor$1' enable 't2'
参考地址:https://hbase.apache.org/2.3/book.html#cp_example
布隆过滤器在hbase的应用:
从前面的hbase的数据存储原理,我们知道hbase的读操作需要访问大量的文件,大部分的 实现通过布隆过滤器来避免大量的读文件操作。
布隆过滤器原理:
通常判断某个元素是否存在用的可以选择hashmap。但是 HashMap 的实现也有缺点,例如存储 容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候, 那 HashMap 占据的内存大小就变得很可观了。
Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判 断一个元素是否属于这个集合。 hbase 中布隆过滤器来过滤指定的rowkey是否在目标文件,避免扫描多个文件。使用布隆过滤器来判 断。 布隆过滤器返回true,结果不一定正确,如果返回false则说明确实不存在。
原理示意图:
Bloom Filter案例:
布隆过滤器,已经不需要自己实现,Google已经提供了非常成熟的实现。
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>27.0.1-jre</version> </dependency>
例: 预估数据量1w,错误率需要减小到万分之一。使用如下代码进行创建:
package com.lagou.hbase.bloom; import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; import java.nio.charset.Charset; public class BloomFilterDemo { public static void main(String[] args) { // 1.创建符合条件的布隆过滤器 // 预期数据量10000,错误率0.0001 BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")), 10000, 0.0001); // 2.将一部分数据添加进去 for (int i = 0; i < 5000; i++) { bloomFilter.put("" + i); } System.out.println("数据写入完毕"); // 3.测试结果 for (int i = 0; i < 10000; i++) { if (bloomFilter.mightContain("" + i)) { System.out.println(i + "存在"); } else { System.out.println(i + "不存在"); } } } }