hbase是
基于hdfs
进行数据的分布式存储,具有高可靠、高性能、列存储、可伸缩、实时读写的nosql数据库。
hbase可以存储海量的数据,并且后期查询性能很高,可以实现上亿条数据的查询秒级返回结果。 Hbase查询数据功能很简单,不支持join等复杂操作,不支持复杂的事务(行级的事务)
与hadoop一样,Hbase目标主要依靠
横向扩展
,通过不断增加廉价的商用服务器,来增加计算和存储能力。
每一行数据可以有截然不同的列
。列族
进行存储数据。为null的列并不占用实际的存储空间
。多个版本
,每一个数据都给一个版本号,这个版本号就是按照我们插入数据的时间戳去确定。字节数组
存储在hbase表中
列族
: 它是在hbase表中列上进行了划分 , 一个hbase表中有很多个列族 , 列族它是表的schema一部分 , 后期在创建hbase表的时候就需要指定有哪些列族 . 列族下的列不是表的schema一部分 , 在创建表的时候就不需要指定 , 后期插入数据的时候指定就可以了.
访问控制、磁盘和内存的使用统计都是在列族层面进行的。
列族越多,在取一行数据时所要参与IO、搜寻的文件就越多,所以,如果没有必要,不要设置太多的列族。一般设置2-3个比较合理。
rowkey
: 它是hbase表的行索引 , 它有一个特性 : hbase表中的数据是按照rowkey的字典序
进行排序 . 这个特性对于我们后期的rowkey的设计非常重要 . 这个特性,将经常一起读取的行存储放到一起
。(位置相关性) Row key行键 (Row key)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),在hbase内部,row key保存为字节数组。
cell
: 就是一个单元格 , 如图中绿框的就是一个cell . 一个cell包括了 : rowkey+列族+列族下的列的名称+version
前提条件
1、下载对应的安装包
2、规划安装目录
3、上传安装包到服务器中
4、解压安装包到指定的规划目录
tar -zxvf hbase-1.2.1-bin.tar.gz -C /export/servers
5、重命名解压目录
mv hbase-1.2.1 hbase
6、修改配置文件
需要把hadoop安装目录下/etc/hadoop文件夹中
core-site.xml
hdfs-site.xml
cp core-site.xml hdfs-site.xml /export/servers/hbase/conf
1、vim hbase-env.sh
#配置java环境变量
export JAVA_HOME=/export/servers/jdk
#指定hbase集群由外部的zk集群去管理,不在使用自带的zk集群
export HBASE_MANAGES_ZK=false
2、vim hbase-site.xml
<property>
<name>hbase.rootdirname>
<value>hdfs://node-1:9000/hbasevalue>
property>
<property>
<name>hbase.cluster.distributedname>
<value>truevalue>
property>
<property>
<name>hbase.zookeeper.quorumname>
<value>node-1:2181,node-2:2181,node-3:2181value>
property>
3、vim regionservers
#指定哪些节点是HRegionServer
node-2
node-3
4、vim backup-masters
#指定哪些节点是备用的Hmaster
node-2
7、配置hbase环境变量
vim /etc/profile
export HBASE_HOME=/export/servers/hbase
export PATH=$PATH:$HBASE_HOME/bin
8、分发hbase目录和环境变量
scp -r hbase node2:/export/servers
scp -r hbase node3:/export/servers
scp /etc/profile node2:/etc
scp /etc/profile node3:/etc
9、让所有hbase节点的环境变量生效
source /etc/profile
10、为了保证集群的可靠性 , 要启动多个HMaster[自己选择]
hbase-daemon.sh start master
注意:使用jdk8的时候,出现了Java HotSpot™ 64-Bit Server VM warning: ignoring
option MaxPermSize=256m; support was removed in 8.0的红色标识。字面意思是MaxPermSize不需要我们配置了,所以我就按照它的方法把default VM arguments中MaxPermSize参数给删掉就不会出现上面的提示了。
start-hbase.sh
stop-hbase.sh
hbase/bin/hbase shell 进入到hbase shell客户端命令操作
1、创建一个表
create 't_user_info','base_info','extra_info'
create 't1', {NAME => 'f1'}, {NAME => 'f2'}, {NAME => 'f3'}
2、查看有哪些表
list
类似于mysql表中sql:show tables
3、查看表的描述信息
describe 't_user_info'
4、修改表的属性
#修改列族的最大版本数
alter 't_user_info', NAME => 'base_info', VERSIONS => 3
5、添加数据到表中
put 't_user_info','00001','base_info:name','zhangsan'
put 't_user_info','00001','base_info:age','30'
put 't_user_info','00001','base_info:address','bejing'
put 't_user_info','00001','extra_info:school','shanghai'
put 't_user_info','00001','base_info:age','40'
put 't_user_info','00001','base_info:age','50'
put 't_user_info','00001','base_info:age','60'
put 't_user_info','00002','base_info:name','lisi'
6、查询表的数据
//按照条件查询
get 't_user_info','00001'
get 't_user_info','00001', {COLUMN => 'base_info'}
get 't_user_info','00001', {COLUMN => 'base_info:name'}
get 't_user_info','00001',{TIMERANGE => [1544243300660,1544243362660]}
get 't_user_info','00001',{COLUMN => 'base_info:age',VERSIONS =>3}
//查询某一时间段age的版本
get 't_user_info','00001',{COLUMN =>'base_info:age' ,TIMERANGE =>[1544271764711,1544272199406]}
//全表查询
scan 't_user_info'
7、删除数据
delete 't_user_info','00001','base_info:name'
//删除表中某一rowkey字段
deleteall 't_user_info','00001'
8、删除表
//首先需要将其置为不可用
disable 't_user_info'
drop 't_user_info'
过滤器查询 : 过滤器的类型很多,但是可以分为两大类——比较过滤器,专用过滤器
过滤器的作用是在服务端判断数据是否满足条件,然后只将满足条件的数据返回给客户端
hbase过滤器的比较运算符:
LESS <
LESS_OR_EQUAL <=
EQUAL =
NOT_EQUAL <>
GREATER_OR_EQUAL >=
GREATER >
NO_OP 排除所有
Hbase过滤器的比较器(指定比较机制):
BinaryComparator 按字节索引顺序比较指定字节数组,采用Bytes.compareTo(byte[])
BinaryPrefixComparator 跟前面相同,只是比较左端的数据是否相同
NullComparator 判断给定的是否为空
BitComparator 按位比较
RegexStringComparator 提供一个正则的比较器,仅支持 EQUAL 和非EQUAL
SubstringComparator 判断提供的子串是否出现在value中。
Hbase的过滤器分类
比较过滤器
Filter filter1 = new RowFilter(CompareOp.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes("row-22")));
scan.setFilter(filter1);
Filter filter1 = new FamilyFilter(CompareFilter.CompareOp.LESS, new BinaryComparator(Bytes.toBytes("colfam3")));
scan.setFilter(filter1);
filter = new QualifierFilter(CompareFilter.CompareOp.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes("col-2"))); scan.setFilter(filter1);
Filter filter = new ValueFilter(CompareFilter.CompareOp.EQUAL, new SubstringComparator(".4") );
scan.setFilter(filter1);
专用过滤器
SingleColumnValueFilter
----会返回满足条件的整行SingleColumnValueFilter filter = new SingleColumnValueFilter( Bytes.toBytes("colfam1"), Bytes.toBytes("col-5"), CompareFilter.CompareOp.NOT_EQUAL, new SubstringComparator("val-5"));
filter.setFilterIfMissing(true); //如果不设置为true,则那些不包含指定column的行也会返回 scan.setFilter(filter1);
PrefixFilter
----针对行键 Filter filter = new PrefixFilter(Bytes.toBytes("row1")); scan.setFilter(filter1);
ColumnPrefixFilter
Filter filter = new ColumnPrefixFilter(Bytes.toBytes("qual2")); scan.setFilter(filter1);
完全的代码开发
package com.itck.hbase;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.filter.*;
import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
import org.apache.hadoop.hbase.filter.FilterList.Operator;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
//todo:Hbase Api操作
public class HbaseDemo {
//初始化Configuration对象
private Configuration conf=null;
//初始化链接
private Connection conn = null;
@Before
public void init() throws Exception{
conf= HBaseConfiguration.create();
// 对于hbase的客户端来说,只需要知道hbase所使用的zookeeper集群地址就可以了
// 因为hbase的客户端找hbase读写数据完全不用经过hmaster
conf.set("hbase.zookeeper.quorum","node-1:2181,node-2:2181,node-3:2181");
//获取链接
conn=ConnectionFactory.createConnection(conf);
}
/**
* 建表
* @throws Exception
* hbase shell------> create 'tableName','列族1','列族2'
*/
@Test
public void createTable() throws Exception{
//获取一个表的管理器
Admin admin = conn.getAdmin();
//构造一个表描述器,并指定表名
HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf("t_user_info".getBytes()));
//构造一个列族描述器,并指定列族名
HColumnDescriptor hcd1 = new HColumnDescriptor("base_info");
// 构造第二个列族描述器,并指定列族名
HColumnDescriptor hcd2 = new HColumnDescriptor("extra_info");
// 为该列族设定一个版本数量
hcd2.setVersions(1, 3);
// 将列族描述器添加到表描述器中
tableDescriptor.addFamily(hcd1).addFamily(hcd2);
//利用表的管理器创建表
admin.createTable(tableDescriptor);
//关闭
admin.close();
conn.close();
}
/**
* 修改表
* @throws Exception
*/ //hbase shell alter 't_user_info' ,'base_info',
@Test
public void modifyTable() throws Exception{
//获取一个表的管理器
Admin admin = conn.getAdmin();
//获取表的描述器
HTableDescriptor tableDescriptor = admin.getTableDescriptor(TableName.valueOf("t_user_info"));
//修改已有的ColumnFamily---extra_info最小版本数和最大版本数
HColumnDescriptor hcd1 = tableDescriptor.getFamily("extra_info".getBytes());
hcd1.setVersions(2,5);
// 添加新的ColumnFamily
tableDescriptor.addFamily(new HColumnDescriptor("other_info"));
//表的管理器admin 修改表
admin.modifyTable(TableName.valueOf("t_user_info"),tableDescriptor);
//关闭
admin.close();
conn.close();
}
/**
* put添加数据
* @throws Exception hbase shell put 't_user_info','rk00001','base_info:name','lisi'
*/
@Test
public void testPut() throws Exception {
//构建一个 table对象,通过table对象来添加数据
Table table = conn.getTable(TableName.valueOf("t_user_info"));
//创建一个集合,用于存放Put对象
ArrayList<Put> puts = new ArrayList<Put>();
// 构建一个put对象(kv),指定其行键 例如hbase shell: put '表名','rowkey','列族:列名称','值'
Put put01 = new Put(Bytes.toBytes("user001")); // "user001".getBytes()
put01.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("zhangsan"));
Put put02 = new Put("user001".getBytes());
put02.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("password"), Bytes.toBytes("123456"));
Put put03 = new Put("user002".getBytes());
put03.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("lisi"));
put03.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("married"), Bytes.toBytes("false"));
Put put04 = new Put("zhang_sh_01".getBytes());
put04.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("zhang01"));
put04.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("married"), Bytes.toBytes("false"));
Put put05 = new Put("zhang_sh_02".getBytes());
put05.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("zhang02"));
put05.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("married"), Bytes.toBytes("false"));
Put put06 = new Put("liu_sh_01".getBytes());
put06.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("liu01"));
put06.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("married"), Bytes.toBytes("false"));
Put put07 = new Put("zhang_bj_01".getBytes());
put07.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("zhang03"));
put07.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("married"), Bytes.toBytes("false"));
Put put08 = new Put("zhang_bj_01".getBytes());
put08.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("username"), Bytes.toBytes("zhang04"));
put08.addColumn(Bytes.toBytes("extra_info"), Bytes.toBytes("married"), Bytes.toBytes("false"));
//把所有的put对象添加到一个集合中
puts.add(put01);
puts.add(put02);
puts.add(put03);
puts.add(put04);
puts.add(put05);
puts.add(put06);
puts.add(put07);
puts.add(put08);
//一起提交所有的记录
table.put(puts);
table.close();
conn.close();
}
/**
* 读取数据 get:一次读一行
* @throws Exception hbase shell : get 't_user_info',"rowkey"
*/
@Test
public void testGet() throws Exception {
//获取一个table对象
Table table = conn.getTable(TableName.valueOf("t_user_info"));
// 构造一个get查询参数对象,指定要get的是哪一行
Get get = new Get("user001".getBytes());
//返回查询结果数据
Result result = table.get(get);
//获取结果中的所有cell
List<Cell> cells = result.listCells();
//遍历所有的cell
for(Cell c:cells){
//获取行键
byte[] rowArray = c.getRowArray(); //00001
//获取列族
byte[] familyArray = c.getFamilyArray(); //base_info
//获取列族下的列名称
byte[] qualifierArray = c.getQualifierArray();//username
//列字段的值
byte[] valueArray = c.getValueArray(); // zhangsan
//打印rowArray、familyArray、qualifierArray、valueArray
// System.out.println(new String(rowArray));
// System.out.println(new String(familyArray));
// System.out.println(new String(qualifierArray));
// System.out.println(new String(valueArray));
//按指定位置截取,获取rowArray、familyArray、qualifierArray、valueArray
System.out.print(new String(rowArray, c.getRowOffset(), c.getRowLength()));
System.out.print(" "+new String(familyArray, c.getFamilyOffset(), c.getFamilyLength()));
System.out.print(":" + new String(qualifierArray, c.getQualifierOffset(), c.getQualifierLength()));
System.out.println(" " + new String(valueArray, c.getValueOffset(), c.getValueLength()));
}
//关闭
table.close();
conn.close();
}
/**
* scan 批量查询数据
* @throws Exception hbase shell scan 't_user_info'
*/
@Test
public void testScan() throws Exception {
//获取table对象
Table table = conn.getTable(TableName.valueOf("t_user_info"));
//获取scan对象
Scan scan = new Scan();
//获取查询的数据
ResultScanner scanner = table.getScanner(scan);
//获取ResultScanner所有数据,返回迭代器
Iterator<Result> iter = scanner.iterator();
//遍历迭代器
while (iter.hasNext()) {
//获取当前每一行结果数据
Result result = iter.next();
//获取当前每一行中所有的cell对象
List<Cell> cells = result.listCells();
//迭代所有的cell
for(Cell c:cells){
//获取行键
byte[] rowArray = c.getRowArray();
//获取列族
byte[] familyArray = c.getFamilyArray();
//获取列族下的列名称
byte[] qualifierArray = c.getQualifierArray();
//列字段的值
byte[] valueArray = c.getValueArray();
//打印rowArray、familyArray、qualifierArray、valueArray
System.out.println(new String(rowArray, c.getRowOffset(), c.getRowLength()));
System.out.print(new String(familyArray, c.getFamilyOffset(), c.getFamilyLength()));
System.out.print(":" + new String(qualifierArray, c.getQualifierOffset(), c.getQualifierLength()));
System.out.println(" " + new String(valueArray, c.getValueOffset(), c.getValueLength()));
}
System.out.println("-----------------------");
}
//关闭
table.close();
conn.close();
}
/**
* 删除表中的列数据
* @throws Exception hbase shell delete 't_user_info','user001','base_info:password'
*/
@Test
public void testDel() throws Exception {
//获取table对象
Table table = conn.getTable(TableName.valueOf("t_user_info"));
//获取delete对象,需要一个rowkey
Delete delete = new Delete("user001".getBytes());
//在delete对象中指定要删除的列族-列名称
delete.addColumn("base_info".getBytes(), "password".getBytes());
//执行删除操作
table.delete(delete);
//关闭
table.close();
conn.close();
}
/**
* 删除表
* @throws Exception hbase shell disable 't_user_info' drop 't_user_info'
*/
@Test
public void testDrop() throws Exception {
//获取一个表的管理器
Admin admin = conn.getAdmin();
//删除表时先需要disable,将表置为不可用,然后在delete
admin.disableTable(TableName.valueOf("t_user_info"));
admin.deleteTable(TableName.valueOf("t_user_info"));
admin.close();
conn.close();
}
/**
* 过滤器使用
* @throws Exception
*/
@Test
public void testFilter() throws Exception {
// 针对行键的前缀过滤器
// Filter pf = new PrefixFilter(Bytes.toBytes("liu"));//"liu".getBytes()
// testScan(pf);
// 行过滤器 需要一个比较运算符和比较器
// RowFilter rf1 = new RowFilter(CompareOp.LESS, new BinaryComparator(Bytes.toBytes("user002")));
// testScan(rf1);
//
// RowFilter rf2 = new RowFilter(CompareOp.EQUAL, new SubstringComparator("01"));//rowkey包含"01"子串的
// testScan(rf2);
//针对指定一个列的value的比较器来过滤
// ByteArrayComparable comparator1 = new RegexStringComparator("^zhang"); //以zhang开头的
// ByteArrayComparable comparator2 = new SubstringComparator("si"); //包含"si"子串
// SingleColumnValueFilter scvf = new SingleColumnValueFilter("base_info".getBytes(), "username".getBytes(), CompareOp.EQUAL, comparator2);
// testScan(scvf);
//针对列族名的过滤器 返回结果中只会包含满足条件的列族中的数据
// FamilyFilter ff1 = new FamilyFilter(CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("base_info")));
// FamilyFilter ff2 = new FamilyFilter(CompareOp.EQUAL, new BinaryPrefixComparator(Bytes.toBytes("base")));
// testScan(ff1);
//针对列名的过滤器 返回结果中只会包含满足条件的列的数据
// QualifierFilter qf1 = new QualifierFilter(CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("password")));
// QualifierFilter qf2 = new QualifierFilter(CompareOp.EQUAL, new BinaryPrefixComparator(Bytes.toBytes("user")));
// testScan(qf2);
//多个过滤器同时使用 select * from t1 where id >10 or age <30
FamilyFilter ff2 = new FamilyFilter(CompareOp.EQUAL, new BinaryPrefixComparator(Bytes.toBytes("base")));
ColumnPrefixFilter cf = new ColumnPrefixFilter("password".getBytes());
FilterList filterList = new FilterList(Operator.MUST_PASS_ALL);
filterList.addFilter(ff2);
filterList.addFilter(cf);
testScan(filterList);
}
//定义一个方法,接受一个过滤器,返回结果数据
public void testScan(Filter filter) throws Exception {
Table table = conn.getTable(TableName.valueOf("t_user_info"));
Scan scan = new Scan();
//设置过滤器
scan.setFilter(filter); //select * from user where age >30
ResultScanner scanner = table.getScanner(scan);
Iterator<Result> iter = scanner.iterator();
//遍历所有的Result对象,获取结果
while (iter.hasNext()) {
Result result = iter.next();
CellScanner cellScanner = result.cellScanner();
while (cellScanner.advance()) {
Cell current = cellScanner.current();
byte[] rowArray = current.getRowArray();
byte[] familyArray = current.getFamilyArray();
byte[] valueArray = current.getValueArray();
byte[] qualifierArray = current.getQualifierArray();
//打印结果
System.out.println(new String(rowArray, current.getRowOffset(), current.getRowLength()));
System.out.print(new String(familyArray, current.getFamilyOffset(), current.getFamilyLength()));
System.out.print(":" + new String(qualifierArray, current.getQualifierOffset(), current.getQualifierLength()));
System.out.println(" " + new String(valueArray, current.getValueOffset(), current.getValueLength()));
}
System.out.println("-----------------------");
}
}
}
region
: region是按照表的行键方向进行了划分 , 一个表最开始只有一个region , 后期不断向该表中添加大量的数据 , region的数据量不断的增大 .当region的数据量达到了10G, 一个region会被划分成两个小的region .
store
: 它是按照行族进行划分 , 表有N个列族 .
memstore
: 在每一个store有一个memstore , 数据达到一定的阈值 , 会把memstore中的数据刷到磁盘中 . 每次刷到磁盘中生成一个storeFile文件 , 最后该文件以HFile这种数据格式写入到HDFS上 .memstore数据刷到磁盘的阈值条件 :
(1) 内存中的数据量达到128M
(2) 时间超过了1小时
HMaster
:(1) 给HRegionServer分配对应的region , 后期去进行管理
(2) 实现HRegionServer的负载均衡
(3) 发现某一个HRegionServer挂掉了 , 然后把它所管理的region重新分配到其他的或者的HRegionServer
具体每一个的职责 :
Client
1 包含访问hbase的接口,client维护着一些cache来加快对hbase的访问,比如region的位置信息。
Zookeeper
1 保证任何时候,集群中只有一个master
2 存贮所有Region的寻址入口----root表在哪台服务器上。
3 实时监控Region Server的状态,将Region server的上线和下线信息实时通知给Master
4 存储Hbase的schema,包括有哪些table,每个table有哪些column family
Master职责
1 为Region server分配region
2 负责region server的负载均衡
3 发现失效的region server并重新分配其上的region
4 HDFS上的垃圾文件回收
5 处理schema更新请求
Region Server职责
1 Region server维护Master分配给它的region,处理对这些region的IO请求
2 Region server负责切分在运行过程中变得过大的region可以看到,client访问hbase上数据的过程并不需要master参与(寻址访问zookeeper和region server,数据读写访问regione server),master仅仅维护者table和region的元数据信息,负载很低。
1 Table中的所有行都按照row key的字典序排列。 WAL
2 Table 在行的方向上分割为多个Hregion
。
3 region按大小分割的(默认10G),每个表一开始只有一个region,随着数据不断插入表,region不断增大,当增大到一个阀值的时候,Hregion就会等分会两个新的Hregion。当table中的行不断增多,就会有越来越多的Hregion。
4 Hregion是Hbase中分布式存储和负载均衡的最小单元
。最小单元就表示不同的Hregion可以分布在不同的HRegion server上。但一个Hregion是不会拆分到多个regionserver上的
。
5 HRegion虽然是负载均衡的最小单元,但并不是物理存储的最小单元
。事实上,HRegion由一个或者多个Store组成,每个store保存一个column family
。每个Strore又由一个memStore和0至多个StoreFile组成。如上图
StoreFile以HFile格式保存在HDFS上。
附 : HFile的格式为 :
HFile的Data Block,Meta Block通常采用压缩方式存储,压缩之后可以大大减少网络IO和磁盘IO,随之而来的开销当然是需要花费cpu进行压缩和解压缩。
目标Hfile的压缩支持两种方式:Gzip,Lzo。
一个region由多个store组成,每个store包含一个列族的所有数据。
Store包括位于内存的memstore和位于硬盘的storefile。
客户端检索数据时,先在memstore找,找不到再找storefile。
WAL 意为Write ahead log(http://en.wikipedia.org/wiki/Write-ahead_logging),该机制用于数据的容错和恢复,Hlog记录数据的所有变更,一旦数据修改,就可以从log中进行恢复。
每个HRegionServer中都有一个HLog对象,HLog是一个实现Write Ahead Log的类,在每次用户操作写入MemStore的同时,也会写一份数据到HLog文件中(HLog文件格式见后续),HLog文件定期会滚动出新的,并删除旧的文件(已持久化到StoreFile中的数据)。
HLog文件就是一个普通的Hadoop Sequence File
:
HLog Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是”写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。
HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue,可参见上文描述。
hbase查询效率高的原因是因为其寻址机制
- 每一个region都有其rowkey的起始值 , 当数据很大时 , 此时就会产生大量的region
- 此时我们就想到了增加一张表作为索引表1 , 保存原始数据中startRowkey , endRowkey , regionserver_id , region_id , 相对于原始表要小很多 , 有自己的startRowkey和endRowkey , 但是多张表又导致索引表1数据也变得比较庞大 , 我们还是采用构建索引表的方法 , 构建索引表2
- 此时大量数据的索引信息就减少了很多 , 一般此时都能大部分解决了 , 此时zk上就保存索引表2 , 由哪一个regionserver所管理的region上
- 当我们查询时 , 找到索引表2的中存储相关信息的regionServer , 依次向前查找 , 知道我们找到元数据中对应的region , 此时采用布隆过滤器找到对应字段组成最终结果返回给客户端
扩展 :
布隆过滤器
: 它是作用在每一个storeFile , 可以快速的跳过不包括当前要查询的rowkey的文件 , 快速找到包含该rowkey的目标文件当我们要查找的是一个条件查询,怎么做?
一般我们会选择按照rowkey查询 , 因为它性能最快 , 这里是由于可以使用到上面的hbase的寻址机制 , 按照条件进行查询 , 这个时候由于整个表有大量的region , 每一个Region中有大量的storeFile , 每一个storeFile都有可能存在满足条件的数据 , 就需要依次进入到每一个文件中 , 进行遍历匹配比较 .
spark中默认的meta表就是索引表1 , 另外一张表(我们称之为ROOT)就是索引表2
现在假设我们要从Table2里面查询一条RowKey是RK10000的数据。那么我们应该遵循以下步骤:
从.META.表里面查询哪个Region包含这条数据。
获取管理这个Region的RegionServer地址。
连接这个RegionServer, 查到这条数据。
系统如何找到某个row key (或者某个 row key range)所在的region
bigtable 使用三层类似B+树的结构来保存region位置。
第一层: 保存zookeeper里面的文件,它持有root region的位置。
第二层:root region是.META.表的第一个region其中保存了.META.表其它region的位置。通过root region,我们就可以访问.META.表的数据。
第三层: .META.表
它是一个特殊的表,保存了hbase中所有数据表的region 位置信息。
说明:
(1) root region永远不会被split,保证了最需要三次跳转,就能定位到任意region 。
(2).META.表每行保存一个region的位置信息,row key 采用表名+表的最后一行编码而成。
(3) 为了加快访问,.META.表的全部region都保存在内存中。
(4) client会将查询过的位置信息保存缓存起来,缓存不会主动失效,因此如果client上的缓存全部失效,则需要进行最多6次网络来回,才能定位到正确的region(其中三次用来发现缓存失效,另外三次用来获取位置信息)。
总结
Region定位流程:
a) 寻找RegionServer
ZooKeeper–> -ROOT-(单Region)–> .META.–> 用户表
b) -ROOT-表
表包含.META.表所在的region列表,该表只会有一个Region;
Zookeeper中记录了-ROOT-表的location。
c) .META.表
表包含所有的用户空间region列表,以及RegionServer的服务器地址
读请求过程:
1) 客户端通过zookeeper以及root表和meta表找到目标数据所在的regionserver
(2)联系regionserver查询目标数据
(3)regionserver定位到目标数据所在的region,发出查询请求
(4)region先在memstore中查找,命中则返回
(5)如果在memstore中找不到,则在storefile中扫描(可能会扫描到很多的storefile----bloomfilter布隆过滤器)
补充:布隆过滤器参数类型有2种:
Row
、row+col
写请求过程:
细节描述:
hbase使用MemStore和StoreFile存储对表的更新。
数据在更新时首先写入Log(WAL log)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时,系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了。
当系统出现意外时,可能导致内存(MemStore)中的数据丢失,此时使用Log(WAL log)来恢复checkpoint之后的数据。
StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作
。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(minor_compact, major_compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行split
,等分为两个StoreFile。
(1) region分配
任何时刻,一个region只能分配给一个region server
。master记录了当前有哪些可用的region server。以及当前哪些region分配给了哪些region server,哪些region还没有分配。当需要分配的新的region,并且有一个region server上有可用空间时,master就给这个region server发送一个装载请求,把region分配给这个region server。region server得到请求后,就开始对此region提供服务。
(2) region server上线
master使用zookeeper来跟踪region server状态
。当某个region server启动时,会首先在zookeeper上的server目录下建立代表自己的znode。由于master订阅了server目录上的变更消息,当server目录下的文件出现新增或删除操作时,master可以得到来自zookeeper的实时通知。因此一旦region server上线,master能马上得到消息 .
(3) region server下线
当region server下线时,它和zookeeper的会话断开,zookeeper而自动释放代表这台server的文件上的独占锁。master就可以确定:
1、region server和zookeeper之间的网络断开了。
2、 region server挂了。
无论哪种情况,region server都无法继续为它的region提供服务了,此时master会删除server目录下代表这台region server的znode数据,并将这台region server的region分配给其它还活着的同志。
Master工作机制
(1) 从zookeeper上获取唯一一个代表active master的锁
,用来阻止其它master成为活着的master。
(2)扫描zookeeper上的servermaster启动进行以下步骤:
父节点,获得当前可用的region server列表。
(3)和每个region server通信,获得当前已分配的region和region server的对应关系。
(4)扫描.META.region的集合,计算得到当前还未分配的region,将他们放入待分配region列表。
由于master只维护表和region的元数据
,而不参与表数据IO的过程,master下线仅导致所有元数据的修改被冻结(无法创建删除表,无法修改表的schema,无法进行region的负载均衡,无法处理region 上下线,无法进行region的合并,唯一例外的是region的split可以正常进行,因为只有region server参与),表的数据读写还可以正常进行
。因此master下线短时间内对整个hbase集群没有影响
。
HBase容错性
布隆过滤可以每列族单独启用
create 'table',{BLOOMFILTER =>'ROW'}
启用布隆过滤可以节省读磁盘过程,可以有助于降低读取延迟
create 'table',{VERSIONS=>'2'}
MIN_VERSIONS=> '0'
是说在compact操作执行之后,至少要保留的版本默认值是NONE
即不使用压缩
该列族是否采用压缩,采用什么压缩算法
create 'table',{NAME=>'info',COMPRESSION=>'SNAPPY'}
Algorithm | % remaining | Encoding | Decoding |
---|---|---|---|
GZIP | 13.4% | 21 MB/s | 118 MB/s |
LZO | 20.5% | 135 MB/s | 410 MB/s |
Zippy/Snappy | 22.2% | 172 MB/s | 409 MB/s |
如果建表之初没有压缩,后来想要加入压缩算法,可以通过alter修改schema
4、alter
使用方法:
disable 'table'
alter 'table',{NAME=>'info',COMPRESSION=>'snappy'}
enable 'table'
但是需要执行major_compact 'table' 命令之后 才会做实际的操作
5、TTL : 默认是2147483647 即:Integer.MAX_VALUE 值大概是68年
这个参数可以根据具体的需求对数据设定存活时间,超过存过时间的数据将在表中不在显示,待下次major compact的时候再彻底删除数据
6、describe ‘table’ 这个命令查看了create table 的各项参数或者是默认值
7、disable_all 'toplist.*'
disable_all 支持正则表达式,并列出当前匹配的表的如下
toplist_a_total_1001 toplist_a_total_1002 toplist_a_total_1003 toplist_a_total_1004
......
Disable the above 25 tables (y/n)? 并给出确认提示
8、drop_all 这个命令和disable_all的使用方式是一样的
9、hbase表预分区
----手动分区
默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions
,这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡。create 't1', 'f1',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}
bin/hbase org.apache.hadoop.hbase.util.RegionSplitter test_table HexStringSplit -c 10 -f info
参数:
test_table是表名
HexStringSplit 是split 方式
-c 是分10个region
-f 是family
就是合理设计rowkey 能让各个region 的并发请求平均分配(趋于均匀) 使IO 效率达到最高
,但是预分区需要将filesize 设置一个较大的值,设置哪个参数呢, hbase.hregion.max.filesize 这个值默认是10G 也就是说单个region ,默认大小是10G
表结构设计
Hbase的设计原则
rowkey长度原则
建议越短越好,不要超过16个字节
rowkey唯一原则
将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。
rowkey散列原则
建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。
热点问题
为了避免写热点,设计rowkey使得不同行在同一个region,但是在更多数据情况下,数据应该被写入集群的多个region,而不是一个。下面是一些常见的避免热点的方法以及它们的优缺点:
这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同
。分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致。加盐之后的rowkey就会根据随机生成的前缀分散到各个region上,以避免热点
。
哈希会使同一行永远用一个前缀加盐
。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据。
第三种防止热点的方法时反转固定长度或者数字格式的rowkey
这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
反转rowkey的例子以手机号为rowkey,可以将手机号反转后的字符串作为rowkey,这样的就避免了以手机号那样比较固定开头导致热点问题
135****1023------>3201****531
136****9301------>1039****631
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为rowkey的一部分对这个问题十分有用,可以用 Long.Max_Value -
timestamp 追加到key的末尾,例如 [key][reverse_timestamp] , [key] 的最新值可以通过scan [key]获得[key]的第一条记录,因为HBase中rowkey是有序的,第一条记录是最后录入的数据。
其他的建议 :
尽量减少行键和列族的大小在HBase中,value永远和它的key一起传输的。
列族尽可能越短越好,最好是一个字符。
冗长的属性名虽然可读性好,但是更短的属性名存储在HBase中会更好。
查询条件为 , 当列族F1下的C1字段等于C11的对应的列族F2的C2字段下的值
(1) 当要对F1:C1这列建立索引是 , 只需要建立F1:C1各列值到其对应行键的映射关系 , 如C11->RK1等 , 这样就完成了对F1:C1列值的二级索引的构建
(2) 当要查询符合F1:C1=C11对应的F2:C2的列值时 , 查询的步骤如下 :
a . 根据C1 = C11到索引数据中查找对应的RK , 查询得到其对应对的RK = RK1
b . 得到RK1后就自然能够根据RK1来查询C2的值了 , 这是构建二级索引大概思路 , 其他组合查询的联合索引的建立也类似 .
二级索引的本质就是建立各列值与行键之间的映射关系