HBase 逻辑模型
Hbase是一个键值型数据库。适合存储列不确定,不确定大小的半结构化数据。
表
类似关系数据库中的表,一个表可以有多个分区
行键
用来表示唯一的一行数据,用字节数组进行存储,相当于关系型数据库中的主键
行键也是Hbase最有效的索引,表中的数据会以行键为标准进行字典序的排序。
列簇
Hbase是一个列式数据库,根据列簇来进行存储时机的数据,每一个列簇会有一个存储仓库(store),每个store会有多个存储文件(storeFile)
列限定符
每个列簇中可以有任意个列限定符来表示不同的列,与关系型数据库不同,使用列无需再表创建时指定,(列簇要在创建表时指定)。
单元格
单元格(cell),由以下因素唯一确定 rowkey(行键),列簇(column family),列,时间戳,类型(put,delete),是Hbase的存储单元,以字节码形式存储。
版本
Hbase数据写入后是不会改变的 ,但是多个数据会形成不同的版本,以时间戳(写入时间)进行区分
分区
分区是集群中高可用,动态扩展,负载均衡的最小单位,一个表会进行拆分成多个分区然后均匀地分布在每台机器上,分区以行键分片,可以再创建表的时候预先分片,也可以进行动态分片。
Hbase物理模型
Hbase中表按行键的范围被划分成不同的分区,各个分区由分区服务器负责管理并提供数据读写服务,
一个分区同时有且仅有一个分区服务器提供服务,当分区增长到配置的大小之后,如果开启了自动拆分(可以选择手动拆分,或建表时划分好分区)分区服务器会将会将这个分区拆分成两个,每一个分区会有一个分区名,格式为<表名,startrowkey,创建时间>,一个分区下每一个列簇都会有一个存储仓库。
每个store有且仅有一个memstore(内存仓库),,分区服务器处理写入请求时,数据的变更操作会先写入WAL,然后再写入MemStore,同时对在内存中对数据进行排序,当Memstore达到配置的大小,然后memstore会刷新为一个新的storeFile到磁盘,存储文件只会顺序写入,不支持修改。
数据块是Hbase中数据读取的最小单位,StoreFile由数据块组成,可以再建表时按列簇指定表数据的数据块大小。
Hmaster
负责监控集群中所有的分区服务器进程(HregionServer),负责所有所有元数据的更新,分区服务器的负载均衡。Hmaster通常与namenode节点放在一起
HRegionServer
管理其负责的分区,处理分区的读写请求,分区的split与分区的压缩(compact)。分区服务器尽量与DataNode放在同一节点。能做到本地读取减少网络的Io。
客户端读数据流程
client首先从Hmaster获取到元数据后缓存到本地,当分区操作抛出异常后,会重新刷新缓存,定位到操作数据对应的分区服务器之后,Hbase客户端将于对应的分区服务器进行交互。
WAL
默认情况下一个分区服务器只有一个WAL,客户端将数据请求操作先写入WAL,然后在写入内存仓库中,分区服务器宕机的时候可以根据WAL来回复分区服务器的状态。
HBase1.0版本引入了mutiWAL,允许分区服务器并发的写入多个WAL,不同的分区可以写入不同的WAL。
Store
每个分区的每一个列簇对应一个存储仓库,一个分区仓库包含一个memstore,和多个存储文件。当memstore达到阈值后,会刷新成一个存储文件,存储文件顺序写入,不支持修改,以Hfile的形式存储在Hadoop的Datanode中。
MemeStore
Memstore位于分区服务器的堆内存,数据在写入Memstore的时候即会按顺序按行键排序memstore相当于一个内存缓存,能够提供对新写入的数据的快速访问。
事务
Hbase支持行级事务,对一行数据的操作能够保证原子性
- Hbase对一行的操作,能够保证原子性,一个put操作会有一个返回值,成功失败或者超时,如果请求超时,那么put可能成功也可能失败,总而言之,要么全成功,要么全失败。
- Hbase不能够保证多行的更新操作的原子性,如一个批量操作Put,a,b,c3行数据,结果可能是 a,b成功,c失败
- CASAPI是一个原子操作
Hbase的事务是通过WAL
来实现的,数据写入时会先写入WAL,再写入MemStore.
隔离性
Hbase支持两种事务隔离级别,在org.apache.hadoop.hbase.client.IsolationLeve
立面定义了这两种隔离级别
- READ_COMMITTED
- READ_INCOMMITTED
Scan scan = new Scan()
scan.setIsolationLevel(IsolationLeve.READ_COMMITTED)
设置事务隔离级别
Hbase的读已提交是怎样实现的?
与mysql相同
HBase Shell
DDL
创建表
创建一个表,包含两个列簇
create `table_name`, {NAME => `cf1`} , {NAME => `cf2`}
查看表
list
查看建表
describe `table_name`
DATA_BLOCK_ENCODING:数据块编码。
BLOOMFILTER:布隆过滤器
REPLICATION_SCORE:集群建数据复制开关,当集群键的数据复制配置好之后,REPLICATION_SCORE=1的表会开启辅助,默认为0,表示不复制。
KEEP_DELETE_CELLS:保留了删除的数据,意味着可以通过Get或者Scan请求获得已经被删除了的数据(如果数据删除后经过一次主压缩,那么这些数据也会被删除),如果需要开启集群建复制,则这个属性必须为true,否则会导致数据复制失败。
VERSIONS:HBASE最多可以保存多少个副本
COMPRESSION:压缩方式。
TTL:数据的有效时间,超过有效时长的数据在主压缩的时候回被删除。
BLOCKSize:Hbase读取数据的最小单元,默认为64kb。
修改表
修改表的模式之前需要将表先下线,然后执行修改命令,
disable `table_name`
alter `table_name` ,{NAME => "cf1",PRELICATION_SCORE=>"1",KEEP_DELETED_CELLS => "TRUE"}
enable `table_name`
5 模式设计
Hbase在设计表的同事应该考虑如何设计行键以提高查询效率,行键在Hbase中充当表的一级索引角色,并且Hbase本身没有提供二级索引机制,因此对行键的设计优化很重要。
行键设计
Hbase数据按照行键自然排序,对扫描操作是一个优化,行键也是Hbase最有效的索引。
设计原则:
1. 唯一原则:行键对应关系型数据库的唯一键
2. 长度原则:长度适中,建议使用定长
3. 散列原则:避免递增,否则读写负载会集中在某个热点分区。
规避热点区间
反转补齐,类似的如果使用时间戳作为行键则可以使用Long.MAX_VALUE-时间戳
这样最新时间的数据行键较小,能排在数据存储文件前列,且做到了分布均匀。
6.客户端API
maven依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-protocol</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-common</artifactId>
<version>1.2.6</version>
</dependency>
Hbase客户端使用Protocol与Hbase服务端进行交互。
Protocol是一种轻便搞笑的结构化数据存储格式。
创建客户端连接
package com.mt.hbase.connection;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import java.io.IOException;
public class HBaseConnectionFactory {
private static Connection connection = null;
static{
createConnection();
}
private static synchronized void createConnection() {
Configuration configuration = HBaseConfiguration.create();
configuration.set("hbase.client.pause", "100");
configuration.set("hbase.client.write.buffer", "10485760");
configuration.set("hbase.client.retries.number", "5");
configuration.set("hbase.zookeeper.property.clientPort", "2181");
configuration.set("hbase.client.scanner.timeout.period", "100000");
configuration.set("hbase.rpc.timeout", "40000");
configuration.set("hbase.zookeeper.quorum", "master1,master2,slave1");
try {
connection = ConnectionFactory.createConnection(configuration);
}catch(IOException ioException){
throw new RuntimeException(ioException);
}
}
public static Connection getConnection() {
return connection;
}
}
数据定义语言
表管理
package com.mt.hbase.chpt06.clientapi.ddl;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.io.encoding.DataBlockEncoding;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class TableDemo {
/**
* 创建表
* @param tableName 表名
* @param familyNames 列族名
* @return
*/
public boolean createTable(String tableName, String... familyNames) throws Exception {
Admin admin = HBaseConnectionFactory.getConnection().getAdmin();
if (admin.tableExists(TableName.valueOf(tableName))) {
return false;
}
//通过HTableDescriptor类来描述一个表,HColumnDescriptor描述一个列族
HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf(tableName));
for (String familyName : familyNames) {
HColumnDescriptor oneFamily= new HColumnDescriptor(familyName);
//设置行键编码格式
oneFamily.setDataBlockEncoding(DataBlockEncoding.PREFIX_TREE);
//设置数据保留多版本
oneFamily.setMaxVersions(3);
tableDescriptor.addFamily(oneFamily);
}
//设置
admin.createTable(tableDescriptor);
return true;
}
/**
* 删除表
* @param tableName 表名
* @return
*/
public boolean deleteTable(String tableName) throws Exception {
Admin admin = HBaseConnectionFactory.getConnection().getAdmin();
if (!admin.tableExists(TableName.valueOf(tableName))) {
return false;
}
//删除之前要将表disable
if (!admin.isTableDisabled(TableName.valueOf(tableName))) {
admin.disableTable(TableName.valueOf(tableName));
}
admin.deleteTable(TableName.valueOf(tableName));
return true;
}
public static void main(String[] args) throws Exception {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
TableDemo tableDemo = new TableDemo();
//新建表
System.out.println(tableDemo.createTable("s_behavior"+format.format(calendar.getTime()),"pc","ph"));
calendar.add(Calendar.MONTH,-1);
//删除表
System.out.println(tableDemo.deleteTable("s_behavior"+format.format(calendar.getTime())));
}
}
分区管理
怎样触发自动压缩。
package com.mt.hbase.chpt06.clientapi.ddl;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.HRegionInfo;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.MetaTableAccessor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.protobuf.generated.AdminProtos;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class AdminDemo {
private static ExecutorService executors = Executors.newFixedThreadPool(20);
public void compact(String table) throws Exception {
Admin admin = HBaseConnectionFactory.getConnection().getAdmin();
Map<String, List<byte[]>> serverMap = new HashMap<String, List<byte[]>>();
List<HRegionInfo> regionInfos = admin.getTableRegions(TableName.valueOf(table));
//将表所有的Region按所在RegionServer分组,这样可以并发在每个RegionServer compact一个Region
for (HRegionInfo hRegionInfo : regionInfos) {
HRegionLocation regionLocation = MetaTableAccessor
.getRegionLocation(HBaseConnectionFactory.getConnection(), hRegionInfo);
if (serverMap.containsKey(regionLocation.getHostname())) {
serverMap.get(regionLocation.getHostname()).add(hRegionInfo.getRegionName());
} else {
List<byte[]> list = new ArrayList<byte[]>();
list.add(hRegionInfo.getRegionName());
serverMap.put(regionLocation.getHostname(), list);
}
}
List<Future<String>> futures = new ArrayList<Future<String>>();
//为每个RegionServer compact一个Region
for (Map.Entry<String, List<byte[]>> entry : serverMap.entrySet()) {
futures.add(executors.submit(new HBaseCompactThread(entry)));
}
for (Future<String> future : futures) {
System.out.println("compact results: " + future.get());
}
}
class HBaseCompactThread implements Callable<String> {
private Map.Entry<String, List<byte[]>> entry;
public HBaseCompactThread(Map.Entry<String, List<byte[]>> entry) {
this.entry = entry;
}
public String call() throws Exception {
Admin admin = HBaseConnectionFactory.getConnection().getAdmin();
for (byte[] bytes : entry.getValue()) {
AdminProtos.GetRegionInfoResponse.CompactionState state = admin
.getCompactionStateForRegion(bytes);
//如果Region当前状态不为主压缩,则触发主压缩
if (state != AdminProtos.GetRegionInfoResponse.CompactionState.MAJOR) {
admin.majorCompactRegion(bytes);
}
while (true) {
//休眠等待当前Region compact结束,以免compact过多Region造成RegionServer压力过大
Thread.sleep(3 * 60 * 1000);
state = admin.getCompactionStateForRegion(bytes);
if (state == AdminProtos.GetRegionInfoResponse.CompactionState.NONE) {
break;
}
}
}
return entry.getKey() + " success";
}
}
public static void main(String[] args) throws Exception {
AdminDemo adminDemo = new AdminDemo();
adminDemo.compact("s_behavior");
}
}
DML
Put
package com.mt.hbase.chpt06.clientapi.dml;
import com.mt.hbase.chpt05.rowkeydesign.RowKeyUtil;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class PutDemo {
private static final String TABLE="s_behavior";
private static final String CF_PC="pc";
private static final String CF_PHONE="ph";
private static final String COLUMN_VIEW="v";
private static final String COLUMN_ORDER="o";
private static final String[] ITEM_ID_ARRAY = new String[]{"1001","1002","1004","1009"};
private static final long userId = 12345;
private static RowKeyUtil rowKeyUtil = new RowKeyUtil();
public static void main(String[] args) throws IOException, InterruptedException {
List<Put> actions = new ArrayList<Put>();
Random random = new Random();
for (int i = 0; i < ITEM_ID_ARRAY.length; i++) {
String rowkey = generateRowkey(userId,System.currentTimeMillis(),i);
Put put = new Put(Bytes.toBytes(rowkey));
//添加列
put.addColumn(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_VIEW),
Bytes.toBytes(ITEM_ID_ARRAY[i]));
if(random.nextBoolean()){
put.addColumn(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_ORDER),
Bytes.toBytes(ITEM_ID_ARRAY[i]));
}
actions.add(put);
}
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
//设置不启用客户端缓存,直接提交
((HTable)table).setAutoFlush(true,false);
//方法一:向Table写入数据
table.put(actions);
// Object[] results = new Object[actions.size()];
//方法二:执行table的批量操作,actions可以是Put、Delete、Get、Increment等操作,并且有可以获取执行结果
// table.batch(actions, results);
//如果启用了客户端缓存,也可以执行flushCommits显示提交
// ((HTable) table).flushCommits();
}
private static String generateRowkey(long userId, long timestamp, long seqId){
return rowKeyUtil.formatUserId(userId)+ rowKeyUtil.formatTimeStamp(timestamp)+seqId;
}
}
Get
package com.mt.hbase.chpt06.clientapi.dml;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class GetDemo {
private static final String TABLE="s_behavior";
private static final String CF_PC="pc";
private static final String CF_PHONE="ph";
private static final String COLUMN_VIEW="v";
private static final String COLUMN_ORDER="o";
public static void main(String[] args) throws IOException, InterruptedException,
ParseException {
List<Get> gets = new ArrayList<Get>();
Get oneGet = new Get(Bytes.toBytes("54321000000000000000092233705146317032071"));
//设置需要Get的数据列族
oneGet.addFamily(Bytes.toBytes(CF_PC));
//设置需要Get的数据列
//oneGet.addColumn(Bytes.toBytes(CF_PHONE),Bytes.toBytes(COLUMN_ORDER));
//设置Get的数据时间范围为2018-01-01到现在
String startS = "2018-01-01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = dateFormat.parse(startS);
oneGet.setTimeRange(startDate.getTime(),System.currentTimeMillis());
//设置Get的数据版本为2
oneGet.setMaxVersions(2);
gets.add(oneGet);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
Result[] results = table.get(gets);
for(Result result : results){
if (null != result.getRow()) {
Cell[] cells = result.rawCells();
System.out.println("rowkey="+ Bytes.toString(result.getRow()));
for (Cell cell : cells) {
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println("qualifier="+ qualifier+",value="+value);
}
}
}
/**
* 输出结果如下所示:
* rowkey=54321000000000000000092233705146317032071
* qualifier=o,value=1002
* qualifier=v,value=1002
*/
}
}
Scan
package com.mt.hbase.chpt06.clientapi.dml;
import com.mt.hbase.chpt05.rowkeydesign.RowKeyUtil;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ScanDemo extends BaseDemo{
private static final long userId = 12345;
public final static String MIN_TIME = "0000000000000000000";
public final static String MAX_TIME = "9999999999999999999";
private static RowKeyUtil rowKeyUtil = new RowKeyUtil();
public static void main(String[] args) throws IOException, InterruptedException,
ParseException {
Scan scan = new Scan();
//设置需要scan的数据列族
scan.addFamily(Bytes.toBytes(CF_PC));
//设置需要scan的数据列
//scan.addColumn(Bytes.toBytes(CF_PHONE),Bytes.toBytes(COLUMN_ORDER));)
//设置small scan以提高性能,如果扫描的数据在一个数据块内,则应该设置为true
scan.setSmall(true);
//设置扫描开始行键,结果包含开始行
scan.setStartRow(Bytes.toBytes(rowKeyUtil.formatUserId(userId) + MIN_TIME));
//设置扫描结束行键,结果不包含结束行
scan.setStopRow(Bytes.toBytes(rowKeyUtil.formatUserId(userId) + MAX_TIME));
//设置事务隔离级别
scan.setIsolationLevel(IsolationLevel.READ_COMMITTED);
//设置每次RPC请求读取数据行
scan.setCaching(100);
//设置scan的数据时间范围为2018-01-01到现在
String startS = "2018-01-01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = dateFormat.parse(startS);
scan.setTimeRange(startDate.getTime(),System.currentTimeMillis());
//设置scan的数据版本为2
scan.setMaxVersions(2);
//设置是否缓存读取的数据块,如果数据会被多次读取则应该设置为true,如果数据仅会被读取一次则应该设置为false
scan.setCacheBlocks(false);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
}
}
delete
package com.mt.hbase.chpt06.clientapi.dml;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
public class DeleteDemo {
private static final String TABLE="s_behavior";
private static final String CF_PC="pc";
private static final String CF_PHONE="ph";
private static final String COLUMN_VIEW="v";
private static final String COLUMN_ORDER="o";
public static void main(String[] args) throws IOException, InterruptedException {
String rowkeyToDelete = "54321000000000000000092233705146317032071";
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
Get oneGet = new Get(Bytes.toBytes(rowkeyToDelete));
Result result = table.get(oneGet);
/**
* 下行输出如下:
* lineNo=1,qualifier=o,value=1002
* lineNo=1,qualifier=v,value=1002
*/
printResult(result,"1");
Delete deleteColumn = new Delete(Bytes.toBytes(rowkeyToDelete));
//设置需要删除的列
deleteColumn.addColumn(Bytes.toBytes(CF_PC),Bytes.toBytes(COLUMN_VIEW));
//设置需要删除一天之前的数据版本
deleteColumn.setTimestamp(System.currentTimeMillis() - 24*60*60*1000);
table.delete(deleteColumn);
oneGet = new Get(Bytes.toBytes(rowkeyToDelete));
result = table.get(oneGet);
/**
* 下行输出如下:
* lineNo=2,qualifier=o,value=1002
*/
printResult(result,"2");
Delete deleteFamily = new Delete(Bytes.toBytes(rowkeyToDelete));
//设置需要删除的列族
deleteFamily.addFamily(Bytes.toBytes(CF_PC));
table.delete(deleteFamily);
oneGet = new Get(Bytes.toBytes(rowkeyToDelete));
result = table.get(oneGet);
/**
* 下行输出为空
*/
printResult(result,"3");
Delete deleteRow = new Delete(Bytes.toBytes(rowkeyToDelete));
//删除整行
table.delete(deleteRow);
oneGet = new Get(Bytes.toBytes(rowkeyToDelete));
result = table.get(oneGet);
/**
* 下行输出为空
*/
printResult(result,"4");
}
private static void printResult(Result result, String lineNo) {
Cell[] cells = result.rawCells();
for (Cell cell : cells) {
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println("lineNo="+lineNo+ ",qualifier="+ qualifier+",value="+value);
}
}
}
Increment
Increment提供一个递增的计数器功能,该计数器的更新操作会获取行锁,能保证计数器递增的原子性。但是Get操作不会获取行锁,因此Increment的结果不会马上被读取,只能保证最终结果的一致性。
package com.mt.hbase.chpt06.clientapi.dml;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Increment;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
import java.util.ArrayList;
import java.util.List;
public class IncrDemo {
private static final String TABLE = "s_behavior";
private static final String CF_PC = "pc";
private static final String COLUMN_FOR_INCR= "i";
public static void main(String[] args) throws InterruptedException {
List<Thread> incrThreadList = new ArrayList<Thread>();
for(int i=0; i < 20; i++){
IncrThread incrThread = new IncrThread();
Thread t = new Thread(incrThread);
t.setName("Thread_"+ i);
incrThreadList.add(t);
t.start();
}
for(Thread incrThread: incrThreadList){
incrThread.join();
}
}
static class IncrThread implements Runnable{
@Override public void run() {
try {
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
Increment increment = new Increment(Bytes.toBytes("rowkeyforincr"));
increment.addColumn(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_FOR_INCR), 1);
table.increment(increment);
Get oneGet = new Get(Bytes.toBytes("rowkeyforincr"));
Result getResult = table.get(oneGet);
Cell[] getCells = getResult.rawCells();
for (Cell cell : getCells) {
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
Long value = Bytes.toLong(CellUtil.cloneValue(cell));
System.out.println(Thread.currentThread().getName()+": qualifier=" + qualifier + ",value=" + value);
}
}catch(Exception e){
System.out.println("incr failed"+ e.getMessage());
}
}
}
}
过滤器
过滤器可以根据行键,列簇,列,时间戳等条件来对数据进行过滤,其中行键的过滤效果最高。
BaseDemo
package com.mt.hbase.chpt06.clientapi;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
public class BaseDemo {
protected static final String TABLE = "s_behavior";
protected static final String CF_PC="pc";
protected static final String CF_PHONE="ph";
protected static final String COLUMN_VIEW="v";
protected static final String COLUMN_ORDER="o";
protected static void printResult(ResultScanner resultScanner) throws IOException {
Result result = null;
while ((result = resultScanner.next()) != null) {
if (result.getRow() == null) {
continue;// keyvalues=NONE
}
Cell[] cells = result.rawCells();
System.out.println("rowkey=" + Bytes.toString(result.getRow()));
for (Cell cell : cells) {
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.println("qualifier=" + qualifier + ",value=" + value);
}
}
}
}
KeyOnlyFilter
该过滤器使查询结果只返回行键
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.KeyOnlyFilter;
public class KeyOnlyFilterDemo extends BaseDemo {
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
/**
* 查询表s_behavior所有数据的行键
*/
scan.setFilter(new KeyOnlyFilter());
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008861
qualifier=v,value=
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=
qualifier=v,value=
rowkey=54321000000000000000092233705133122008863
qualifier=o,value=
qualifier=v,value=
rowkey=54321000000000000000092233705133122009210
qualifier=o,value=
qualifier=v,value=
*/
}
}
FirstKeyOnlyFilter
该过滤器使得查询结果只返回每行的第一个单元值。通常与KeyOnlyFilter结合在一起来执行搞笑的行统计操作。
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter;
public class FirstKeyOnlyFilterDemo extends BaseDemo {
private static final String TABLE = "s_behavior";
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
/**
* 查询表s_behavior所有数据,每行只返回第一列,通常与KeyOnlyFilter一起使用
*/
scan.setFilter(new FirstKeyOnlyFilter());
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008861
qualifier=v,value=1002
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=1004
rowkey=54321000000000000000092233705133122008863
qualifier=o,value=1009
rowkey=54321000000000000000092233705133122009210
qualifier=o,value=1001
*/
}
}
PrefixFilter
该过滤器用来匹配行键包含指定的前缀的数据。
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt05.rowkeydesign.RowKeyUtil;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.PrefixFilter;
import org.apache.hadoop.hbase.util.Bytes;
public class PrefixFilterDemo extends BaseDemo {
private static final String TABLE = "s_behavior";
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
RowKeyUtil rowKeyUtil = new RowKeyUtil();
/**
* 查询用户12345的所有数据
*/
PrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes(rowKeyUtil.formatUserId(12345)));
scan.setFilter(prefixFilter);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
}
}
RowFilter
该过滤器通过行键来匹配满足条件的数据行,例如使用BinaryConpactor可以查询具有某个行键的数据行。
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt05.rowkeydesign.RowKeyUtil;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.BinaryComparator;
import org.apache.hadoop.hbase.filter.CompareFilter;
import org.apache.hadoop.hbase.filter.RowFilter;
import org.apache.hadoop.hbase.util.Bytes;
import java.text.SimpleDateFormat;
import java.util.Date;
public class RowFilterDemo extends BaseDemo {
public static void main(String[] args) throws Exception {
RowKeyUtil rowKeyUtil = new RowKeyUtil();
Scan scan = new Scan();
/**
* 查询用户ID12345,2018-01-01之后的数据
*/
String startS = "2018-01-01";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date startDate = dateFormat.parse(startS);
String rowKey = rowKeyUtil.formatUserId(12345)+rowKeyUtil.formatTimeStamp(startDate.getTime());
RowFilter rowFilter = new RowFilter(CompareFilter.CompareOp.LESS, new BinaryComparator(
Bytes.toBytes(rowKey)));
scan.setFilter(rowFilter);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008861
qualifier=v,value=1002
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=1004
qualifier=v,value=1004
rowkey=54321000000000000000092233705133122008863
qualifier=o,value=1009
qualifier=v,value=1009
rowkey=54321000000000000000092233705133122009210
qualifier=o,value=1001
qualifier=v,value=1001
*/
}
}
SingleColumnValueFilter
该过滤器类似于关系型数据库的where条件语句,通过怕暖数据行指定的列限定符对应的值是否匹配指定的条件,来决定是否将改行数据返回。
可以使用SingleColumnValueFilter.setFilterIfMissing(true)
可以用来决定查询结果是否返回不包含指定字段的数据行
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.CompareFilter;
import org.apache.hadoop.hbase.filter.SingleColumnValueFilter;
import org.apache.hadoop.hbase.util.Bytes;
public class SingleColumnValueFilterDemo extends BaseDemo {
private static final String TABLE = "s_behavior";
private static final String CF_PC="pc";
private static final String CF_PHONE="ph";
private static final String COLUMN_VIEW="v";
private static final String COLUMN_ORDER="o";
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
/**
* 查询表s_behavior 列限定符'o',值为1004的数据
*/
SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_ORDER),
CompareFilter.CompareOp.EQUAL, Bytes.toBytes("1004"));
/**
* 如果数据行不包含列限定符'o' 则不返回该行
* 如果设置为false,则会返回不包含列限定符'o'的数据行
*/
singleColumnValueFilter.setFilterIfMissing(true);
scan.setFilter(singleColumnValueFilter);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=1004
qualifier=v,value=1004
*/
}
}
TimeStampFilter
该过滤器可以用来过来吧某个时间戳的数据,可以与Get和Scan一起使用。
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.TimestampsFilter;
import java.util.ArrayList;
import java.util.List;
public class TimestampsFilterDemo extends BaseDemo {
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
/**
* 查询表s_behavior hbase时间戳为1523541994550的数据
*/
List<Long> timeStampList = new ArrayList<Long>();
timeStampList.add(1523541994550L);
TimestampsFilter timestampsFilter = new TimestampsFilter(timeStampList);
scan.setFilter(timestampsFilter);
/**
也可以使用如下命令替换
scan.setTimeStamp(1523541994550L);
scan.setTimeRange(1523541994550L,1524550412164L);
*/
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008861
qualifier=v,value=1002
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=1004
qualifier=v,value=1004
rowkey=54321000000000000000092233705133122008863
qualifier=o,value=1009
qualifier=v,value=1009
rowkey=54321000000000000000092233705133122009210
qualifier=o,value=1001
qualifier=v,value=1001
*/
}
}
ValueFileter
该过滤器使用单元格的值来过滤数据,只有满足指定条件,指定值得单元格才会被返回,与SingleColumnValueFilter不同的是后者需要指定匹配的列限定符并且返回整行数据,
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.BinaryComparator;
import org.apache.hadoop.hbase.filter.CompareFilter;
import org.apache.hadoop.hbase.filter.ValueFilter;
import org.apache.hadoop.hbase.util.Bytes;
public class ValueFilterDemo extends BaseDemo {
private static final String TABLE = "s_behavior";
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
/**
* 查询表s_behavior包含商品1001记录的数据
* 注意如果一行有多列数据,只有值等于1001数据的一列会返回
*/
ValueFilter valueFilter = new ValueFilter(CompareFilter.CompareOp.EQUAL, new BinaryComparator(
Bytes.toBytes("1001")));
scan.setFilter(valueFilter);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122009210
qualifier=o,value=1001
qualifier=v,value=1001
*/
}
}
WhileMatchFilter
该过滤器与SkipFilter类似,区别为WhileMatchFilter
当条件不满足时即会停止,SkipFilter
当包装条件满足时,跳过该满足条件的数据行。
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.*;
import org.apache.hadoop.hbase.util.Bytes;
public class WhileMatchFilterDemo extends BaseDemo {
public static void main(String[] args) throws Exception {
Scan scan = new Scan();
/**
* 查询表s_behavior
* 相当于while执行,当条件不满足了即返回结果
* 如下过滤条件相当于遇到1009的数据内容就返回
*/
WhileMatchFilter whileMatchFilter = new WhileMatchFilter(new ValueFilter(CompareFilter.CompareOp.NOT_EQUAL,
new BinaryComparator(Bytes.toBytes("1009"))));
scan.setFilter(whileMatchFilter);
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
ResultScanner resultScanner = table.getScanner(scan);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008861
qualifier=v,value=1002
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=1004
qualifier=v,value=1004
*/
}
}
FilterList
FilterList的构造函数设计两个参数,一个是包装的过滤器的列表,另一个是列表的操作关系。包括FilterList.Operator.MUSR_PASS_ALL
和FilterList.Operator.MUST_PASS_ONE
前者表示数据行需要满足包装的过滤器列表的所有过滤关系,后者表示只需要满足包装的过滤器的列表中任意一个过滤器的的过滤条件
package com.mt.hbase.chpt06.clientapi.filter;
import com.mt.hbase.chpt06.clientapi.BaseDemo;
import com.mt.hbase.connection.HBaseConnectionFactory;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.filter.*;
import org.apache.hadoop.hbase.util.Bytes;
import java.util.ArrayList;
import java.util.List;
public class FilterListDemo extends BaseDemo {
public static void main(String[] args) throws Exception {
Table table = HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
/**
* 查询表s_behavior满足如下条件的数据行
* 1.列限定符 pc:o 值等于 1004
* 2.列数据时间戳等于1523541994550L
* 3.只返回数据的行键,不返回列数据值
*/
List<Filter> filters = new ArrayList<Filter>();
SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_ORDER),
CompareFilter.CompareOp.EQUAL, Bytes.toBytes("1004"));
List<Long> timeStampList = new ArrayList<Long>();
timeStampList.add(1523541994550L);
TimestampsFilter timestampsFilter = new TimestampsFilter(timeStampList);
filters.add(singleColumnValueFilter);
filters.add(timestampsFilter);
filters.add(new KeyOnlyFilter());
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL, filters);
Scan scanOnlyKey = new Scan();
scanOnlyKey.setFilter(filterList);
ResultScanner resultScanner = table.getScanner(scanOnlyKey);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=
qualifier=v,value=
*/
filters.clear();
/**
* 查询表s_behavior满足如下条件的数据行
* 1.列限定符 pc:o 值等于 1004 或者 1002
* 2.列数据时间戳等于1523541994550L
* 3.只返回数据的行键,不返回列数据值
*/
SingleColumnValueFilter singleColumnValueFilter2 = new SingleColumnValueFilter(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_ORDER),
CompareFilter.CompareOp.EQUAL, Bytes.toBytes("1002"));
filters.add(singleColumnValueFilter);
filters.add(singleColumnValueFilter2);
FilterList filterListAll = new FilterList(FilterList.Operator.MUST_PASS_ALL,
timestampsFilter,
new FilterList(FilterList.Operator.MUST_PASS_ONE, filters),
new KeyOnlyFilter());
Scan scanWithValue = new Scan();
scanWithValue.setFilter(filterListAll);
resultScanner = table.getScanner(scanWithValue);
printResult(resultScanner);
/**
输出结果:
rowkey=54321000000000000000092233705133122008861
qualifier=v,value=1002
rowkey=54321000000000000000092233705133122008862
qualifier=o,value=1004
qualifier=v,value=1004
*/
}
}
7.架构实现
存储
二叉树其查找时间复杂度与树的深度有关,当数据量很大,树的深度很深时,二叉树搜索需要访问的节点就会很多,这些节点都存储在磁盘,树的深度越深,搜索就越慢。
B树平衡树在二叉树的基础上优化而来,主要是的目的是降低树的深度,减少磁盘的IO.B树将二叉树改为多叉树,在每个节点上存储指针的信息,降低磁盘IO次数。
B+树是B树的一种变体,其区别如下
- 叶子节点包含了全量的索引数据,一般会存储一个指向数据的指针。
- 叶子节点按照索引的顺序从小到大连接起来
- 所有的非叶子节点包含了一部分的索引数据,节点的索引数据为其子节点中多大或者最小的索引数据
为什么B+树比B树更适合做索引?
因为B+树只要遍历所有叶子节点就可以实现区间扫描而无需回溯到父节点,B+树最大的性能问题是插入,随着越来越多的数据的插入,叶子节点会慢慢分类,逻辑上连续的叶子节点可能会存储到不同的块,做区间扫描时可能产生大量的随机读IO.同时数据接入时需要维护树的分裂合并。
LSM树
HBase引入了LSM树,LSM树的核心思想是将一棵大树,拆分为多棵小树,Hbase数据写入都会先写入MemStore,在内存中构建一棵有序的小树,当MemStore达到一定条件时即会刷新输出到磁盘为一个StoreFile,此时的数据已经是有序并且顺序写入磁盘的,所以写入速度会很快,这里的弊端是随着数据量的增大,StoreFile(小树)会越来越多,数据查询的时候需要扫描很多的文件。
解决方法为,当MemStore刷新后,StoreFile达到配置的数量或者距离上次压缩的时间满足配置的间隔时,Hbase会自动触发压缩(分为MINOR和MAJOR两种压缩类型)
1.minor合并(minor compaction)
将多个小文件(通过参数配置决定是否满足合并的条件)重写为数量较少的大文件,减少存储文件数量(多路归并),因为hfile的每个文件都是经过归类的,所以合并速度很快,主要受磁盘IO性能影响
2.major合并(major compaction)
将一个region中的一个列簇的若干个hfile重写为一个新的hfile。而且major合并能扫描所有的键/值对,顺序重写全部数据,重写过程中会略过做了删除标记的数据(超过版本号限制、超过生存时间TTL、客户端API移除等数据)
LSM的核心思想是牺牲一定的读性能来换取写能力的最大化,可以使用布隆过滤器在提高读性能。
LSM为了提高写性能,数据线写入内存,如果服务器宕机或者断电,就会导致内存数据丢失,为了解决该问题,Hbase引入了WAL.
WAL
WAL即预写日志,由于LSM为了提高写性能,数据先写入内存,如果服务器宕机或者断电就会导致内存数据的丢失(WAL就像Mysql的redo log)Hbase在写入内存之前需要写入WAL以便异常恢复
WAL有以下几种持久化类型org.apache.hadoop.hbase.client.Dureaility
- SYNC_WAL 将数据的修改以同步的方式写入WAL,并且刷新到文件系统,
- USE_DEFAULT 使用默认的持久化策略,Hbase全局默认的持久化策略为Sync_WAL
- SKIP_WAL 不写WAL,
- ASYNC_WAL:将数据的修改以异步的方式写入WAL,与SYNC_WAL相比性能会有所提升,客户端无需等待WAL写入完成后返回。
Hbase 分区服务器对数据的插入、删除等每一个键值得修改都会封装到WALEdit
,Hbase仅支持行级事务,因此一行数据的多列的修改均会封装到同一个WALEdit
WAL写入流程(待整理)
数据写入流程
- Hbase 客户端根据连接配置的Zookeeper地址获取到znode /hbase/meta-region-server 的值,即hbase:meta分区锁在的分区服务器,注意
hbase:meta
有且仅有一个分区,获取地址后客户端会缓存该地址 - Hbase客户端根据第一步获取到的分区服务器的地址获取
hbase:meta
表的数据并缓存,hbase:meta
表数据列出了每个分区所在的分区服务器以及开始和结束行键,Hbase客户端会缓存这些信息。 - Hbase客户端将操作的数据根据行键所在的分区服务器分组,分别向对应的分区服务器发起RPC请求进行数据操作,由于前面已经缓存了分区的开始结束行键以及所在的分区服务器地址,因此之后的数据操作只需与对应的分区服务器交互,如果客户端捕捉到了IOException,此时Hbase客户端会清除缓存,重新来去分区位置信息。
Hbase客户端会将hbase:meta
表数据缓存在本地,因此大部分情况下前两步只有在客户端第一次做数据同步氢气的时候发生,因此对Zookeeper的压力很小。
8 协处理器
观察者协处理器(没用到过,用的时候再整理)
观察者协处理器类似于关系型数据库立面的触发器(在数据插入,删除之前或者之后这行)
端点类型协处理器(没用到过,用的时候再整理)
端点类型协处理器类似于关系型数据库里面的存储过程。
9. Hbase性能调优
客户端调优
- 设置客户端写入缓存
如果业务能容忍数据的丢失,如一些日志数据,那么在客户端写入HBase表的时候可以采用批量缓存的方式,将数据缓存在客户端,当达到配置的阈值的时候再批量提交到服务器端。为什么会有数据丢失呢?假如客户端的时候还没有提交到服务器,此时客户端宕机了,那么缓存的数据就会丢失。
List<Put> actions = new ArrayList<Put>();
Put put = new Put(Bytes.toBytes(generateRowkey(userId,System.currentTimeMillis(),1)));
put.addColumn(Bytes.toBytes(CF_PC), Bytes.toBytes(COLUMN_VIEW),
Bytes.toBytes("1001"));
actions.add(put);
HTable table = (HTable)HBaseConnectionFactory.getConnection().getTable(TableName.valueOf(TABLE));
table.setAutoFlushTo(false);
table.setWriteBufferSize(1024 * 1024 * 10);// 缓存大小10M
Object[] results = new Object[actions.size()];
table.batch(actions, results);
-
连接级别设置缓存
conf.set("hbase.client.write.buffer","10485760")
-
设置合适的扫描缓存
Scan操作一般需要查询大量的数据,如果一次Rpc请求就将所有的数据都加载到客户端,则请求的时间会被拉长,同时由于数据量大,网络传输也容易出错,因此Hbase Scan APi提供了一个分批拉取数据缓存到客户端的功能。
Scan scan = new Scan(1000);
scan.setCaching(1000)
- 跳过WAL写入
可以跳过WAL写入以提高写入速度。
Put.setWriteToWal(false)
Delete.setWriteToWal(false)
-
设置重试次数与间隔
当Hbase客户端请求在服务端出错抛出异常后,如果抛出的异常不是DoNotRetryIoException
类的子类,那么客户端会发起重试。重试的时间与最大次数需要配置合理,否则容易造成分区服务器请求过多,导致服务器性能降低。hbase.client.pause:重试的休眠时间
hbase.client.retries.number:最大重试次数,默认为35,建议减少,如5s
休眠时间为休眠时间乘以一个系数x[times]
x为一个数组[1,2,3,5,10,20,40,100,100,100,100,100,200,200] -
选用合适的过滤器
过滤器尽量使用字节比较器,因为Hbase存储数据以字节形式存储
服务端调优
分区大小不宜太大,分区的大小一般在10G-50G之间,太大的分区一旦发生拆分,将非常耗服务器的性能
一个表尽量只涉及一个列簇,一个表不应该包含很多列簇,因为MemStore的刷新与StoreFile的压缩式分区级别的。
多个列簇如果数据量差异过多,数据量小的列簇Scan操作会比较低效,会跨域很多分区服务器。
- 建表语句优化
- 使用数据块编码
DATA_BLOCK_ENCODING
表示针对行键使用的数据块编码格式。常用的数据块编码为PREFIX
,当开启PREFIX
编码后Hbase数据存储文件中会添加一个保存着当前行键与上一行行键具有相同前缀字符的列。
Hbase还支持一下几种数据库编码
1. DIFF:对PREFIC的一种扩展,将PREFIX对键前缀的缩略扩展到值与时间戳,DIFF对读写性能会有比较大的影响,但是可以缓存更多的数据,一般不用
2. FAST_DIFF:对DIFF更快的实现,与DIFF区别不大
3. PREDIX_TREE,前缀树,这种编码对内存的使用率的提高(压缩了数据,相同的内存可以缓存更多的数据)前缀树牺牲编码速度来换取更快的随机读写速度。
-
使用布隆过滤器
布隆过滤器可以用来提高随机读的性能。
布隆过滤器的数据存在StoreFile的元数据中,开启布隆过滤器会有一定的存储以及内存的开销,在生产环境中使用布隆过滤器的开销微乎其微。 -
开启数据压缩
Hbase支持多种数据压缩方式来减少存储到HDFS的文件大小以解决磁盘空间。常用算法包括(GZIP,BZIP2,LZO,SNAPPY)压缩/解压是一个cpu操作,因此启用Hbase压缩会导致CPU使用率上升,数据压缩在写入MemStore之后,Memstore刷新输出到磁盘之前,对写的性能影响不大,但是读取数据时需要将数据块解压后才能读取以及放入缓存,因此对读性能有影响。
开启压缩分为两步在hadoop配置文件core-site.xml中添加 <property> <name>io.compression.codecs</name> <value> org.apache.hadoop.io.compress.GzipCodec, org.apache.hadoop.io.compress.DefaultCodec, org.apache.hadoop.io.compress.LzoCodec, org.apache.hadoop.io.compress.LzopCodec, org.apache.hadoop.io.compress.BZip2Codec, org.apache.hadoop.io.compress.SnappyCodec </value> </property> 建表的时候增加属性 COMPRESSION => 'SNAPPY'
-
设置合理的数据块大小
BLOCKSIZE属性决定了Hbase读取数据的最小块的大小。为了提升性能,理想情况下查询所需要扫描的数据都能够放到一个数据块,或者是数据块的整数倍。
HFile.java源码中有一段注释,推荐将数据块的大小设置为8KB-1MB,大的数据块比较适合顺序查询,但不适合随机查询(随机查询需要解压一个大的数据块),小的数据块适合做随机查询,但是需要更多的内存来保存数据块的索引,而且创建文件会比较慢因为每个数据块的结尾都要把撒所的数据流Flush到文件中去。
BLOCKSIZE => '131072'
-
预先分区
一般一个分区的StoreFile的大小可以在10-50GB,太大的StoreFile压缩或者拆分会对集群性能造成影响,加入存储数据在1TB左右,那么可以将分区设为50-100这样就能保证分区的大小合适。
DDL
SPLITS => ['1','2','3','4','5']
-
禁止分区自动拆分与压缩
默认情况下Hbase自动管理分区的拆分和主压缩(major compact)这样可以减少一些运维工作。
但是生产环境中主压缩时间过长可能影响服务器性能。-
禁止自动拆分
在hbase-site.xml中设置<property> <name>hbase.regionserver.regionSplitLimit</name> <value>1</value> </property>
-
禁止自动主压缩
- hbase.hregion.majorcompaction:该参数控制主压缩间隔的时间,默认为604800000ms,(7天),设置为0表示禁止自动主压缩。
- hbase.hstore.blockingStoreFiles:当Store的StoreFile数量超过该参数配置的值时,需要在刷新MemStore前先进行拆分或者压缩,除非等待超过
hbase.hstore.blockingWaitTimes
配置的时间,默认的配置时间为90000ms,因此需要适量调大该参数,一面Memstore刷新被阻塞进而影响写入操作,导致整个分区服务器异常。 - hbase.hstore.compactionThreshold:当Store的StoreFile数量大于等于该参数配置的值时,可能会触发压缩。默认值为3,如果配置的过大,可以推迟触发压缩的时间,但是会造成Store的StoreFile数量过大,影响查询的性能,一般设置为5以内。
-
-
开启Short Circuit Local Reads
默认情况下不管是本地读还是远程读,其实都需要经过一层DataNode的RPC调用,当开启Short Circuit Local Reads配置后,相当于Hadoop DFSClient 直接读取本地文件,而无需经过DataNode 。
Short Circuit Local Reads用带了Unix Domain Socket,它是一种进程间的通信方式,使得同一台机器上的两个进程能以Socket的方式通信。它带来的另一大好处是,利用它两个进程之间除了可传递普通数据外,还可以传递文件描述符。
假设机器上有两个用户,A和B,AY欧诺个由读取某个文件的权限而B没有,借助Unix Domain Socket,可以让A打开文件得到一个文件描述符,然后把文件描述符传递给B,B就能读取文件的内容了,即使B没有相应的权限。在HDFS的场景立面,A就是DataNode,B就是DFSClient,需要读取的文件就是DataNode数据目录的文件。
需要配置hdfs-site.xml即可开启Short Circuit Local Reads ,配置好之后需要重启DataNode与Hbase,并且创建一个空文件用来进程通信。<property> <name>dfs.client.read.shortcircuit</name> <value>true</value> </property> <property> <name>dfs.domain.socket.path</name> <value>/data/dn_socket</value>## 空文件 </property>
-
开启补偿重试读
当开启了
Short Circuit Local Reads
之后Hbase读取数据会优先从本地读取,某些情况下由于本都磁盘或者网络出了问题可能会导致短时间内的本地读失败,Hbase提供了补偿重试读。
当客户端发起一个本地读时,如果超时配置的时间还没有返回,客户端就会向数据副本所在的其他DataNode发送相同的数据请求,第一个返回的接受,其他的废弃。
在hbase-site.xml
中开启可重复读
<property>
<name>dfs.client.hedged.read.threadpool.size</name>## 并发补偿重试读线程池大小
<value>20</value>
</property>
<property>
<name>dfs.client.hedged.read.threshold.millis</name>## 补偿重试读开始前等待的时间,如果一个请求在该配置时间内还未返回,则发起重试读。
<value>5000</value>## 5000ms
</property>
- JVM调优
集群键的数据复制
目前只用过使用Export
和import
Hbase提供了Export Mapreduce 作业用来把Hbase的表导出为文件,然后使用Import Mapreduce作业来把文件导入同一个或者另外一个集群的Hbase表中。