HBase API及协处理器
HBase API 应用
引入依赖
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.3.1</version>
</dependency>
HBase API 使用示例:
public class HBaseClient {
Connection connection;
Admin admin;
@Before
public void init() throws IOException {
Configuration configuration = HBaseConfiguration.create();
configuration.set("hbase.zookeeper.quorum","linux2,linux3,linux4");
configuration.set("hbase.zookeeper.property.clientPort","2181");
connection = ConnectionFactory.createConnection(configuration);
}
/**
* 创建表
* @throws IOException
*/
@Test
public void createTable() throws IOException {
admin = connection.getAdmin();
//创建表描述器
HTableDescriptor teacher = new HTableDescriptor(TableName.valueOf("teacher"));
//设置列族
teacher.addFamily(new HColumnDescriptor("info"));
admin.createTable(teacher);
System.out.println("创建teacher表成功");
}
/**
* 新增数据
* @throws IOException
*/
@Test
public void putData() throws IOException {
Table teacher = connection.getTable(TableName.valueOf("teacher"));
//设置rowkey
Put put = new Put(Bytes.toBytes("003"));
put.addColumn(Bytes.toBytes("info"),Bytes.toBytes("name"),Bytes.toBytes("xiaoqing"));
put.addColumn(Bytes.toBytes("info"),Bytes.toBytes("age"),Bytes.toBytes("13"));
teacher.put(put);
teacher.close();
}
/**
* 查询某个列族数据
*/
@Test
public void getData() throws IOException {
HTable teacher = (HTable) connection.getTable(TableName.valueOf("teacher"));
Get get=new Get(Bytes.toBytes("001"));
get.addFamily(Bytes.toBytes("info"));
Result result = teacher.get(get);
Cell[] cells = result.rawCells();
for (Cell cell : cells){
String cellFamily = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
System.out.println("rowkey:"+rowkey+","+cellFamily+"__"+column+"__"+value);
}
}
/**
* scan全表
* @throws IOException
*/
@Test
public void scanAll() throws IOException {
HTable teacher = (HTable) connection.getTable(TableName.valueOf("teacher"));
Scan scan=new Scan();
ResultScanner resultScanner = teacher.getScanner(scan);
for (Result result : resultScanner){
Cell[] cells = result.rawCells();
for (Cell cell : cells){
String cellFamily = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
System.out.println("rowkey:"+rowkey+","+cellFamily+"__"+column+"__"+value);
}
}
}
/**
* scan全表数据根据rowkey范围
* @throws IOException
*/
@Test
public void scanRowKey() throws IOException {
HTable teacher = (HTable) connection.getTable(TableName.valueOf("teacher"));
Scan scan=new Scan();
scan.setStartRow("001".getBytes(StandardCharsets.UTF_8));
scan.setStopRow("002".getBytes(StandardCharsets.UTF_8));
ResultScanner resultScanner = teacher.getScanner(scan);
for (Result result : resultScanner){
Cell[] cells = result.rawCells();
for (Cell cell : cells){
String cellFamily = Bytes.toString(CellUtil.cloneFamily(cell));
String column = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
System.out.println("rowkey:"+rowkey+","+cellFamily+"__"+column+"__"+value);
}
}
}
/**
* 删除数据
* @throws IOException
*/
@Test
public void deleteData() throws IOException {
Table teacher = (Table) connection.getTable(TableName.valueOf("teacher"));
Delete delete=new Delete(Bytes.toBytes("001"));
teacher.delete(delete);
teacher.close();
System.out.println("删除数据成功");
}
@After
public void destory(){
if(admin != null){
try {
admin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
HBase 协处理器
通常查询 HBase 数据是使用 scan 或者 get,再根据获取到的数据进行业务计算。但是在数据量非常大的时候,比如一个有上亿行及十万个列的数据集,再按照常用方式获取数据就会获得性能问题。客户端也需要有强大的计算能力以及足够的内存来处理这么多的数据。
此时就可以考虑使用 Coprocessor(协处理器),将业务运算代码封装到 Coprocessor 中并在 RegionServer 上运行,即在数据实际存储位置执行,最后将运算结果返回到客户端。利用协处理器,用于可以编写运行在 HBase Server 端的代码。
协处理器类型:
1. Observer:
协处理器与触发器类似,在一些特定事件发生时回调函数(也称作钩子函数)被执行。这些事包括一些用户产生的事件,也包括服务端内部自动产生的事件。
协处理器框架提供的接口如下:
- RegionObserver:用户可以用这种的处理器处理数据修改事件
- MasterObserver:可以被用作管理或 DDL 类型的操作,这些是集群级事件
- WALObserver:提供控制 WAL 的钩子函数
案例实战:
实现 HBase 当中向 t1 表中插入一条数据,指定的 t2 表也要插入一条一模一样的数据。
- 先创建两个表
create 't1','info'
create 't2','info'
- 引入依赖
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>1.3.1</version>
</dependency>
- 代码编写
public class MyProcessor extends BaseRegionObserver {
@Override
public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability) throws IOException {
//把自己需要执行的逻辑定义在此处,向t2表插入数据,数据具体是什么内容与Put一样
final HTableInterface t2 = 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
hdfs dfs -mkdir -p /processor
hdfs dfs -put hbase-1.0-SNAPSHOT.jar /processor
- 挂载协处理器
alter 't1',METHOD => 'table_att','Coprocessor'=>'hdfs://linux2:9000/processor/hbase-1.0-SNAPSHOT.jar|com.lagou.hbase.MyProcessor|1001|'
挂载完成后,可以通过 以下命令看看挂载成功没
describe 't1'
这样就是挂载成功了
- 验证
put 't1','rk1','info:name','lisi'
向 t1 表插入数据后,发现 t2 表也插入了数据。
卸载协处理器
disable 't1'
alter 't1',METHOD=>'table_att_unset',NAME=>'coprocessor$1'
enable 't1'
2. Endpoint
这类协处理器类似传统数据库中的存储过程,客户端可以调用这些 Endpoint 协处理器在 Regionserver 中执行一段代码,并将 RegionServer 端执行结果返回给客户端进一步处理。
常见用途
如:聚合操作。假设需要找出一张表中最大的数据,普通做法是全表扫描,然后在 Client 端内遍历结果,并执行求最大值的操作。这种方式存在的弊端是无法利用底层集群的并发运算能力,把所有计算都集中到 Client 端,效率低下。
使用 Endpoint Coprocessor,用户可以把求最大值的代码部署到 RegionServer 端,HBase 会利用集群中多个节点的优势来并发执行求最大值的操作。也就是每个 Region 范围内求最大值,将每个 Region 的最大值在 RegionServer 端算出,仅仅将 max 值返回给 Client。在 Client 进一步将多个 Region 的最大值进行比较找到全局的最大值即可。
Endpoint Coprocessor 的应用借助于 Phoenix 非常容易就能实现。
HBase 表的 RowKey 设计
- RowKey 字典顺序
RowKey 是基于 ASCII 码值进行字典排序的。先比较第一个字节,如果相同,就比较第二个字节。如果到第 X 个字节,其中一个已经超出了 rowkey 的长度,短 rowkey 的排在前面。
- RowKey 长度原则
rowkey 是一个二级制码流,可以是任意字符串,最大长度 64kb,实际应用中一般为 10-100bytes,以 byte[]形式保存,一般设计成定长。
建议越短越好,不要超过 16 个字节。设计过长会降低 Memstore 内存的利用率和 HFile 存储数据的效率
- RowKey 散列原则
建议将 rowkey 的高位作为散列字段,这样将提高数据均衡分布在每个 RegionServer,以实现负载均衡的几率
- RowKey 唯一原则
必须在设计上保证其唯一性。访问 hbase table 中的行,有三种方式:
- 单个 rowkey
- rowkey 的 range
- 全表扫描(尽量避免全表扫描)
- RowKey 排序原则
HBase 的 RowKey 是按照 ASCII 有序设计的,我们设计 RowKey 时要充分利用这点。
HBase 表的热点
什么是热点?
检索 hbase 的记录首先要通过 rowkey 来定位数据行,当大量的 client 访问 hbase 集群的一个或少数几个节点,造成少数 region server 请求过多,而其他 region server 请求很少,就造成了“热点”现象。
热点的解决方案
- 预分区
预分区的目的是让表的数据可以均衡的分散在集群中,而不是默认只有一个 region 分布在集群的一个节点上。
- 加盐
这里的“加盐”指的是在 rowkey 的前面增加随机数,具体就是给 rowkey 分配一个随机前缀以使得它和之前的 rowkey 的开头不同
- 哈希
哈希会使同一行永远用同一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的 rowkey,可以使用 get 操作准确获取某一个行数据。
原始数据: abc1,abc2,abc3
哈希:
md5(abc1)=92231b....., 9223-abc1
md5(abc2) =32a131122...., 32a1-abc2
md5(abc3) = 452b1...., 452b-abc3.
- 反转
反转固定长度或者数字格式的 rowkey,这样可以使得 rowkey 中经常改变的部分放在前面,这样可以有效的随机 rowkey,但是牺牲了 rowkey 的有序性。
HBase 应用技巧
二级索引
HBase 按照 rowkey 查询性能是最高的,rowkey 就相当于 hbase 表的一级索引。
为了 HBase 的数据查询更高效、适应更多的场景。如:使用非 rowkey 字段检索也能秒级相应,或者支持多个字段组合查询或模糊查询等。因此需要在 HBase 上构建二级索引,以满足现实中复杂多样的业务需求。
hbase 的二级索引其本质就是建立 hbase 表中列与行键的映射关系。
常见的二级索引我们一般可以借助各种其他的方式来实现,例如 Phoenix、Solr、Es。
布隆过滤器的应用
之前在讲 hbase 的数据存储原理的时候,我们知道 hbase 的读操作需要访问大量的文件,大部分的实现通过布隆过滤器来避免大量的读文件操作。