HBase
概念
HBase是一种分布式、可扩展、支持海量数据存储的NoSQL(non-relational或者not only sql)数据库
NoSQL泛指非关系型数据库,与关系型数据库不同的是,它们不保证ACID特性
NoSQL易扩展,具有较高的读写性能
结构
有列族、row key、region、store等概念。
-
每行数据都由一个RowKey和多个列组成,数据按RowKey的字典顺序存储
-
每个列都由Column Family(列族)和Column Qualifier(列限定符)进行限定
-
HBase可以比较方便地添加列
-
HBase支持对数据的增删改(HDFS是不可以的)
- 通过追加的方式,把修改/删除的信息放到末尾。用TimeStamp记录和Type
- 先“骗”用户,让用户感觉已经修改成功了,再在之后适当的时机,把数据修改过
-
StoreFile保存实际物理文件,以Hfile的形式存储在HDFS上,数据在StoreFile中都是有序的
-
MeMStore是写缓存,HFile有序,所以数据先存储在MeMStorez中,排好序后再刷写进HFile,每次刷写都生成一个新的HFile
-
WAL(Write Ahead Log),在数据写入HFile前,数据都在内存中,为了避免数据丢失的问题,数据要先写在WAL日志文件中,再写入MeMStore.系统出现故障时,可根据日志文件重建
-
BlockCache,读缓存,每次查询的数据都会缓存在BlockCache中。
写流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ma9LUwTn-1680437254986)(.\Hbase写流程.png)]
- Client先访问zookeeeper,获取hbase:meta表位于哪个RegionServer
- 访问对应的RegionServer,获取hbase:meta表,根据读请求的namespace:table/rowkey查询出目标数据位于哪个RegionServer中的哪个Region。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache
- 与目标regionServer进行通讯
- 将数据顺序写入(追加)到WAL
- 将数据写入对应的MemStore,数据会在MemStore进行排序
- 向客户端发送ack
- 等达到MemStore的刷写时机后,把数据刷写到HFile
MemStore刷写时机
-
当某个memstore的大小达到了hbase.hregion.memstore.flush.size(默认128M),其所在region的所有memstore都会刷写
当memstore的大小达到了:hbase.hregion.memstore.flush.size(默认128M)、hbase.hregion.memstore.block.multiplier(默认4)时,回阻止继续向该memstore写数据
-
当region server中的memstore的总大小达到了java_heapsize, hbase.regionserver.global.memstore.size(默认值0.4),hbase.regionserver.global.memstore.size.lower.limit(默认值0.95)
region会按其所有memstore的大小顺序(从大到小)依次进行刷写,直到region server中所有memstore的总大小减小到小于上面两个值。
当region server中的memstore总大小达到java_heapsize, hbase.regionserver.global.memstore.size(默认值0.4)时,阻止继续往所有的memstore写数据
-
到达自动刷新时间,也会触发memstore flush。自动刷新时间间隔由hbase.regionserver.optionalcacheflushinterval(默认1小时)配置
-
当WAL文件数量达到hbase.regionserver.max.logs时,region会按时间顺序依次进行刷写,知道WAL文件数量减小到hbase.regionserver.max.logs以下(现在不用设置了,最大值32)
读流程
- Client先访问zookeeper,获取hbase:meta表位于哪个RegionServer
- 访问对应的RegionServer,获取hbase:meta表,根据读请求的namespace:table.rowker,查询出目标数据位于哪个region server中的哪个region。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问
- 与目标RegionServer进行通讯
- 分别在MemStore和StoreFile(HFile)中查询目标数据,并将查到的数据(同一条数据的不同版本或者不同类型)进行合并
- 将查询到的新的数据块(Block,HFile数据存储单元,默认大小64KB)缓存到Block Cache
- 将合并后的最终数据返回到客户端
StoreFile Compaction
每次memstore刷写都会生成一个新的HFile,且每个字段的不同版本和不同类型有可能分布在不同的HFile中,因此查询时需要遍历所有HFile。为减少HFile的个数以及清理掉过期和删除的数据,会进行StoreFile Compaction。
Store Compaction分为:Minor Compaction和Major Compaction。Minor Compaction会将临近的若干个较小的HFile合并成一个较大的HFile,并清理掉部分过期和删除的数据。Major Compaction会将一个Store下的所有HFile合并成一个大的HFile,并清理掉所有过期和删除的数据
Region Split
默认情况下,每个Table起初只有一个Region,随着数据不断写入,Region自动进行拆分。刚拆分时,两个子Region都位于当前RegionServer,但处于负载均衡的考虑,HMaster有可能会将某个Region转移给其他的Region Server
Region Split时机:一个region中的某个store下所有StoreFile的总大小超过Min(initialSize*R^3 ,hbase.hregion.max.filesize"),该Region就会进行拆分。其中initialSize的默认值为2*hbase.hregion.memstore.flush.size,R为当前Region Server中属于该Table的Region个数
具体的切分策略为:
第一次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了。
Hbase 2.0引入了新的split策略:如果当前RegionServer上该表只有一个Region,按照2 * hbase.hregion.memstore.flush.size分裂,否则按照hbase.hregion.max.filesize分裂
使用
-
在启动前,要保证集群间的时间节点时间同步,否则regionserver可能无法启动(或者修改hbase.master.maxlockskew的值,将其增大)
-
在启动HBase前需要先启动Zookeeper。Zookeeper为HBase提供了master高可用,RegionServer监控、元数据入口以及集群配置的维护等工作
-
HDFS为HBase提供最终的底层数据存储服务
-
HBase中用HMaster监控HRegionServer的生命周期,均衡RegionServer的负载,需要配置HMaster的高可用,两种方式:
- 1.启动多个节点时,都单点启动master(hbase-daemon.sh start master),后起的就是backup
- 2.在conf目录下创建backup-masters文件,在里面写后备节点,如hadoop103
基本操作
进入HBase客户端:hbase shell
查看namespace:list_namespace
创建namespace:create_namespace “test01”, {“author”=>“jojo”, “create_time”=>“2023-02-13 08:08:08”}
查看namespace:describe_namespace “test01”
修改namespace(只能修改属性):alter_namespace “test01”, {“create_time”=>“2023-03-13 08:08:08”}
删除namespace:drop_namespace “test01”(只有namespace下没有表时可以删除)
创建表create ‘student’,‘info’ create ‘mydb:student’,‘info’
插入数据
put ‘mydb:student’,‘1001’,‘info:sex’,‘female’
put ‘mydb:student’,‘1001’,‘info:age’,‘20’
put ‘mydb:student’,‘1002’,‘info:age’,‘23’
put ‘mydb:student’,‘1002’,‘info:name’,‘jojo’
put ‘mydb:student’,‘1002’,‘info:sex’,‘male’
查看数据:scan ‘mydb:student’
ROW COLUMN+CELL
1001 column=info:age, timestamp=1676288716119, value=20
1001 column=info:sex, timestamp=1676288695862, value=female
1002 column=info:age, timestamp=1676288725028, value=23
1002 column=info:name, timestamp=1676288743436, value=jojo
1002 column=info:sex, timestamp=1676288754795, value=male
2 row(s)
查看表结构:describe ‘mydb:student’
更新表数据:put ‘mydb:student’,‘1002’,‘info:name’,‘yoyo’
查看指定行/列数据:get ‘mydb:student’,‘1002’ get ‘mydb:student’,‘1001’,‘info:name’
统计行数:count ‘mydb:student’ 2
删除某rowkey的某列数据:delete ‘mydb:student’,‘1002’,‘info:name’
删除某rowkey的全部数据:deleteall ‘mydb:student’,‘1001’
清空表数据:truncate ‘mydb:student’【步骤:Disabling table… ===> Truncating table…】
disable 表:disable ‘mydb:student’
删除表之前需要先disable,再drop ‘mydb:student’
API调用
DDL
public class Hbase_DDL {
private static Connection connection;
static {
Configuration configuration = HBaseConfiguration.create();
// 和hbase-site.xml里的配置一个套路
configuration.set("hbase.zookeeper.quorum","hadoop102,hadoop103,hadoop104");
try {
connection = ConnectionFactory.createConnection(configuration);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
// createNameSpace("mydb1");
// createTable("mydb1","t1","info1","info2");
dropTable("mydb1","t1");
}
// 创建namespace
public static void createNameSpace(String namespace) throws IOException {
// 基本判空操作
if (namespace==null||namespace.equals(""))
System.err.println("name space 名字不能为空");
// 获取admin对象
Admin admin = connection.getAdmin();
NamespaceDescriptor.Builder builder = NamespaceDescriptor.create(namespace);
NamespaceDescriptor namespaceDescriptor = builder.build();
// 调方法
try {
admin.createNamespace(namespaceDescriptor);
System.out.println(namespace+"创建成功");
}catch (NamespaceExistException e){
System.err.println(namespace+"已存在");
}
}
// 判断表是否存在
public static boolean isTableExist(String nameSpaceName,String tableName) throws IOException {
boolean b;
Admin admin = connection.getAdmin();
b= admin.tableExists(TableName.valueOf(nameSpaceName,tableName));
admin.close();
System.out.println(nameSpaceName+tableName+"是否存在:"+b);
return b;
}
// 创建表
public static void createTable(String nameSpace,String tableName,String ...cfs) throws IOException {
if (isTableExist(nameSpace,tableName)){
System.out.println(nameSpace+tableName+"表已存在");
return;
}
Admin admin = connection.getAdmin();
TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf(nameSpace,tableName));
if (cfs==null || cfs.length<=1){
System.err.println("至少指定一个列族");
return;
}
for (String cf:cfs){
ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder=ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf));
ColumnFamilyDescriptor build = columnFamilyDescriptorBuilder.build();
tableDescriptorBuilder.setColumnFamily(build);
}
TableDescriptor tableDescriptor = tableDescriptorBuilder.build();
admin.createTable(tableDescriptor);
System.out.println(nameSpace+tableName+"创建成功");
admin.close();
}
// 删除表
public static void dropTable(String nameSpaceName,String tableName) throws IOException {
if (!isTableExist(nameSpaceName,tableName)){
System.err.println(nameSpaceName+tableName+"表不存在,无法删除");
return;
}
Admin admin = connection.getAdmin();
admin.disableTable(TableName.valueOf(nameSpaceName,tableName));
admin.deleteTable(TableName.valueOf(nameSpaceName,tableName));
System.out.println(nameSpaceName+tableName+"已删除");
admin.close();
}
}
DML
public class HBase_DML {
private static Connection connection;
static {
Configuration configuration = HBaseConfiguration.create();
// 和hbase-site.xml里的配置一个套路
configuration.set("hbase.zookeeper.quorum","hadoop102,hadoop103,hadoop104");
try {
connection = ConnectionFactory.createConnection(configuration);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
scanData("mydb","student");
}
//插入数据
public static void insertData(String nameSpace,String tableName,String rowKey,
String cf,String cl,String value) throws IOException {
// put 'mydb:student','1001','info:sex','female'
Admin admin = connection.getAdmin();
boolean b =admin.tableExists(TableName.valueOf(nameSpace,tableName));
if (b){
Table table = connection.getTable(TableName.valueOf(nameSpace, tableName));
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes(cf),Bytes.toBytes(cl),Bytes.toBytes(value));
table.put(put);
table.close();
System.out.println("已插入数据:"+nameSpace+" "+tableName+" "+cf+" "+cl+" "+value);
}
admin.close();
}
// 删除数据
public static void delData(String nameSpace,String tableName,String rowKey,
String cf,String cl) throws IOException {
// delete 'mydb:student','1002','info:name'
// deleteall 'mydb:student','1001'
Table table = connection.getTable(TableName.valueOf(nameSpace, tableName));
Delete delete = new Delete(Bytes.toBytes(rowKey));
// 删除某个数据
delete.addColumn(Bytes.toBytes(cf), Bytes.toBytes(cl));
// 删除某个列族
delete.addFamily(Bytes.toBytes(cf));
table.delete(delete);
table.close();
}
// 获取数据
public static void getData(String nameSpace,String tableName,String rowKey,
String cf,String cl) throws IOException {
Table table = connection.getTable(TableName.valueOf(nameSpace, tableName));
Get get = new Get(Bytes.toBytes(rowKey));
get.addColumn(Bytes.toBytes(cf),Bytes.toBytes(cl));
Result result = table.get(get);
Cell[] cells = result.rawCells();
for (Cell cell : cells) {
String cellString = Bytes.toString(CellUtil.cloneRow(cell)) + " : " +
Bytes.toString(CellUtil.cloneFamily(cell)) + " : " +
Bytes.toString(CellUtil.cloneQualifier(cell))+ " : " +
Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(cellString);
}
table.close();
}
// 扫描数据
public static void scanData(String nameSpace,String tableName) throws IOException {
Table table = connection.getTable(TableName.valueOf(nameSpace, tableName));
Scan scan = new Scan();
ResultScanner results = table.getScanner(scan);
for (Result result : results) {
for (Cell cell : result.rawCells()) {
System.out.println(Bytes.toString(CellUtil.cloneRow(cell))+":"+
Bytes.toString(CellUtil.cloneFamily(cell))+":"+
Bytes.toString(CellUtil.cloneQualifier(cell))+":"+
Bytes.toString(CellUtil.cloneValue(cell)));
}
}
table.close();
}
}
预分区
分为四个分区:
create ‘staff1’,‘info’,SPLITS => [‘1000’,‘2000’,‘3000’,‘4000’]
按16进制预分区为15个分区
create ‘staff2’,‘info’,{NUMREGIONS => 15, SPLITALGO => ‘HexStringSplit’}
Created table staff2
按文件分区:
建一个split.txt【 aaaa bbbb cccc dddd 】
在该文件所在的目录下启动hbase
create ‘staff3’,‘info’,SPLITS_FILE => ‘splits.txt’
预分区API
public static void createTableWithRegions(String nameSpaceName, String tableName,String ... cfs ) throws IOException {
if(existsTable(nameSpaceName, tableName)){
System.err.println((nameSpaceName == null || nameSpaceName.equals("")? "default" : nameSpaceName) + ":" + tableName + "表已经存在");
return ;
}
Admin admin = connection.getAdmin() ;
TableDescriptorBuilder tableDescriptorBuilder =
TableDescriptorBuilder.newBuilder(TableName.valueOf(nameSpaceName,tableName));
if(cfs == null || cfs.length < 1){
System.err.println("至少指定一个列族");
return ;
}
for (String cf : cfs) {
ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder =
ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(cf));
ColumnFamilyDescriptor columnFamilyDescriptor =
columnFamilyDescriptorBuilder.build();
tableDescriptorBuilder.setColumnFamily(columnFamilyDescriptor);
}
TableDescriptor tableDescriptor = tableDescriptorBuilder.build();
byte [][] splitkeys = new byte[4][];
//['1000','2000','3000','4000']
splitkeys[0] = Bytes.toBytes("1000");
splitkeys[1] = Bytes.toBytes("2000");
splitkeys[2] = Bytes.toBytes("3000");
splitkeys[3] = Bytes.toBytes("4000");
admin.createTable(tableDescriptor,splitkeys);
admin.close();
}
rowkey的设计
原则:唯一性、散列性、长度
例子:
1388888888(主叫) 13999999999(被叫) 2021-05-14 12:12:12 360 ......
业务: 查询某个用户 某天 某月 某年 的通话记录
预分区: 预计规划50个分区 .
-∞ ~ 00|
00| ~ 01|
01| ~ 02|
…
千万不能拿某个用户来分区,因为有的用户通话量大,有的通话量很少,会造成数据不均衡
分析: 假如将某个用户某天的数据存到一个分区中. 查某天的数据只需要扫描一个分区
加入将某个用户某月的数据存到一个分区中. 查某天 某月的数据只需要扫描一个分区. √
rowkey: 01_1388888888_2021-05-14 12:12:12 -> 1388888888_2021-05 % 分区数 = 01
01_1388888888_2021-05-15 12:12:12 -> 1388888888_2021-05 % 分区数 = 01
01_1388888888_2021-05-16 12:12:12
01_1388888888_2021-05-17 12:12:12
先算应该进哪个分区,再把该进的分区拼到前面,能保证它一定能进那个分区
03_1377777777_2021-05-16 12:12:12 -> 1377777777_2021-05 % 分区数 = 03
验证:
查询 1388888888 用户 2020年08月的通话记录
- 先计算分区号
1388888888_2020-08 % 50 = 04 - rowkey
04_1388888888_2020-08-… - scan
scan “teldata” ,{STARTROW=> ‘04_1388888888_2020-08’ STOPROW=> ‘04_1388888888_2020-08|’}
查询 1388888888 用户 2020年08月08日的通话记录
- 先计算分区号
1388888888_2020-08 % 50 = 04 - rowkey
04_1388888888_2020-08-08… - scan
scan “teldata” ,{STARTROW=> ‘04_1388888888_2020-08-08’ STOPROW=> ‘04_1388888888_2020-08-08|’}
查询 1388888888 用户 2020年08月 和 09月的通话记录
- 先计算分区号
1388888888_2020-08 % 50 = 04
1388888888_2020-09 % 50 = 06 - rowkey
04_1388888888_2020-08-…
06_1388888888_2020-09-… - scan
scan “teldata” ,{STARTROW=> ‘04_1388888888_2020-08’ STOPROW=> ‘04_1388888888_2020-08|’}
scan “teldata” ,{STARTROW=> ‘06_1388888888_2020-09’ STOPROW=> ‘06_1388888888_2020-09|’}
查询 1388888888 用户 2020年08月09日 和 10日的通话记录
- 先计算分区号
1388888888_2020-08 % 50 = 04 - rowkey
04_1388888888_2020-08-09…
04_1388888888_2020-08-09…
04_1388888888_2020-08-10… - scan
scan “teldata” ,{STARTROW=> ‘04_1388888888_2020-08-09’ STOPROW=> ‘04_1388888888_2020-08-10|’}
内存优化
1.Zookeeper会话超时时间
hbase-site.xml
属性:zookeeper.session.timeout
解释:默认值为90000毫秒(90s)。当某个RegionServer挂掉,90s之后Master才能察觉到。可适当减小此值,以加快Master响应,可调整至60000毫秒。
2.设置RPC监听数量
hbase-site.xml
属性:hbase.regionserver.handler.count 解释:默认值为30,用于指定RPC监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。
3.手动控制Major Compaction
hbase-site.xml
属性:hbase.hregion.majorcompaction
解释:默认值:604800000秒(7天), Major Compaction的周期,若关闭自动Major Compaction,可将其设为0
4.优化HStore文件大小
hbase-site.xml
属性:hbase.hregion.max.filesize 解释:默认值10737418240(10GB),如果需要运行HBase的MR任务,可以减小此值,因为一个region对应一个map任务,如果单个region过大,会导致map任务执行时间过长。该值的意思就是,如果HFile的大小达到这个数值,则这个region会被切分为两个Hfile。
5.优化HBase客户端缓存
hbase-site.xml
属性:hbase.client.write.buffer 解释:默认值2097152bytes(2M)用于指定HBase客户端缓存,增大该值可以减少RPC调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少RPC次数的目的。
6.指定scan.next扫描HBase所获取的行数
hbase-site.xml
属性:hbase.client.scanner.caching 解释:用于指定scan.next方法获取的默认行数,值越大,消耗内存越大。
7.BlockCache占用RegionServer堆内存的比例
hbase-site.xml
属性:hfile.block.cache.size
解释:默认0.4,读请求比较多的情况下,可适当调大
8.MemStore占用RegionServer堆内存的比例
hbase-site.xml
属性:hbase.regionserver.global.memstore.size
解释:默认0.4,写请求较多的情况下,可适当调大
Hive和HBase对比
Hive
- 数据分析工具,本质上是将HDFS中已经存储的文件在MySql中做了一个双射关系,以方便使用HQL去管理查询
- 适合于离线的数据分析与清洗,延迟较高
- Hive的存储依旧在DataNode上,编写的HQL语句会转化为MR代码执行(Hive基于HDFS和MR)
HBase
- 数据库, 面向列族存储的非关系型数据库
- 用于存储结构化和非结构化的数据,适合于单表非关系型数据存储,不适合做关联查询
- 数据持久化存储的体现形式是HFile,存放于DataNode,被RegionServer以region的形式进行管理(HBase基于HDFS)
- 延迟较低,接入在线业务使用
hbase-site.xml
属性:hfile.block.cache.size
解释:默认0.4,读请求比较多的情况下,可适当调大
8.MemStore占用RegionServer堆内存的比例
hbase-site.xml
属性:hbase.regionserver.global.memstore.size
解释:默认0.4,写请求较多的情况下,可适当调大
Hive和HBase对比
Hive
- 数据分析工具,本质上是将HDFS中已经存储的文件在MySql中做了一个双射关系,以方便使用HQL去管理查询
- 适合于离线的数据分析与清洗,延迟较高
- Hive的存储依旧在DataNode上,编写的HQL语句会转化为MR代码执行(Hive基于HDFS和MR)
HBase
- 数据库, 面向列族存储的非关系型数据库
- 用于存储结构化和非结构化的数据,适合于单表非关系型数据存储,不适合做关联查询
- 数据持久化存储的体现形式是HFile,存放于DataNode,被RegionServer以region的形式进行管理(HBase基于HDFS)
- 延迟较低,接入在线业务使用