海量列式非关系数据库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 + "不存在");
            }
        }
    }
}

 

posted @ 2021-09-15 09:47  wangheng1409  阅读(355)  评论(0编辑  收藏  举报