1、 学HBase的意义是什么
我本想用MySQL来与HBase作比较,但发现他们两者毫无可比性,因为两者运用领域不同,各自有各自的优点,就好比爬山穿登山鞋,潜水穿脚蹼一般。
一门技术的兴起,一个优秀的开源项目的存在肯定是有它所存在的意义,正如大数据一样,正是因为随着时间的发展,随着技术的发展导致我们每天的数据增量达到一个非常庞大的状态,同时在数据之中又能挖掘到很多有用的信息。所以才有了大数据技术的飞速发展。
而学习HBase不仅仅是因为他属于Hadoop生态圈,而且他很特殊;
我想各位在接触HBase之前可能就没有看到过哪个数据库是面向列存储的,我也不知该如何简述他的与众不同,总之我们就沉浸下来,由笔者带各位从下文的学习中深刻体会一下吧。
1.1、引入
HBase是什么
HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统;
HBase是Apache的Hadoop项目的子项目;
HBase不同于一般的关系数据库,它是一个适合于非结构化数据存储的数据库;
HBase另一个不同的是HBase基于列的而不是基于行的模式;
HBase利用Hadoop的HDFS作为其文件存储系统,利用zookeeper作为其分布式协调服务主要用来存储半结构化或非结构化的松散数据。
1.2、HBase能做什么
海量数据存储:
上百亿行 x 上百万列
并没有列的限制
当表非常大的时候才能发挥这个作用, 最多百万行的话,没有必要放入hbase中
1.3、准实时查询:
百亿行 x 百万列,在百毫秒以内
Hbase在实际场景中的应用
1). 交通方面:
船舶GPS信息,全长江的船舶GPS信息,每天有1千万左右的数据存储
2). 金融方面:
消费信息,贷款信息,信用卡还款信息等
3). 电商:
淘宝的交易信息等,物流信息,浏览信息等
4). 移动:
通话信息等,都是基于HBase的存储
1.4、HBase特点是什么
容量大:
传统关系型数据库,单表不会超过五百万,超过要做分表分库
Hbase单表可以有百亿行、百万列,数据矩阵横向和纵向两个维度所支持的数据量级都非常具有弹性
面向列:
面向列的存储和权限控制,并支持独立检索,可以动态增加列,即,可单独对列进行各方面的操作
列式存储,其数据在表中是按照某列存储的,这样在查询只需要少数几个字段的时候,能大大减少读取的数量
多版本:
Hbase的每一个列的数据存储有多个Version,比如住址列,可能有多个变更,所以该列可以有多个version
稀疏性:
为空的列并不占用存储空间,表可以设计的非常稀疏。
不必像关系型数据库那样需要预先知道所有列名然后再进行null填充
拓展性:
底层依赖HDFS,当磁盘空间不足的时候,只需要动态增加datanode节点服务(机器)就可以了
高可靠性:
WAL机制,保证数据写入的时候不会因为集群异常而导致写入数据丢失
Replication机制,保证了在集群出现严重的问题时候,数据不会发生丢失或者损坏
Hbase底层使用HDFS,本身也有备份。
高性能:
底层的LSM数据结构和RowKey有序排列等架构上的独特设计,使得Hbase写入性能非常高。
Region切分、主键索引、缓存机制使得Hbase在海量数据下具备一定的随机读取性能,该性能针对Rowkey的查询能够到达毫秒级别
LSM树,树形结构,最末端的子节点是以内存的方式进行存储的,内存中的小树会flush到磁盘中(当子节点达到一定阈值以后,会放到磁盘中,且存入的过程会进行实时merge成一个主节点,然后磁盘中的树定期会做merge操作,合并成一棵大树,以优化读性能。)
总结:
面向列,容量大,写入比mysql快但是读取没有,超过五百万条数据的话建议读写用Hbase。
2、HBase数据模型
在HBase中有些术语需要提前了解一下:
2.1、NameSpace
命名空间类似于关系型数据库中数据库的概念,它其实是表的逻辑分组。
命名空间是可以管理维护的,可以创建,删除或者更改命名空间
HBase有两个特殊定义的命名空间:
default:没有明确指定命名空间的表将自动划分到此命名空间
hbase:系统命名空间,用于包含HBase内部表
2.2、Table
HBase采用表来组织数据;
他不同于MySQL的是他的表不是单纯由行(记录)列(字段)组成
他的表由RowKey、Colum Family、Colum Qualifier、Timestamp、cell共同构成
2.3、RowKey
RowKey是用来检索记录的主键,是一行数据的唯一标识
RowKey可以是任意字符串最大长度是64KB,以字节数组保存
存储时,数据按照Row Key的字典序排序,设计RowKey时要充分考虑排序存储这个特性,将经常读取的行存放到一起
2.4、 Colum Family
列族在物理上包含了许多列与列的值,每个列族都有一些存储的属性可配置
将功能相近的列存放到同一个列族中,相同列族中的列会存放在同一个store中
列族一般需要在创建表的时候声明,一般一个表中的列族不超过3个
列隶属于列族,列族隶属于表
2.5、Colum Qualifier
列族的限定词,理解为列的唯一标识。但是列标识是可以改变的,因此每一行会有不同的列标识
使用的时候必须列族:列
列可以根据需求动态添加或删除,同一个表中的不同行的数据列都可以不同
2.6、Timestamp
通过rowkey、columFamily、columqualifier确定一个存储单元通过时间戳来索引
每个cell都保存着同一份数据的多个版本
每个cell中,不同版本的数据按照时间顺序倒叙排序,即最新的数据排到最前面。
为了避免数据存在过多版本中造成管理负担,HBase提供了两种数据版本回收方式
一是存储数据的最后n个版本
二是保存最近一段时间的版本
2.7、cell
Cell是由row columFamily、columQualifier、version组成
cell中数据没有类型,全部是字节码存储的
因为HDFS上的数据是字节数组
3、HBase架构模型
HBase架构有三个主要组成部分:
客户端(Client)
主服务器(HMaster)
区域服务器(HRegionServer)
3.1、Client
主要功能
客户端负责发送请求到数据库,客户端连接的方式有很多种
hbase shell
类JDBC
client维护着一些cache来加快对hbase的访问,比如regione的位置信息。
发送请求的类型
DDL:数据库定义语言(表的建立,删除,添加删除列族,控制版本)
DML:数据库操作语言(增删改)
DQL:数据库查询语言(查询–全表扫描–基于主键–基于过滤器)
3.2、HMaster
定义
HBase集群的主节点,HMaster也可以实现高可用(active–standby)
通过Zookeeper来维护主副节点的切换
作用
上下线的监督,创建表的时候为Region server分配region并负责Region server的负载均衡
负责接受客户端对table的结构DDL(创建,删除,修改)操作,DML和DQL由其他节点承担
因为HMaster没有联邦机制,业务承载能力有限,而且数据库的表结构很少会变化,大部分都是CRUD操作
表的元数据信息–>Zookeeper上面
表的数据–>HRegionServer上
负责监督HRegionServer的健康状况
当HRegionServer下线的时候,HMaster会将当前HRegionServer上的Region转移到其他的HRegionServer
3.3、 HRegionServer
定义
HBase的具体工作节点(RegionServer属于HBase具体数据的管理者),一般一台主机就是一个RegionServer
作用
一个RegionServer中包含很多HMaster分配给RegionServer的Region,同时RegionServer处理这些Region的IO请求(DML和DQL请求)
当客户端发送DML和DQL操作的时候,HRegionServer负责和客户端建立连接
HRegionServer会实时和HMaster保持心跳,汇报当前节点的信息
当接收到Hmaster命令创建表的时候,分配一个Region对应一张表
Region server负责切分在运行过程中变得过大的region
其他:
当意外关闭的时候,当前节点的Region会被其他HRegionServer管理
图解 RegionServer、Region、store和storefile之间的关系
3.4、HRegion
定义理解
HRegion是HBase中分布式存储和负载均衡最小单元(HBase的表数据具体存放的位置)
最小单元就表示不同的HRegion可以分布在不同的 HRegion server上。
一个Region只属于一张表,但是一张表可以有多个Region
HBase自动把表水平划分成多个区域(region),每个region会保存一个表里面某段连续的数据
Region的平分
最开始声明表的时候就会为这个表默认创建一个Region,一个Region只属于一张表,随着时间的推移Region会越来越大 ,当达到阈值10G时,然后Region会1分为2(逻辑上平分,尽量保证数据的完整性)
切分后的其中一个Region转移到其他的HRegionServer上管理
预分区
当table中的行不断增多,就会有越来越多的region。这样一张完整的表被保存在多个Regionserver 上。
为了防止前期数据的处理都集中在一个HRegionServer,我们可以根据自己的业务进行预分区
3.5、Store
定义理解
一个表中的一个列族对应一个Store
一个Store里面分为1个MenStore和0或多个StoreFile
HRegion、Store和columns family之间的关系
HRegion是表获取和分布的基本元素,由一个或者多个Store组成,每个store保存一个columns family。
HFile
HFile是Hbase在HDFS中存储数据的格式,它包含多层的索引,这样在Hbase检索数据的时候就不用完全的加载整个文件。
StoreFile存储在HDFS上之后就称为HFile
3.6、StoreFile
定义理解
StoreFile是文件的硬盘存储,直接存到HDFS上,存到HDFS之后被称为HFile
StoreFile是数据存储文件的映射,对应HDFS上的HFile
表、Region、Store、StoreFile之间的关系
一个table对应多个Region,一个Region对应多个Store,一个Store对应一个MEMStore和多个StoreFile,多个StoreFile内部有序,但是外部无序
集群会设置一些阈值,当达到阈值的时候开始将小文件合并成大文件
3.7、MemStore
定义理解
MemStore是基于内存存放数据,每个Store大概分配128M的空间
HFile中并没有任何Block,数据首先存在于MemStore中。Flush发生时,创建HFile Writer
数据最开始优先写入到MemStore,当flush的时候才会被写入到磁盘中(之前在内存中)
默认情况下,一个MemStore的大小为128M,当客户端向数据库插入数据的时候,当内存使用到128M的时候,直接申请128M的内存空间,数据直接写到新内存中,原来已经满的数据写出到HDFS上,称为HFile
MemStore与 Data Block之间的关系
当操作数据的时候,第一个空的Data Block初始化,初始化后的Data Block中为Header部分预留了空间,Header部分用来存放一个Data Block的元数据信息。
位于MemStore中的KeyValues被一个个append到位于内存中的第一个Data Block中
如果配置了Data Block Encoding,则会在Append KeyValue的时候进行同步编码,编码后的数据不再是单纯的KeyValue模式。
Data Block Encoding是HBase为了降低KeyValue结构性膨胀而提供的内部编码机制
3.8、 Hlog
定义理解
HBase的日志机制,WAL(Write After Log)做任何操作之前先写日志,一个HRegionServer只有一个Log文档
日志也会存储到HDFS上,在任何操作之前先记录日志到HDFS,以后MenStore丢失数据或者RegionServer异常都能够通过日志进行恢复一个RegionServer对应的一个Hlog
HLog文件就是一个普通的Hadoop Sequence File,SequeceFile的Key是HLogKey对象
作用
当memStore达到阈值的时候开始写出到文件之后,会在日志中对应的位置标识一个检查点
WAL记录所有的Hbase数据改变,如果一个RegionServer在MemStore进行FLush的时候挂掉了,WAL可以保证数据的改变被应用到。如果写WAL失败了,那么修改数据的完整操作就是失败的。
图解Hlog在整个HBase中的结构
3.9、Zookeeper
定义理解
HBase的协调服务
作用
主备选举与切换
记录当前集群的状态信息,当主备切换的时候,集群的状态可以被新主节点直接读取到
记录当前集群的数据存放信息
存储HBase的元数据信息
4、HBase集群搭建
忽略
5、HBase操作
hbase的操作也类似于MySQL库、表的增删改查等操作
这里罗列一些常用的hbase操作
通过命令:hbase shell进入hbase(hbase集群启动的情况下)
通过help命令查看帮助命令
通过exit命令退出hbase客户端界面
查看服务器状态:status
查看hbase版本:version
5.1、命名空间操作
创建命名空间
语法:create_namespace ‘命名空间名称’
create_namespace ‘test’
查看命名空间
根据命名空间名称查询
describe_namespace ‘test’
在某命名空间中创建表
语法:create ‘命名空间名称:表名’,‘列族’,‘列族’
create ‘test:tab_test’,‘love’,‘you’
5.2、表操作
创建表
# 语法:create 表名,列族1,列组2,...
# 例如:create 'tabname','column_family01','column_family02'
create 'student','info','grade'
现在先不用创建列,列名是后期插入数据时才定义的。展示表,list:罗列出所有表
hbase:012:0> list
TABLE
student
tab_test
2 row(s)
Took 0.0286 seconds
=> [“student”, “tab_test”]
describe:展示表的详细信息
hbase:013:0> describe 'tab_test'
Table tab_test is ENABLED
tab_test
COLUMN FAMILIES DESCRIPTION
{NAME => 'column_family01', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSION
S => '1', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRES
SION => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BL
OCKSIZE => '65536', REPLICATION_SCOPE => '0'}
{NAME => 'column_family02', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSION
S => '1', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRES
SION => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BL
OCKSIZE => '65536', REPLICATION_SCOPE => '0'}
2 row(s)
Quota is disabled
Took 0.1754 seconds
5.3、列族
增加列族
语法:alter ‘tablename’,‘column_famaily03’
alter ‘student’,‘class’
删除列族
语法:alter 表名, ‘delete’ => 列族名
我们删除student表的class列族试试:
alter 'student','delete'=>'class'
alter 'student',{NAME=>'class',METHOD=>'delete'}
hbase:015:0> alter 'student','delete'=>'class'
Updating all regions with the new schema...
1/1 regions updated.
Done.
Took 2.8162 seconds
hbase:016:0> describe 'student' # 展示student的详细信息,发现class列族已经没有了
Table student is ENABLED
student
COLUMN FAMILIES DESCRIPTION
{NAME => 'grade', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSIONS => '1',
KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRESSION => 'N
ONE', TTL => 'FOREVER', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE =>
'65536', REPLICATION_SCOPE => '0'}
{NAME => 'info', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSIONS => '1', K
EEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRESSION => 'NO
NE', TTL => 'FOREVER', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE =>
'65536', REPLICATION_SCOPE => '0'}
2 row(s)
Quota is disabled
Took 0.1113 seconds
删除表
表创建成功后,默认状态是enable,即“使用中”的状态,删除表之前需先设置表为“关闭中”。
disable ‘student’
再使用关键字drop删除表
drop ‘student’
对数据的操作
插入(跟新)数据,由于hbase有时间戳版本这一概念,所以跟新操作跟插入操作一样,但是旧数据不会就消失了,旧数据会被当做老版本依旧存放于表中。语法:put ‘表名’,‘行键’,‘列族:列名’,‘值
put 'student','student_01','grade:math','82'
put 'student','student_01','grade:english','96'
put 'student','student_01','info:name','lisi'
put 'student','student_01','info:addr','chongqing'
查看数据(get|scan),语法:get: 只查看某个行键的数据 get ‘表名’ ,‘行键’
scan:查看表的所有数据 scan ‘表名’
说明:scan全表扫描与get获取到的数据都是目前时间戳最新的数据。
我们如何查看老版本的信息呢:scan时可以设置是否开启RAW模式,开启RAW模式会返回已添加删除标记但是未实际进行删除的数据
语法:scan ‘表名’,{RAW=>true,VERSIONS=>你想展示多少个版本的信息就写几}
scan ‘student’,{RAW=>true,VERSIONS=>2}
删除一行数据中的列值
delete ‘表名’,‘行键’,‘列族:列名’ # 不指定时间戳的话,默认删除当前最新版本的记录
或deleteall ‘表名’,‘行键’,‘列族:列名’ # 删除指定单元格所有版本的记录
delete ‘student’,‘student_01’,‘info:addr’
hbase:036:0> get 'student','student_01' # 第一次获取数据,zhangsan的地址是beijing
COLUMN CELL
grade:english timestamp=2022-09-22T11:32:38.926, value=45
grade:math timestamp=2022-09-22T11:32:38.540, value=99
info:addr timestamp=2022-09-22T11:32:40.774, value=beijing
info:name timestamp=2022-09-22T11:32:39.091, value=zhangsan
1 row(s)
Took 0.0510 seconds
hbase:039:0> delete 'student','student_01','info:addr' # 删除掉了新版本的addr记录
Took 0.1579 seconds
hbase:040:0> get 'student','student_01' # 第二次获取数据,zhangsan的地址是chongqing(旧版本)
COLUMN CELL
grade:english timestamp=2022-09-22T11:32:38.926, value=45
grade:math timestamp=2022-09-22T11:32:38.540, value=99
info:addr timestamp=2022-09-22T11:16:53.171, value=chongqing
info:name timestamp=2022-09-22T11:32:39.091, value=zhangsan
1 row(s)
Took 0.0583 seconds
hbase:041:0> delete 'student','student_01','info:addr' # 再次删除掉当前最新版本也就是之前的旧版本chongqing
Took 0.0370 seconds
hbase:042:0> get 'student','student_01' # 由于只存入了两个版本的信息,两条addr的信息都被删除后就,没有数据展示了
COLUMN CELL
grade:english timestamp=2022-09-22T11:32:38.926, value=45
grade:math timestamp=2022-09-22T11:32:38.540, value=99
info:name timestamp=2022-09-22T11:32:39.091, value=zhangsan
1 row(s)
Took 0.0424 seconds
deleteall ‘student’,‘student_01’,‘grade:math’
hbase:048:0> get 'student','student_01'
COLUMN CELL
grade:english timestamp=2022-09-22T11:32:38.926, value=45
grade:math timestamp=2022-09-22T11:32:38.540, value=99
info:name timestamp=2022-09-22T11:32:39.091, value=zhangsan
1 row(s)
Took 0.0599 seconds
hbase:049:0> deleteall 'student','student_01','grade:math' # 一次性删除所有版本的记录
Took 0.0256 seconds
hbase:050:0> get 'student','student_01'
COLUMN CELL
grade:english timestamp=2022-09-22T11:32:38.926, value=45
info:name timestamp=2022-09-22T11:32:39.091, value=zhangsan
1 row(s)
Took 0.0303 seconds
删除一行数据(deleteall),deleteall ‘表名’,‘行键’ deleteall ‘student’,‘student_01’
5.4、HBase读写流程
写流程,先回顾一下我们的节点规划:
接下来以我们搭建好的hbase集群与我们刚才上文对表的操作来讲讲当我们提交了put ‘student’,‘student_01’,‘grade:math’,'82’命令后hbase到底做了什么(建议初学者将下图着重掌握):
写入流程
由客户端发起写入数据的请求, 首先会先连接zookeeper
从zookeeper中获取 hbase:meta表(meta-region-server)被哪一个个regionServer所管理
我们也可以登录zookeeper客户端(zkCli.sh)后使用命令:get /hbase/meta-region-server 获取meta表存储的信息,如图现在meta表在node002上。
连接meta表对应的RegionServer地址(假设是node001), 从meta表获取当前要写入的表对应region被那个RegionServer所管理(一般只会返回一个RegionServer地址, 除非一次性写入多条数据)
连接对应要写入RegionServer的地址, 开始写入数据, 将数据首先会写入到HLog中,然后将数据写入到对应Region的对应Store模块的MemStore中(有可能会写入到MemStore), 当这两个地方都写入完成后, 客户端认为数据写入完成了(即hbase服务端与客户端的一次交流就结束了)
服务端写入过程: 异步操作(可能客户端执行N多次写入后, 服务端才开始对之前的数据进行操作)
随着客户端不断的写入操作, memstore中数据会越来越多, 当内存中数据达到阈值(128M / 1h)后, 就会触发flush刷新机制, 将数据<最终>刷新到HDFS上形成StoreFile(小Hfile)文件.
随着不断的刷新, 在HDFS上StoreFile文件会越来越多, 当StoreFlie文件数量达到阈值(3个及以上)后, 就会触发compact合并压缩机制, 将多个StoreFlie文件<最终>合并为一个大的HFile文件
随着不断的合并, 大的HFile也会越来越大, 当大HFile达到一定的阈值(<最终>10GB)后, 就会触发Split分裂机制, 将大HFile进行一分为二,形成两个新的大HFile, 同时管理这个大HFile的Region也会形成两个新的Region, 形成的两个新的Region和两个新的大HFile 进行一对一的管理即可, 原来的Region和原来的大的HFile就会下线删除掉。
5.5 、读流程
读取流程
客户端发起读取数据的请求, 首先会先连接zookeeper
从zookeeper中获取一个 hbase:meta表 被那个RegionServer所管理着
连接meta表对应RegionServer, 从meta表获取当前要读取的这个表对应的Region是那些, 并且这些Region对应的RegionServer是谁当表有多个Region的时候: 如果执行的Get操作获取某一条数据, 只会返回一个RegionServer的地址;如果执行的Scan操作, 会将所有的Region对应RegionServer地址全部返回(前三步与写流程差不多)。
连接要读取表对应的RegionServer, 从RegionServer上开始获取数据即可:
读取顺序:
MemStore —> blockCache(缓存) —> StoreFlie(小HFile) —>大HFile
当从后续的文件中读取到数据后, 会将这一部分存储到缓存中
如果执行Scan操作, blockCache基本没有太大意义
6、javaAPI访问HBase数据库
在操作之前确保hbase集群正常运行!
编程实现
环境介绍
使用的是IDEA+Maven来进行测试
Maven的pom.xml中hbase依赖如下:
org.apache.hbase,hbase-client 2.4.5
org.apache.hbase.hbase-common 2.4.5
org.apache.hbase.hbase-protocol 2.4.5
org.apache.hbase hbase-server 2.4.5
junit.junit 4.12
获取所有表
package com.libing.hbase;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import java.io.IOException;
/**
* @author liar
* @version 1.0
* @date 2022/9/24 13:48
*/
public class GetAllTableTest {
public static Configuration cfg = HBaseConfiguration.create();
public static Connection conn;
public static void main(String[] args) throws IOException {
cfg.set("hbase.zookeeper.quorum","192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181");
//cfg.set("hbase.zookeeper.quorum","node001:2181,node002:2181,node003:2181");
//创建数据库连接
conn = ConnectionFactory.createConnection(cfg);
/**
* Admin 用于管理HBase数据库的表信息
* org.apache.hadoop.hbase.client.Admin是为管理HBase而提供的接口,在Connection
* 实例调用getAdmin()和close()方法期间有效。
*/
Admin admin = conn.getAdmin();
for(TableName name : admin.listTableNames())
{
System.out.println(name);
}
//关闭连接
conn.close();
}
}
注:这里运行报错Caused by: java.net.UnknownHostException: can not resolve node001,16000,1663…的需要在Windows的C:\Windows\System32\drivers\etc\hosts文件中添加对应的域名解析(我也不知道为啥,反正我的加了解决了报错):192.168.1.101 node001
言归正传:
org.apache.hadoop.hbase.client.Admin是为管理HBase而提供的接口,在Connection
实例调用getAdmin()和close()方法期间有效。使用Admin接口可以实现的主要
HBase Shell命令包括create, list, drop, enable, disable, alter,相应java方法如下表:
创建表
package com.libing.hbase;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import java.io.IOException;
/**
* @author liar
* @version 1.0
* @date 2022/9/24 15:16
*/
public class CreateTableTest {
public static Configuration cfg = HBaseConfiguration.create();
public static Connection conn;
public static void main(String[] args) throws IOException {
cfg.set("hbase.zookeeper.quorum","192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181");
//创建数据库连接
conn = ConnectionFactory.createConnection(cfg);
/**
* Admin 用于管理HBase数据库的表信息
* org.apache.hadoop.hbase.client.Admin是为管理HBase而提供的接口,在Connection
* 实例调用getAdmin()和close()方法期间有效。
*/
Admin admin = conn.getAdmin();
String tableName = "create_test";
String columFamily1 = "create_test_family1";
String columFamily2 = "create_test_family2";
HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf(tableName));
HColumnDescriptor hColumnDescriptor1 = new HColumnDescriptor(columFamily1);
HColumnDescriptor hColumnDescriptor2 = new HColumnDescriptor(columFamily2);
tableDescriptor.addFamily(hColumnDescriptor1).addFamily(hColumnDescriptor2);
admin.createTable(tableDescriptor);
for (TableName tables :admin.listTableNames()) {
System.out.println(tables);
}
//关闭连接
conn.close();
}
}
通过hbase客户端也发现这张表的列族也是按照要求创建好了的。
Table接口用于和HBase中的表进行通信,代表了该表的实例,使用Connection的getTable(TableName tableName)方法可以获取该接口的实例,用于获取、添加、删除、扫描HBase表中的数据。
Table接口包含的主要方法如下:
这里不对每一个方法进行展示,不然文章就太臃肿了,读者视情况可以自行测试。
7、HBase常用性能优化
在线的OLTP系统对响应时间的要求非常高。当HBase为OLTP系统提供在线实时的数据存储时,响应时间以及吞吐量尤为重要。某一个配置项的不妥当可能直接造成线上HBase集群整体响应超时,然后应用服务器线程池耗尽,最终导致服务不可用,而一些简单的配置改动可能会让HBase集群性能提升数倍,因此HBase在线调优对HBase在企业生产环境的应用非常重要。
7.1、客户端调优
7.1.1、设置客户端写入缓存
如果业务能够容忍数据丢失,如一些日志数据,那么客户端写入HBase表时可以采取批量缓存的方式,数据先缓存在客户端,当达到配置的阈值时再批量提交到服务器端。注意,如果客户端重启或者宕机,则这部分缓存的数据会丢失。
HBase 1.x版本API只需要通过设置HTable.setAutoFlush(false),再设置缓存大小,即可在实现客户端写入时先在客户端缓存。
表级别设置写入缓存如下:
HTable.setAutoFlush(false);
HTable.setWriteBufferSize(1024 * 1024 * 10);// 缓存大小10MB
package com.mt.hbase.chpt09.client;
import com.mt.hbase.chpt05.rowkeydesign.RowKeyUtil;
import com.mt.hbase.connection.HBaseConnectionFactory;
import com.mt.hbase.constants.Constants;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.BufferedMutator;
import org.apache.hadoop.hbase.client.BufferedMutatorParams;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
importjava.util.ArrayList;
import java.util.List;
/**
* 批量、异步向HBase写入数据
*/
private static void doBufferedMutator() {
final BufferedMutator.ExceptionListener listener = new BufferedMutator. ExceptionListener() {
@Override
public void onException(RetriesExhaustedWithDetailsException e, BufferedMutator mutator) {
for (int i = 0; i < e.getNumExceptions(); i++){
System.out.println("error mutator: " + e.getRow(i));
}
}
};
BufferedMutatorParams params = new BufferedMutatorParams(TableName.valueOf (Constants.TABLE)).listener(listener);
params.writeBufferSize(10*1024*1024);
try {
Connection conn = HBaseConnectionFactory.getConnection();
BufferedMutator mutator = conn.getBufferedMutator(params);
List<Put> actions = new ArrayList<Put>();
Put put = new Put(Bytes.toBytes("rowkey1"));
put.addColumn(Bytes.toBytes(Constants.CF_PC), Bytes.toBytes(Constants. COLUMN_VIEW),Bytes.toBytes("value1"));
actions.add(put);
mutator.mutate(actions);
mutator.close();
conn.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
7.1.2、设置合适的扫描缓存
Scan操作一般需要查询大量的数据,如果一次RPC请求就将所有数据都加载到客户端,则请求时间会比较长。同时,由于数据量大,网络传输也容易出错,因此HBase Scan API提供了一个分批拉取数据然后缓存到客户端的功能。每次ResultScanner.next()被调用时,只要当前客户端扫描缓存数据为空,HBase客户端就会去服务器端拉取下一批数据。如果缓存值设置得过大,每次获取的数据过多,那么容易造成请求超时,甚至由于数据过大造成内存OutOfMemory异常;如果缓存值设置得过小,就会增加一些额外的RPC请求。因此一般会根据业务需求进行平衡,设置一个最适合业务的值,如1000等。设置批量拉取数据的代码如下:
Scan scan = new Scan();
scan.setCaching(1000);//表示一个RPC请求读取的数据条数
7.1.3、跳过WAL写入
预写入日志(WAL)可用于分区服务器异常恢复,在第7章有详细介绍。数据的写入操作需要等待WAL刷新写入文件系统,因此,对于一些能够容忍部分数据丢失的业务,如日志系统等,可以跳过WAL写入以提高写入速度,代码如下:
Put.setDurability(Durability.SKIP_WAL);
Delete.setDurability(Durability.SKIP_WAL);
7.1.4、设置重试次数与间隔
当 HBase 客户端请求在服务器端出错并抛出异常后,如果抛出的异常不是DoNotRetryIOException类的子类,那么客户端会发起重试。客户端超时时间、重试的间隔与次数需要配置合理,否则容易造成分区服务器请求雪崩,进而导致应用服务器线程池线程耗尽,系统无法正常响应。以下两个配置项决定了重试次数以及重试间隔。
(1)hbase.client.pause:重试的休眠时间系数。
(2)hbase.client.retries.number:最大重试次数,默认为35,建议减少,如5。
重试间隔为休眠时间系数乘A,其中A=RETRY_BACKOFF[重试次数],RETRY_BACKOFF是一个常数数组,代码如下:
public static final int [] RETRY_BACKOFF = {1, 2, 3, 5, 10, 20, 40, 100, 10 0,
100, 100, 200, 200};
如果设置hbase.client.pause=1000,hbase.client.retries.number=10,那么10次重试间隔为1、2、3、5、10、20、40、100、100、100,单位为秒(s)。
如果重试次数超过了RETRY_BACKOFF数组大小,则A=200(数组最后一个元素)。
HBase 2.4.9源代码中描述了如下几种可重试的异常:
NotServingRegionException;
RegionServerStoppedException;
OutOfOrderScannerNextException;
UnknownScannerException;
ScannerResetException。
7.1.5、选用合适的过滤器
Scan请求通常需要扫描大量的数据行,过滤器可以用来在服务器端过滤掉一部分不需要的数据,从而减少在服务器端和客户端之间传输的数据量。
使用过滤器来提升Scan请求性能的手段:
(1)组合使用KeyOnlyFilter、FirstKeyOnlyFilter。KeyOnlyFilter可以使得服务器端返回的数据量只包含行键,FirstKeyOnlyFilter可以减少服务器端扫描的数据量,只需要扫描到每行的第一列。
(2)避免使用包含大量Filter的FilterList。假如在用户行为日志管理系统中查询出商品ID从1001到9999的订单数据,此时使用包含多个SingleColumnValueFilter的FilterList可以满足需求(多个过滤器的关系为Operator.MUST_PASS_ONE,类似于MySQL中的OR),但是性能可能会非常低,此时采用扫描用户所有的订单数据到客户端过滤的性能反而会更好。我试过在7000多行(每行数据大小约200字节)的用户数据中使用SingleColumnValueFilter,100个以内使用FilterList的过滤操作速度会很快,一旦超过100个,使用Scan设置开始行键和结束行键扫描用户所有的数据到内存再过滤反而会更快。当然,这与用户的数据量大小有关,并不是说100就是一个最优数字,不同业务可以通过实验得到不同的最优数字。
(3)过滤器尽量使用字节比较器,因为HBase数据以字节形式存储。
7.2、服务端优化
7.2.1、 COMPRESSION
配置数据的压缩算法,这里的压缩是HFile中block级别的压缩。对于可以压缩的数据,配置压缩算法可以有效减少磁盘的IO,从而达到提高性能的目的。但是并不是所有数据都可以进行有效压缩,如图片,因为图片一般是已经压缩后的数据,所以压缩效果有限。常用的压缩算法是SNAPPY,因为它有较好的压缩和解压速度和可以接受的压缩率。
7.2.2、 IN_MEMORY
配置表的数据优先缓存在内存中,这样可以有效提升读取的性能。适合小表,而且需要频繁进行读取操作的。
7.2.3、 预分区
在HBase中数据是分布在各个Region中的,每个Region都负责一个起始RowKey和结束Rowkey的范围,在向HBase中写数据的时候,会根据RowKey请求到对应的Region上,如果写请求都集中在某一个Region或某几个Region上的时候,性能肯定不如写请求均匀分布在各个Region上好。默认情况下,创建的HBase的只有一个Region分区,会随着数据量的变大,进行split,拆分成多个Region,最开始的性能肯定会很不好
建议在设计HBase的的时候,进行预分区,并设计一个良好的Rowkey生成规则,尽量将数据分散到各个Region上,那样在进行HBase的读写的时候,对性能会有很好的改善。
7.2.4、 合理设置WAL存储级别
数据在写入HBase的时候,先写WAL,再写入缓存。通常情况下写缓存延迟很低,WAL机制一方面是为了确保数据即使写入缓存后数据丢失也可以通过WAL恢复,另一方面是为了集群之间的复制。默认WAL机制是开启的,并且使用的是同步机制写WAL。
如果业务不特别关心异常情况下部分数据的丢失,而更关心数据写入吞吐量,可考虑关闭WAL写,这样可以提升2~3倍数据写入的吞吐量。如果业务不能接受不写WAL,但是可以接受WAL异步写入,这样可以带了1~2倍性能提升。HBase中可以通过设置WAL的持久化等级决定是否开启WAL机制、以及HLog的落盘方式。
WAL的持久化等级分为如下四个等级:
SKIP_WAL:只写缓存,不写HLog日志。这种方式因为只写内存,因此可以极大的提升写入性能,但是数据有丢失的风险。在实际应用过程中并不建议设置此等级,除非确认不要求数据的可靠性。
ASYNC_WAL:异步将数据写入HLog日志中。
SYNC_WAL:同步将数据写入日志文件中,需要注意的是数据只是被写入文件系统中,并没有真正落盘,默认。
FSYNC_WAL:同步将数据写入日志文件并强制落盘。最严格的日志写入等级,可以保证数据不会丢失,但是性能相对比较差。
同样,除了在创建表的时候直接设置WAL存储级别,也可以通过客户端设置WAL持久化等级,代码: