什么是HBase?
是⼀个分布式、海量、列式、⾮关系型 数据库系统,可以提供超⼤规模数据集的实时随机读写
列存储的优点:
- 1)减少存储空间占⽤。
- 2)⽀持好多列
HBase的特点
- 海量存储:底层基于
HDFS
存储海量数据 - 列式存储:
HBase
表的数据是基于列族进⾏存储的,⼀个列族包含若⼲列 - 极易扩展:底层依赖
HDFS
,当磁盘空间不⾜的时候,只需要动态增加DataNode
服务节点就可以 - ⾼并发:⽀持⾼并发的读写请求
- 稀疏:稀疏主要是针对HBase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占⽤存储空间的。
- 数据的多版本:
HBase
表中的数据可以有多个版本值,默认情况下是根据版本号去区分,版本号就是插⼊数据的时间戳 - 数据类型单⼀:所有的数据在
HBase
中是以字节数组进⾏存储
HBase应用场景
-
HBase
适合海量明细数据的存储,并且后期需要有很好的查询性能(单表超千万、上亿,且并发要求⾼)
HBase数据模型
-
HBase
中的每一张表就是所谓的BigTable
。 -
BigTable
会存储一系列的行记录,行记录有三个基本类型的定义:-
RowKey
:是行在BigTable
中的唯一标识 -
TimeStamp
:是每一次数据操作对应关联的时间戳,可以看作SVN
的版本 -
Column
:定义为:
-
逻辑存储模型
- RowKey:与
NoSQL
数据库一样,RowKey
是用来检索记录的主键- 访问
HBase Table
中的行三种方式:- 通过单个
RowKey
访问 - 通过
RowKey
的Range
- 全表扫描
- 通过单个
- 注意:
-
RowKey
行键可以任意字符串(最大长度64KB,实际应用中长度一般为10-100bytes),在HBase
内部RowKey
保存为字节数组。 - 存储时,数据按照
RowKey
的字典序(byte order
)排序存储,设计key
时,要充分了解这个特性,将经常一起读取的行存放在一起。 - 行的一次读写是原子操作(不论一次读写多少列)
-
- 访问
- 列簇:
HBase
表中的每个列,都归属于某个列簇,列簇是表的schema
的一部分(而列不是),必须在使用表之前定义。- 列名都以列簇作为前缀,例如:
courses:history
,courses:math
都属于courses
这个列簇。 - 访问控制,磁盘和内存的使用统计都是在列簇层面进行的。
- 实际应用中,列簇上的控制权限能帮助我们管理不同类型的应用:我们允许一些应用可以添加新的基本数据
- 列名都以列簇作为前缀,例如:
- 时间戳:
HBase
中通过row
和columns
确定的为一个存储单元称为cell
。每个cell
都保存着同一份数据的多个版本。版本通过时间戳来索引- 时间戳的类型是64位整型,时间戳可以由
HBase
在写入时自动赋值,此时时间戳是精确到毫秒的当前系统时间。时间戳也可以由客户显示赋值
- 时间戳的类型是64位整型,时间戳可以由
- Cell:由
{row key, column(=+), version}
唯一确定的单元-
Cell
中的数据是没有类型的,全部是字节码形式存储
-
HBase整体架构
-
HBase
仍然采用Master/Slave
架构
Zookeeper:
- 实现了
HMaster
的⾼可⽤ - 保存了
HBase
的元数据信息,是所有HBase
表的寻址⼊⼝ - 对
HMaster
和HRegionServer
实现了监控
HMaster(Master):
- 为
HRegionServer
分配Region
- 维护整个集群的负载均衡
- 维护集群的元数据信息
- 发现失效的
Region
,并将失效的Region
分配到正常的HRegionServer
上
HRegionServer(RegionServer)
- 负责管理
Region
- 接受客户端的读写数据请求
- 切分在运⾏过程中变⼤的
Region
Region
- 每个
HRegion
由多个Store
构成, - 每个
Store
保存⼀个列族(Columns Family
),表有⼏个列族,则有⼏个Store
, - 每个
Store
由⼀个MemStore
和多个StoreFile
组成,MemStore
是Store
在内存中的内容,写到⽂件后就是StoreFile
。StoreFile
底层是以HFile
的格式保存。
Hbase环境安装
tar -zxvf hbase-1.3.1-bin.tar.gz -C /opt/servers
mv /opt/servers/hbase-1.3.1 /opt/servers/hbase
# 配置环境变量
export HBASE_HOME=/opt/servers/hbase
export PATH=$PATH:$HBASE_HOME/bin
修改配置⽂件
- 需要把
hadoop
中的配置core-site.xml
、hdfs-site.xml
拷⻉到hbase
安装⽬录下的conf
⽂件夹中
ln -s /opt/servers/hadoop-2.9.2/etc/hadoop/core-site.xml /opt/servers/hbase/conf/core-site.xml
ln -s /opt/servers/hadoop-2.9.2/etc/hadoop/hdfs-site.xml /opt/servers/hbase/conf/hdfs-site.xml
- 修改
hbase-env.sh
#添加java环境变量
export JAVA_HOME=/opt/servers/jdk1.8
#指定使⽤外部的zk集群
export HBASE_MANAGES_ZK=FALSE
- 修改
hbase-site.xml
hbase.rootdir
hdfs://os1:9000/hbase
hbase.cluster.distributed
true
hbase.zookeeper.quorum
os1:2181,os2:2181,os3:2181
- 修改
regionservers
⽂件
#指定regionserver节点
os1
os2
os3
-
hbase
的conf
⽬录下创建⽂件backup-masters (Standby Master)
echo 'os2' > backup-masters
- 分发hbase⽬录和环境变量到其他节点
HBase集群的启动和停⽌
- 前提条件:先启动
hadoop
和zookeeper
集群 - 启动:
start-hbase.sh
- 停⽌:
stop-hbase.sh
HBase shell 基本操作
- 进入
hbase
客户端
hbase shell
创建⼀张表, 包含两个列族
create 'test1', 'base_info', ''
create 'test2', {NAME => 'base_info', VERSIONS => '3'},{NAME => 'extra_info',VERSIONS => '3'}
添加数据操作
put 'test1', 'rk1', 'base_info:name', 'test'
put 'test1', 'rk1', 'base_info:age', 30
查询数据
- 通过
rowkey
进⾏查询
get 'test1', 'rk1'
- 查看
rowkey
下⾯的某个列族的信息
get 'test1', 'rk1', 'base_info'
get 'test1', 'rk1', 'base_info:name', 'base_info:age'
- 查看
rowkey
指定多个列族的信息
get 'test1', 'rk1', 'base_info', 'extra_info'
get 'test1', 'rk1', {COLUMN => ['base_info:name', 'extra_info:address']}
- 指定
rowkey
与列值查询
get 'test1', 'rk1', {FILTER => "ValueFilter(=, 'binary:test')"}
- 指定
rowkey
模糊查询
get 'test', 'rk1', {FILTER => "(QualifierFilter(=,'substring:e'))"}
- 查询所有数据
scan 'test1'
- 列族查询
scan 'test1', {COLUMNS => 'base_info', RAW => true, VERSIONS => 3}
- 指定
rowkey
模糊查询
scan 'test1',{FILTER=>"PrefixFilter('rk')"}
更新
put 'test1', 'rk1', 'base_info:name', 'test2'
删除数据和表
- 指定
rowkey
以及列名进⾏删除
delete 'test1', 'rk1', 'base_info:name'
- 删除列族
alter 'test1', 'delete' => 'base_info'
- 清空表数据
truncate 'test1'
- 删除表:先
disable
再drop
disable 'test1'
drop 'test1'
HBase原理深⼊
HBase读数据流程
- 1)⾸先从
zk
找到meta
表的region
位置,然后读取meta
表中的数据,meta
表中存储了⽤户表的region
信息 - 2)根据要查询的
namespace
、表名和rowkey
信息。找到写⼊数据对应的region
信息 - 3)找到这个
region
对应的regionServer
,然后发送请求 - 4)查找对应的
region
- 5)先从
memstore
查找数据,如果没有,再从BlockCache
上读取
HBase
上Regionserver
的内存分为两个部分- ⼀部分作为
Memstore
,主要⽤来写; - 另外⼀部分作为
BlockCache
,主要⽤于读数据;
- ⼀部分作为
- 6)如果
BlockCache
中也没有找到,再到StoreFile
上进⾏读取- 从
storeFile
中读取到数据之后,不是直接把结果数据返回给客户端, ⽽是把数据先写⼊到BlockCache
中,⽬的是为了加快后续的查询; - 然后在返回结果给客户端。
- 从
HBase写数据流程
- 1)⾸先从
zk
找到meta
表的region
位置,然后读取meta
表中的数据,meta
表中存储了⽤户表的region
信息 - 2)根据
namespace
、表名和rowkey
信息。找到写⼊数据对应的region
信息 - 3)找到这个
region
对应的regionServer
,然后发送PUT
请求 - 4)把数据分别写到
HLog(write ahead log)
和memstore
各⼀份 - 5)
memstore
达到阈值后把数据刷到磁盘,⽣成storeFile
⽂件 - 6)删除
HLog
中的历史数据
HBase的flush(刷写)及compact(合并)机制
Flush机制
- 当
memstore
的⼤⼩超过这个值的时候,会flush
到磁盘,默认为128M
hbase.hregion.memstore.flush.size
134217728
- 当
memstore
中的数据时间超过1⼩时,会flush
到磁盘
hbase.regionserver.optionalcacheflushinterval
3600000
-
HregionServer
的全局memstore
的⼤⼩,超过该⼤⼩会触发flush
到磁盘的操作,默认是堆⼤⼩的40%
hbase.regionserver.global.memstore.size
0.4
- ⼿动
flush
flush tableName
阻塞机制
-
Hbase
中是周期性的检查是否满⾜以上标准满⾜则进⾏刷写,但是如果在下次检查到来之前,数据疯狂写⼊Memstore
中,会触发阻塞机制,此时⽆法写⼊数据到Memstore
,数据⽆法写⼊Hbase
集群- 1)
memstore
中数据达到512MB
计算公式:
hbase.hregion.memstore.flush.size
刷写的阀值,默认是134217728
,即128MB
。hbase.hregion.memstore.block.multiplier
是⼀个倍数,默认是4
。-
2)
RegionServer
全部memstore
达到规定值-
hbase.regionserver.global.memstore.size.lower.limit
是0.95
, -
hbase.regionserver.global.memstore.size
是0.4
, - 堆内存总共是
16G
, - 触发刷写的阈值是:
6.08GB
触发阻塞的阈值是:6.4GB
-
- 1)
Compact合并机制
- 在
hbase
中主要存在两种类型的compact
合并
minor compact ⼩合并
- 在将
Store
中多个HFile(StoreFile)
合并为⼀个HFile
- 这个过程中,删除和更新的数据仅仅只是做了标记,并没有物理移除,这种合并的触发频率很⾼。
-
minor compact
⽂件选择标准由以下⼏个参数共同决定
hbase.hstore.compaction.min
3
hbase.hstore.compaction.max
10
hbase.hstore.compaction.min.size
134217728
hbase.hstore.compaction.max.size
9223372036854775807
- 触发条件
-
memstore flush
- 在进⾏
memstore flush
前后都会进⾏判断是否触发compact
- 在进⾏
- 定期检查线程
- 周期性检查是否需要进⾏
compaction
操作 - 由参数:
hbase.server.thread.wakefrequency
决定,默认值是10000 millseconds
- 周期性检查是否需要进⾏
-
major compact ⼤合并
- 合并
Store
中所有的HFile
为⼀个HFile
- 这个过程有删除标记的数据会被真正移除,同时超过单元格
maxVersion
的版本记录也会被删除。 - 合并频率⽐较低,默认
7
天执⾏⼀次,并且性能消耗⾮常⼤,建议⽣产关闭(设置为0
),在应⽤空闲时间⼿动触发。 - ⼀般可以是⼿动控制进⾏合并,防⽌出现在业务⾼峰期。
- 这个过程有删除标记的数据会被真正移除,同时超过单元格
major compaction触发时间条件
hbase.hregion.majorcompaction
604800000
⼿动触发
##使⽤major_compact命令
major_compact tableName
Region 拆分机制
-
Region
中存储的是⼤量的rowkey
数据 ,当Region
中的数据条数过多的时候,直接影响查询效率 - 当
Region
过⼤的时候,HBase
会拆分Region
, 这也是Hbase
的⼀个优点
拆分策略
ConstantSizeRegionSplitPolicy
- 0.94版本前默认切分策略
- 当
region
⼤⼩⼤于某个阈值(hbase.hregion.max.filesize=10G
)之后就会触发切分 - ⼀个
region
等分为2个region
。 - 弊端:切分策略对于⼤表和⼩表没有明显的区分。
- 阈值(
hbase.hregion.max.filesize
)设置较⼤对⼤表⽐较友好,但是⼩表就有可能不会触发分裂,极端情况下可能就1个,这对业务来说并不是什么好事。 - 如果设置较⼩则对⼩表友好,但⼀个⼤表就会在整个集群产⽣⼤量的
region
,这对于集群的管理、资源使⽤、failover
来说都不是⼀件好事。
- 阈值(
IncreasingToUpperBoundRegionSplitPolicy
- 0.94版本~2.0版本前默认切分策略
- ⼀个
region
⼤⼩⼤于设置阈值就会触发切分。 - 阈值在⼀定条件下不断调整,调整规则和
region
所属表在当前regionserver
上的region
个数有关系. -
region split
的计算公式是:
regioncount^3 * 128M * 2,当region达到该size的时候进⾏split
例:
第⼀次split:1^3 * 256 = 256MB
第⼆次split:2^3 * 256 = 2048MB
第三次split:3^3 * 256 = 6912MB
第四次split:4^3 * 256 = 16384MB > 10GB,因此取较⼩的值10GB
SteppingSplitPolicy
- 2.0默认版本
- 依然和待分裂
region
所属表在当前regionserver
上的region
个数有关系- 如果
region
个数等于1,切分阈值为flush size * 2 - 否则为
MaxRegionFileSize
- 如果
KeyPrefixRegionSplitPolicy
- 根据
rowKey
的前缀对数据进⾏分组 - 这⾥是指定
rowKey
的前多少位作为前缀
⽐如
rowKey
都是16位的,指定前5位是前缀,那么前5位相同的rowKey
在进⾏region split
的时候会分到相同的region
中
DelimitedKeyPrefixRegionSplitPolicy
- 保证相同前缀的数据在同⼀个
region
中
例如rowKey的格式为:
userid_eventtype_eventid
指定的delimiter
为_
,则split的的时候会确保userid
相同的数据在同⼀个region中。
DisabledRegionSplitPolicy
- 不启⽤⾃动拆分, 需要指定⼿动拆分
RegionSplitPolicy的应⽤
-
Region
拆分策略可以全局统⼀配置,也可以为单独的表指定拆分策略。- 通过
hbase-site.xml
全局统⼀配置(对hbase
所有表⽣效)
hbase.regionserver.region.split.policy org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicy - 通过
Java API
为单独的表指定Region
拆分策略
HTableDescriptor tableDesc = new HTableDescriptor("test1"); tableDesc.setValue(HTableDescriptor.SPLIT_POLICY, IncreasingToUpperBoundRegionSplitPolicy.class.getName()); tableDesc.addFamily(new HColumnDescriptor(Bytes.toBytes("cf1"))); admin.createTable(tableDesc);
- 通过
HBase Shell
为单个表指定Region
拆分策略
create 'test2', {METADATA => {'SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicy'}},{NAME => 'cf1'}
- 通过
HBase表的预分区(region)
为何要预分区?
- 当⼀个
table
刚被创建的时候,Hbase
默认的分配⼀个region
给table
- 这个时候,所有的读写请求都会访问到同⼀个
regionServer
的同⼀个region
中 - 这时,不到负载均衡的效果了,集群中的其他
regionServer
就可能会处于⽐较空闲的状态。
- 这个时候,所有的读写请求都会访问到同⼀个
- 解决这个问题可以⽤
pre-splitting
,在创建table
的时候就配置好,⽣成多个
region
- 增加数据读写效率
- 负载均衡,防⽌数据倾斜
- ⽅便集群容灾调度region
- 每⼀个
region
维护着startRow
与endRowKey
,如果加⼊的数据符合某个region
维护的rowKey
范围,则该数据交给这个region
维
- 每⼀个
⼿动指定预分区
create 'person','info1','info2',SPLITS => ['1000','2000','3000']
- 也可以把分区规则创建于⽂件中
vi split.txt
aa
bb
cc
dd
# 执行
create 'student','info',SPLITS_FILE => '/root/hbase/split.txt'
Region 合并
HBase API应⽤和优化
HBase API客户端操作
HBase 协处理器
问题背景
- 访问
HBase
的⽅式是使⽤scan
或get
获取数据,在获取到的数据上进⾏业务运算 - 但是在数据量⾮常⼤的时候,⽐如⼀个有上亿⾏及⼗万个列的数据集,再按常⽤的
⽅式移动获取数据就会遇到性能问题。 - 客户端也需要有强⼤的计算能⼒以及⾜够的内存来处理这么多的数据。
- 此时就可以考虑使⽤
Coprocessor
(协处理器)- 将业务运算代码封装到
Coprocessor
中并在RegionServer
上运⾏,即在数据实际存储位置执⾏,最后将运算结果返回到客户端。 - 利⽤协处理器,⽤户可以编写运⾏在
HBase Server
端的代码
- 将业务运算代码封装到
Hbase Coprocessor类似概念
- 触发器和存储过程:
- ⼀个
Observer Coprocessor
有些类似于关系型数据库中的触发器,通过它我们可以在⼀些事件(如Get
或是`Scan)发⽣前后执⾏特定的代码。 -
Endpoint Coprocessor
则类似于关系型数据库中的存储过程,因为它允许我们在RegionServer
上直接对它存储的数据进⾏运算,⽽⾮是在客户端完成运算。
- ⼀个
-
MapReduce
:-
MapReduce
的原则就是将运算移动到数据所处的节点。Coprocessor
也是按照相同的原则去⼯作的。
-
-
AOP
:- 如果熟悉
AOP
的概念的话,可以将Coprocessor
的执⾏过程视为在传递请求的过程中对请求进⾏了拦截,并执⾏了⼀些⾃定义代码。
- 如果熟悉
Observer 案例
通过协处理器Observer实现Hbase当中t1表插⼊数据,指定的另⼀张表t2也需要插⼊相对应的数据
create 't1','info'
create 't2','info'
- 思路:通过
Observer
协处理器捕捉到t1
插⼊数据时,将数据复制⼀份并保存到t2
表中 - 添加依赖
- 编写
Observer
public class MyProcessor extends BaseRegionObserver {
@Override
public void prePut(ObserverContext e, Put put, WALEdit edit, Durability durability) throws IOException {
//把自己需要执行的逻辑定义在此处,向t2表插入数据,数据具体是什么内容与Put一样
//获取t2表table对象
final HTable t2 = (HTable) e.getEnvironment().getTable(TableName.valueOf("t2"));
//解析t1表的插入对象put
final Cell cell = put.get(Bytes.toBytes("info"), Bytes.toBytes("name")).get(0);
//table对象.put
final Put put1 = new Put(put.getRow());
put1.add(cell);
t2.put(put1); //执行向t2表插入数据
t2.close();
}
}
- 打成
jar
包,上传HDFS
cd /opt/sw
mv original-hbaseStudy-1.0-SNAPSHOT.jar processor.jar
hdfs dfs -mkdir -p /processor
hdfs dfs -put processor.jar /processor
- 挂载协处理器
describe 't1'
alter 't1',METHOD =>
'table_att','Coprocessor'=>'hdfs://os1:9000/processor/processor.jar|com.test.hbase.processor.MyProcessor|1001|'