第三部分 HBase API应用和优化
第 1 节 HBase API客户端操作
创建Maven⼯程,添加依赖
<dependencies>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.14.3</version>
<scope>test</scope>
</dependency>
</dependencies>
package com.lagou.hbase;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
/**
* @Author wanglitao
* @Date 2020/8/4 10:47 下午
* @Version 1.0
* @Description
*/
public class HbaseClientDemo {
Configuration conf = null;
Connection conn = null;
HBaseAdmin admin = null;
@Before
public void init() throws IOException {
conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "linux121,linux122");
conf.set("hbase.zookeeper.property.clientPort", "2181");
conn = ConnectionFactory.createConnection(conf);
}
//创建一张hbase表
@Test
public void createTable() throws IOException {
//获取HbaseAdmin对象用来创建表
admin = (HBaseAdmin) conn.getAdmin();
//创建Htabledesc描述器,表描述器
HTableDescriptor worker = new HTableDescriptor(TableName.valueOf("worker"));
//指定列族
worker.addFamily(new HColumnDescriptor("info"));
admin.createTable(worker);
System.out.println("worker表创建成功!!");
}
//插入一条数据
@Test
public void putData() throws IOException {
//需要获取一个table对象
Table worker = conn.getTable(TableName.valueOf("worker"));
//准备put对象
Put put = new Put(Bytes.toBytes("110"));//指定rowkey
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("addr"), Bytes.toBytes("beijing"));
//插入数据,参数类型是put
worker.put(put);
//准备list<puts>,可以执行批量插入
//关闭table对象
worker.close();
System.out.println("插入数据到worker表成功!!");
}
//删除一条数据
@Test
public void deleteData() throws IOException {
//需要获取一个table对象
Table worker = conn.getTable(TableName.valueOf("worker"));
//准备delete对象
Delete delete = new Delete(Bytes.toBytes("110"));
//执行删除
worker.delete(delete);
//关闭table对象
worker.close();
System.out.println("删除数据成功!!");
}
//查询数据
@Test
public void getData() throws IOException {
//准备table对象
Table worker = conn.getTable(TableName.valueOf("worker"));
//准备get对象
Get get = new Get(Bytes.toBytes("110"));
//指定查询某个列族或者列
get.addFamily(Bytes.toBytes("info"));
//执行查询
Result result = worker.get(get);
//获取到result中所有cell对象
Cell[] cells = result.rawCells();
//遍历打印
for (Cell cell : cells) {
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
String f = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println("rowkey-->" + rowkey + ";cf-->" + f + ";column-->" + column + ";value-->" + value);
}
worker.close();
}
//全表扫描
@Test
public void scanData() throws IOException {
//准备table对象
Table worker = conn.getTable(TableName.valueOf("worker"));
//准备scan对象
Scan scan = new Scan();
//执行扫描
ResultScanner resultScanner = worker.getScanner(scan);
for (Result result : resultScanner) {
//获取到result中所有cell对象
Cell[] cells = result.rawCells();
//遍历打印
for (Cell cell : cells) {
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
String f = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println("rowkey-->" + rowkey + ";cf-->" + f + ";column-->" + column + ";value-->" + value);
}
}
worker.close();
}
//指定scan 开始rowkey和结束rowkey,这种查询方式建议使用,指定开始和结束rowkey区间避免全表扫描
@Test
public void scanStartEndData() throws IOException {
//准备table对象
Table worker = conn.getTable(TableName.valueOf("worker"));
//准备scan对象
Scan scan = new Scan();
//指定查询的rowkey区间,rowkey在hbase中是以字典序排序
scan.setStartRow(Bytes.toBytes("001"));
scan.setStopRow(Bytes.toBytes("004"));
//执行扫描
ResultScanner resultScanner = worker.getScanner(scan);
for (Result result : resultScanner) {
//获取到result中所有cell对象
Cell[] cells = result.rawCells();
//遍历打印
for (Cell cell : cells) {
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
String f = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println("rowkey=" + rowkey + ";cf=" + f + ";column=" + column + ";value=" + value);
}
}
worker.close();
}
@After
public void destroy() {
if (admin != null) {
try {
admin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
第 2 节 Hbase 协处理器
2.1 协处理器概述
官方地址:http://hbase.apache.org/book.html#cp
访问HBase的⽅式是使用scan或get获取数据,在获取到的数据上进行业务运算。但是在数据量非常⼤的时候,⽐如一个有上亿行及⼗万个列的数据集,再按常用的方式移动获取数据就会遇到性能问题。客户端也需要有强大的计算能力以及足够的内存来处理这么多的数据。
此时就可以考虑使⽤Coprocessor(协处理器),将业务运算代码封装到Coprocessor中并在RegionServer上运行,即在数据实际存储位置执行,最后将运算结果返回到客户端。利用协处理器,⽤户可以编写运行在 HBase Server 端的代码。
Hbase Coprocessor类似以下概念
触发器和存储过程:
一个Observer Coprocessor有些类似于关系型数据库中的触发器,通过它我们可以在一些事件(如Get或是Scan)发生前后执行特定的代码。
Endpoint Coprocessor则类似于关系型数据库中的存储过程,因为它允许我们在RegionServer上直接对它存储的数据进行运算,⽽非是在客户端完成运算。
MapReduce:MapReduce的原则就是将运算移动到数据所处的节点。Coprocessor也是按照相同的原则去工作的。
AOP:如果熟悉AOP的概念的话,可以将Coprocessor的执⾏过程视为在传递请求的过程中对请求进⾏了拦截,并执⾏了一些⾃定义代码。
2.2 协处理器类型
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语句就能搞定。
2.3 Observer 案例
需求
通过协处理器Observer实现Hbase当中t1表插入数据,指定的另一张表t2也需要插入相对应的数据。
create 't1','info'
create 't2','info'
实现思路
通过Observer协处理器捕捉到t1插⼊数据时,将数据复制一份并保存到t2表中
开发步骤
1. 编写Observer协处理器
package com.lagou.coprocessor;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CoprocessorEnvironment;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
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.filter.ByteArrayComparable;
import org.apache.hadoop.hbase.filter.CompareFilter;
import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
import java.io.IOException;
// 重写prePut方法,监听到向t1表插入数据时,执行向t2表插入数据的代码
public class MyProcessor extends BaseRegionObserver {
@Override
public void prePut(ObserverContext<RegionCoprocessorEnvironment> ce, Put put, WALEdit edit, Durability durability) throws IOException {
//
HTableWrapper t2 = (HTableWrapper)ce.getEnvironment().getTable(TableName.valueOf("t2"));
Cell nameCell = put.get("info".getBytes(), "name".getBytes()).get(0);
Put put1 = new Put(put.getRow());
put1.add(nameCell);
t2.put(put);
t2.close();
}
}
添加依赖
<!-- 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>
2. 打成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
3. 挂载协处理器
hbase(main):056:0> describe 't1'
# 1001表示优先级,该数字越小,优先级越高
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'
4.验证协处理器
向t1表中插⼊数据(shell⽅式验证)
put 't1','rk1','info:name','lisi'
5.卸载协处理器
disable 't1'
alter 't1',METHOD=>'table_att_unset',NAME=>'coprocessor$1'
enable 't2'
第 3 节 HBase表的RowKey设计
RowKey的基本介绍
ASCII码字典顺序。
012,0,123,234,3.
0,3,012,123,234
0,012,123,234,3
字典序的排序规则。
先⽐较第一个字节,如果相同,然后⽐对第⼆个字节,以此类推
如果到第X个字节,其中⼀个已经超出了rowkey的长度,短rowkey排在前面。
1.RowKey⻓度原则
rowkey是⼀个二进制码流,可以是任意字符串,最⼤长度64kb,实际应⽤中一般为10-100bytes,以byte[]形式保存,一般设计成定长。
- 建议越短越好,不要超过16个字节
-
- 设计过⻓会降低memstore内存的利用率和HFile存储数据的效率。
2.RowKey散列原则
建议将rowkey的⾼位作为散列字段,这样将提⾼高数据均衡分布在每个RegionServer,以实现负载均衡的⼏几率。
3.RowKey唯⼀原则
必须在设计上保证其唯一性,访问hbase table中的行有3种⽅式:
- 单个rowkey
- rowkey 的range
- 全表扫描(⼀定要避免全表扫描)
实现⽅式:
1)org.apache.hadoop.hbase.client.Get
2)scan方法: org.apache.hadoop.hbase.client.Scan
scan使⽤的时候注意:
- setStartRow,setEndRow 限定范围, 范围越⼩,性能越⾼。
4.RowKey排序原则
HBase的Rowkey是按照ASCII有序设计的,我们在设计Rowkey时要充分利用这点.
第 4 节 HBase表的热点
4.1 什么是热点
检索habse的记录首先要通过row key来定位数据行。当⼤量的client访问hbase集群的⼀个或少数⼏个节点,造成少数region server的读/写请求过多、负载过⼤,⽽其他region server负载却很小,就造成了“热点”现象
4.2 热点的解决⽅案
- 预分区
预分区的目的让表的数据可以均衡的分散在集群中,⽽不是默认只有⼀个region分布在集群的⼀个节点上。
- 加盐
这里所说的加盐不是密码学中的加盐,而是在rowkey的前⾯增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。
4个region:[,a),[a,b),[b,c),[c,]
原始数据:abc1,abc2,abc3.
加盐后的rowkey:a-abc1,b-abc2,c-abc3
abc1,a
abc2,b
- 哈希
哈希会使同⼀行永远⽤一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某⼀个行数据。
原始数据: abc1,abc2,abc3
哈希:
md5(abc1)=92231b....., 9223-abc1
md5(abc2) =32a131122...., 32a1-abc2
md5(abc3) = 452b1...., 452b-abc3.
- 反转
反转固定⻓度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
第 5 节 HBase的⼆级索引
HBase表按照rowkey查询性能是最高的。rowkey就相当于hbase表的一级索引!!
为了HBase的数据查询更高效、适应更多的场景,诸如使⽤非rowkey字段检索也能做到秒级响应,或者⽀持各个字段进行模糊查询和多字段组合查询等, 因此需要在 HBase上⾯构建二级索引, 以满⾜现实中更复杂多样的业务需求。
hbase的二级索引其本质就是建立hbase表中列与行键之间的映射关系。
常⻅的二级索引我们一般可以借助各种其他的方式来实现,例如Phoenix或者solr或者ES等
第 6 节 布隆过滤器在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>
使用guava的布隆过滤器,封装的⾮常好,使用起来非常简洁⽅便。
例:预估数据量1w,错误率需要减小到万分之一。
使⽤如下代码进行创建。
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 + "不存在");
}
}
}