31 hbase背景
HBase作为面向列的数据库运行在HDFS之上,HDFS缺乏随机读写操作,HBase正是为此而出现。HBase参考 Google 的 Bigtable 实现,以键值对的形式存储。项目的目标就是快速在主机内数十亿行数据中定位所需的数据并访问它。
2 hbase特点
- 建立在HDFS之上的分布式面向列的数据库
- KV结构数据库,原生不支持标准SQL,属于NOSQL数据库
- 支持快速随机读写海量数据
- 具备HDFS的高容错能力
- 不属于关系型数据库,适合存储非结构化数据,基于列存储
3 hbase和hive的区别
- hive适合统计分析,hive底层执行的是MapReduce,延迟较高
- hbase适合大数据量查询,不适合统计分析,hbase底层采用KV结构存储,可以快速返回数据
- hbase采用列式存储,可以动态扩展列
4 hbase逻辑存储
4.1 hbase的数据单元
hbase是一个稀疏的、多维度、有序的映射表,表中的每个单元是通过行键、列族、列限定符和时间戳组成的索引来标识的,每个单元存储的值是一个未经解释的二进制数组byte[],没有数据类型,当用户在表中存储数据时,每一行都有一个唯一的行键和任意多的列,表的每一行由一个或者多个列族组成,一个列族可以包含任意多个列。
- 行键rowkey
每条数据的主键,rowkey是有序的,采用字典顺序排序,方便快速查找,rowkey的设计至关重要,建表时不指定。
- 列族column family
多个列的组合,建表时指定。
- 列限定符column
归属于一个列族,代表着一列,建表时不指定,可动态扩展列,表达方式为column family:column,例:cf:name,标识在cf列族下的name列。
- 时间戳version
默认为系统时间戳timestamp,代表着一份数据不同时间节点的版本。
- 值value
由rowkey、column family、column、version索引检索得到的唯一值,key<rowkey、column family、column、version> value<唯一的值>,KV结构就由此而来。
4.2 hive行式存储与hbase列式存储
如下示例进行两种数据存储方式的对比:
- hive行式存储
id(主键) | name | age | address |
---|---|---|---|
1001 | user1 | 20 | beijing |
1002 | user2 | 21 | shanghai |
-
hbase列式存储
按照id查询用户信息(id是唯一的), 可以将id作为rowkey
rowkey | cf | column | version | value |
---|---|---|---|---|
1001 | cf | cf:name | t1 | user1 |
1001 | cf | cf:age | t2 | 20 |
1001 | cf | cf:address | t3 | beijing |
1002 | cf | cf:name | t4 | user2 |
1002 | cf | cf:age | t5 | 21 |
1002 | cf | cf:address | t6 | shanghai |
# 建表
create 'xinniu:user_info','cf'
# 添加数据:rowkey: id
put 'xinniu:user_info', '1001', 'cf:name', 'user1'
put 'xinniu:user_info', '1001', 'cf:age', '20'
put 'xinniu:user_info', '1001', 'cf:address', 'beijing'
# 查看表所有数据
scan 'xinniu:user_info'
put 'xinniu:user_info', '1002', 'cf:name', 'user2'
put 'xinniu:user_info', '1002', 'cf:age', '21'
put 'xinniu:user_info', '1002', 'cf:address', 'shanghai'
4.3 hbase version
hbase没有修改语法,当要修改一条数据只需要直接写入即可。
version默认是由系统时间戳表示,当用户重复写入一条数据时,hbase会记录两条数据,因为rowkey、colume family、colume相同,此时则使用version字段进行区分,并且会保留上一个版本的数据,同一条数据不同版本使用version倒序排序,如下:
原数据
rowkey | cf | column | version | value |
---|---|---|---|---|
1001 | cf | cf:name | t1 | user1 |
1001 | cf | cf:age | t2 | 20 |
1001 | cf | cf:address | t3 | beijing |
1002 | cf | cf:name | t4 | user2 |
1002 | cf | cf:age | t5 | 21 |
1002 | cf | cf:address | t6 | shanghai |
此时用户要修改如下数据name的value值
rowkey | cf | colume | value |
---|---|---|---|
1001 | cf | cf:name | newusername |
执行添加数据命令put 'namespace:tablename','1001','cf:name','newusername'后hbase表数据
rowkey | cf | colume | version | value |
---|---|---|---|---|
1001 | cf | cf:name | t7 | newusername |
1001 | cf | cf:name | t1 | user1 |
1001 | cf | cf:age | t2 | 20 |
1001 | cf | cf:address | t3 | beijing |
1001 | cf | cf:name | t4 | user2 |
1001 | cf | cf:age | t5 | 21 |
1001 | cf | cf:address | t6 | shanghai |
当一条数据存在多个版本的时候,查询如果不指定版本,则默认查询最新一条数据,hbase的version也不是可以无限存的,默认版本数为3,可以设置最多存储多少个版本,当超过设定的版本数之后则删除最早版本的数据。
5 hbase架构设计
hbase采用主从(master/slave)架构,由zookeeper、HMaster、HRegionServer三部分组成,底层数据存储在HDFS上。
5.1 hbase组件介绍
5.1.1 HMaster
hbase引入zookeeper,避免HMaster单点问题,HMaster主要负责table和region的管理工作:
1)管理表操作,如:create、alter、drop;
2)管理HRegionServer的负载均衡,调整region分布;
3)region split后,负责新region重分布;
4)在HRegionServer停机后,负责失效的HRegionServer上region的迁移;
5.1.2 HRegionServer
是hbase中最核心的模块,一般HRegionServer会选择和DataNode部署在同一个节点,实现数据本地化,HRS主要功能:
1)维护region,处理这些region的IO请求,如:put、get、scan、delete;
2)regionserver负责切分在运行过程中逐渐变大的region;
5.1.2.1 HRegion/Region
hbase使用rowkey将表水平切割成多个HRegion/Region,从HMaster的角度,每个HRegion都记录了startkey和endkey(第一个Region的startkey为空,最后一个Region的endkey为空),由于rowkey是有序的,因此client端可以通过HMaster快速定位到某个rowkey在哪个HRegion中。
如果建表时未进行预分region,startkey和endkey都为空,随着数据增加,region分裂后会生成新的region,此时startkey和endkey会生成具体的值。
# region: 多个行组成的区域, 由startkey, endkey的范围决定。
# 一个table 可以有多个region
# 默认刚开始创建表时,就一个region, 范围:无限大。
xinniu:testacl, , 1639464190878 . b73797ab3302f91f155845ae9558b996.
其中:
xinniu: 命名空间
testacl:表名
, , 之间:startkey
1639464190878:时间戳
b73797ab3302f91f155845ae9558b996: regionid
# 实际存储:
hdfs://worker-1:8020/hbase/data/xinniu1/tableacl/02f5adff232b37422fc846cc5c1d8328/cf/32cef12e26a54213827b370a813ba52b
/hbase:hbase配置文件中配置的 [hbase的hdfs根目录]
/hbase/data/xinniu1/tableacl: 存储 xinniu1:tableacl表的实际hdfs目录
02f5adff232b37422fc846cc5c1d8328: regionid
5.1.2.2 HStore/Store
每一个列族对应一个HStore/Store,一个HRegion/Region里包含一个或者多个HStore/Store,由此在设计cf时,尽量将同一系列的数据存在一个列族中,便于同一系列的数据都存在同一个region中。
5.1.2.3 Hlog
hbase WAL(write ahead log),在用户发起写请求时先向Hlog写一份,然后再将数据向memstore中写,Hlog数据是写磁盘,为了避免HRegionServer故障时memstore数据丢失,Hlog滚动更新,新数据会加入会对应冲抵掉较早的Hlog数据。
5.1.2.4 Memstore(写缓存)
hbase写缓存,在用户发起写请求时先写入hlog,然后再写入memstore中,当memstore写入达到flush阈值时,将memstore中的数据写到hdfs上(hfile),每个列族对应一个memstore,即一个HStore/Store中只有一个memstore。
5.1.2.5 storefile
当memstore写数据达到设定的阈值之后,会将数据溢写到hdfs,即storefile,内部存储hfile。storefile会进行合并,当storefile经过多次合并后变得已经达到指定规则的分裂阈值,则再进行region分裂。
5.2 HRS、DataNode、table、region、columefamily、Hstore/store、memstore、storefile关系
- 一张表可以拥有多个列族(cf1、cf2、……、cfN),一张表可以分布在不同的HRegionServer上(分布式数据存储);
- 一个HRS上可以分布多个HRegion/region(region由rowkey决定,按照startkey与endkey切分region);
- 一个region中包含多个HStore/store,多少个取决于共有多少个列族,一个列族对应一个HStore/store;
- 一个HStore/store中包含一个memstore(写缓存), 和0 到 N个StoreFile。
- 当memstore写数据达到设定阈值后,将数据溢写到storefile,storefile中存储的hfile文件,storefile保存在HDFS上;
6. hbase读写数据流程
6.1 写数据流程
客户端寻找HRegionServer,及缓存位置信息:
从这个过程中,我们发现客户端会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息,除非某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。
写数据流程:
-
在本地缓存或者在zk中找到该写数据最终需要去的HRegionServer;
-
客户端将写请求发送给相应的HRegionServer,在HRegionServer中它首先会将该写操作写入WAL(Hlog)日志文件中(Flush到磁盘中);
- 写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey,startkey、endkey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中,此时写成功,并返回通知客户端;
6.1.1 写memstore后动作
memstore写数据达到设定阈值后,将memstore中的数据溢写到storefile中,随着时间推移会产生多个storefile,当storefile compact机制触发时将多个小的storefile合并为一个大的storefile,当合并后的storefile达到设定的分裂阈值时,会进行region分裂,将这个大的storefile分裂为两个小的storefile,并且分布在新的region上。
6.1.2 memstore flush机制
1)当某个 memstore 的大小达到了hbase.hregion.memstore.flush.size(默认128M),其所在region的所有memstore都会刷写。当memstore的大小达到了hbase.hregion.memstore.flush.size(默认128M)* hbase.hregion.memstore.block.multiplier(默认2)时,会阻止继续往该memstore写数据。(region级别的)
2)当 HRegionServer 中 memstore 的总量达到
java堆内存 hbase.regionserver.global.memstore.size(默认0.4) hbase.regionserver.global.memstore.size.lower.limit(默认0.95),
RegionServer 会把所有memstore 按照由大到小的顺序依次进行刷写。直到 HRegionServer 中所有memstore的总大小减小到上述值以下。
当 HRegionServer 中 memstore 的总量达到 java堆内存 * hbase.regionserver.global.memstore.size(默认值0.4) (Memstore 所占最大堆空间比例)时,会阻塞往memstore的写操作。(regionserver级别的)
3)到达自动刷写的时间,也会触发memstore flush。自动刷新的时间间隔由该属性进行配置hbase.regionserver.optionalcacheflushinterval(默认1小时,-1 代表不自动刷写)。
4)当前HRegionServer中HLog的大小超过阈值,当前HRegionServer中所有HRegion中的MemStore都会Flush到HDFS中,Flush时按照时间顺序。(regionserver级别的)
6.1.3 storefile compact机制
Compaction 操作分成下面两种:
- Minor Compaction:是选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,对于删除、过期、多余版本的数据不进行清除。
- Major Compaction:是指将所有的StoreFile合并成一个StoreFile,对于删除、过期、多余版本的数据进行清除。优先采用Minor Compaction,如果达不到要求,再执行Major Compaction 。
注:Compaction的触发时机Major Compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发Major Compaction功能,改为手动在业务低峰期触发。
HBase中可以触发compaction的因素有很多,最常见的因素有这么三种:Memstore Flush、后台线程周期性检查、手动触发。
1)Memstore Flush:
每当 RegionServer发生一次Memstore flush操作之后也会进行检查是否需要进行Compaction操作。
2)周期性检查:
通过CompactionChecker线程来定时检查是否需要执行compaction(RegionServer启动时在initializeThreads()中初始化),每隔10000毫秒(可配置)检查一次。
3)手动触发:
手动触发compection通常是为了执行major compaction,执行命令"major_compact '表名'",原因如下:
自动major compaction影响读写性能,因此会选择低峰期手动触发;
执行完alter操作之后希望立刻生效,执行手动触发major compaction;
# put几条数据
put 'xinniu:table1','id01','cf1:name', 'n1'
flush 'xinniu:table1'
scan 'xinniu:table1' # 拿到n1 的时间戳
put 'xinniu:table1','id01','cf1:name', 'n2'
flush 'xinniu:table1'
scan 'xinniu:table1' # 拿到n2 的时间戳
# 用 n1的时间戳指定查询,是能查询到的
get 'xinniu:table1', 'id01', {COLUMN => 'cf1:name', TIMESTAMP => 1639640290699}
# 执行major合并, 由于n1是历史版本,所以n1被合并没了, 只留下n2(最新版本数据)
major_compact 'xinniu:table1'
# 用 n1的时间戳指定查询,查询不到了(n1被合并没了)
get 'xinniu:table1', 'id01', {COLUMN => 'cf1:name', TIMESTAMP => 1639640290699}
6.1.4 region split机制
HRegionServer拆分region的步骤是,先将该region下线,然后拆分,将其子region加入到hbase:meta表中,再将他们加入到原本的HRegionServer中,最后汇报Master。
split前:hbase:meta表有: region_p
-
在region_p对应的hdfs目录下生成.splits目录,用于保存分割后的region信息,如:tablename/region_p/.splits
-
关闭region_p,数据写入并触发flush操作,将写入region的数据全部持久化到磁盘。
- 在region_p对应的.splits目录下,创建两个子目录,并在里面创建两个子region的引用文件。
.split引用文件目录 |
---|
tablename/region_p/.splits/region1/region1引用文件 (splitkey, true) |
tablename/region_p/.splits/region2/region2引用文件 (splitkey, false) |
引用文件用于记录从哪分割(splitkey)和是上半部分(true)还是下半部分(false)
- region_p 分裂为两个子region后,将.split目录下的region1、region2 的目录 copy 到region_p的同级目录下,形成两个新的region。
tablename目录结构 |
---|
tablename/region_p/.splits |
tablename/region1/cf/region1引用文件(splitkey, true) |
tablename/region2/cf/region2引用文件(splitkey, false) |
- 把region_p在hbase:meta表标记下线和split,把两个子region添加到hbase:mate表。
regionname | location | split | offline | split |
---|---|---|---|---|
region_p | /xxxx/xxxx/xxxx/ | true | true | region1,region2 |
region1 | /xxxx/xxxx/xxxx/ | false | false | |
region2 | /xxxx/xxxx/xxxx/ | false | false |
-
开启两个子region,可以接收请求了。此时还没有拉取region_p split的数据。
-
当region发生major compact时,会把父region的split数据拉取到子region,并和当前的子region进行合并,子region拉取完数据后,把引用文件删除。
- hbase会启动线程检查父region是否达到删除的条件,如果达到就删除父region。
删除条件:父region的元数据是split状态and所有子region下的引用文件已删除。
6.1.5 region split策略
可以通过设置RegionSplitPolicy的实现类来指定拆分策略,RegionSplitPolicy类的实现类有:
ConstantSizeRegionSplitPolicy
IncreasingToUpperBoundRegionSplitPolicy
DelimitedKeyPrefixRegionSplitPolicy
KeyPrefixRegionSplitPolicy
DisabledRegionSplitPolicy // 不拆分
其中:
ConstantSizeRegionSplitPolicy:(一刀切)
当一个region中最大store大小大于设置阈值(hbase.hregion.max.filesize 默认10G)就会触发切分,每10s检查一次region大小,hbase.server.thread.wakefrequency=10000。
弊端:
设置阈值大些,对大表友好,但对小表并不友好,可能小表不会分裂;
如果阈值小些,对小表友好,但对大表并不友好,可能会大量分裂;
IncreasingToUpperBoundRegionSplitPolicy:
默认使用的拆分策略,Region的前几次拆分的阈值不是固定的数值,是需要进行计算得到,当同一table在同一regionserver上的region数量在[0,100)之间时按照如下的计算公式算,否则按照ConstantSizeRegionSplitPolicy策略计算:
Min (R^3 "hbase.hregion.memstore.flush.size"2, "hbase.hregion.max.filesize")
-
R为同一个table中在同一个regionserver中region的个数
-
hbase.hregion.memstore.flush.size默认为128M
- hbase.hregion.max.filesize默认为10G
第一次分裂: 1*1*1*128*2=256M
第二次分裂:8*128*2 = 2G
第三次分裂: 27*128*2 = 6.75G
KeyPrefixRegionSplitPolicy:
除了根据大小来拆分,我们还可以自己定义拆分点,KeyPrefixRegionSplitPolicy是IncreasingToUpperBoundRegionSplitPolicy的子类,在前者的基础上增加了对拆分点(splitPoint,拆分点就是Region被拆分处的rowkey)的定义,保证了有相同前缀的rowkey不会被拆分到两个不同的Region里面,使用KeyPrefixRegionSplitPolicy.prefix_length配置前缀长度,如果配置的为5,则截取rowkey前五位,前五位相同的数据将被分到一个region上。
DelimitedKeyPrefixRegionSplitPolicy:
也是继承自IncreasingToUpperBoundRegionSplitPolicy,它也是根据你的rowkey前缀来进行拆分的。唯一的不同就是:KeyPrefixRegionSplitPolicy是根据rowkey的固定前几位字符来进行判断,而DelimitedKeyPrefixRegionSplitPolicy是根据分隔符来判断的,在有些系统中rowkey的前缀可能不一定都是定长的,比如你拿服务器的名字来当前缀,有的服务器叫host12有的叫host1。这些场景下严格地要求所有前缀都定长可能比较难,而且这个定长如果未来想改也不容易,DelimitedKeyPrefixRegionSplitPolicy就给了你一个定义长度字符前缀的自由,使用DelimitedKeyPrefixRegionSplitPolicy.delimiter配置。
SteppingSplitPolicy:
这种策略和IncreasingToUpperBoundRegionSplitPolicy策略很相似,但更简单,第一个Region容量的上限为256M,之后都是10G,这个策略考虑到IncreasingToUpperBoundRegionSplitPolicy会多拆分几个Region(256M -> 2G -> 6.75G -> 10G),所以进行了简化。
DisabledRegionSplitPolicy:
不做region拆分。
# 语法: split '表名','splitkey'
split 'xinniu:table1', 'id03'
查看webui :
6.2 读数据流程
理论上,读memstore和storefile就可以把数据读取出来,但这样命中磁盘几率增加,会降低hbase的读性能;
通过把经常读取的数据放到读缓存(blockcache),这样可以减少命中磁盘的几率;
-
找到要读的数据的HRegionServer。
-
根据读取的TableName和RowKey的startkey、endkey找到对应的HRegion。
- 如果读取的数据是memstore中put的数据,可以直接从写缓存找到并返回,如果读取的数据是没有在memory中,则会到blockcahce上找数据,再查不到则到磁盘(storefile)查找,并把读入的数据同时放入blockcache。
BlockCache:
1)BlockCache称为读缓存;
2)HBase会将一次文件查找的Block块缓存到Cache中,以便后续同一请求或者邻近数据查找请求,可以直接从内存中获取,避免昂贵的IO操作。
注意:如果是遍历表的数据(只遍历一遍),那就不需要读缓存。
HBase中Block:
1)Block是HBase中最小的数据存储单元,默认为64K,在建表语句中可以通过参数BlockSize指定。
2)HBase中Block分为四种类型:Data Block,Index Block,Bloom Block和Meta Block。 其中:
Data Block:用于存储实际数据,通常情况下每个Data Block可以存放多条KeyValue数据对;
Meta Block:主要存储整个HFile的元数据;
Index Block:通过存储索引数据加快数据查找;
Bloom Block:通过布隆过滤算法可以过滤掉部分一定不存在待查KeyValue的数据文件,减少不必要的IO操作;
布隆过滤器:
判断一个数据是否在集合中存在;
只能判断一个数据要么一定在集合中不存在,要么在集合中可能存在;
作用:减少不必要数据的读取,提升读的性能。
7 rowkey设计
RowKey可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,字典顺序排序,rowkey的设计至关重要,会影响region分布,如果rowkey设计不合理还会出现region写热点等一系列问题。
rowkey设计原则:
-
保证rowkey的唯一性:性质与主键唯一一致。
-
能满足需求的情况下,长度越短越好:推荐16字节。
- 高位散列:高位散列的目的是使数据均匀分布到不同的region上,散列方式一般采用"反转"、"加盐"、"MD5"的方式对高位进行处理。(防止写热点问题)
需求:hbase存储的是用户的交易信息, 我想查某个用户在某个时间段内的交易记录,如何设计rowkey
用户id(md5), 用户名称, 交易时间, 交易金额, 交易说明
用户id(md5), 交易时间
rowkey设计: 用户id(md5) + _ + 交易时间
create 'xinniu:flow', 'cf'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210110000', 'cf:name', 'user1'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210110000', 'cf:amt', '1000'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210110000', 'cf:time', '2021-12-10 11:00:00'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210120000', 'cf:name', 'user1'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210120000', 'cf:amt', '2000'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210120000', 'cf:time', '2021-12-10 12:00:00'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210130000', 'cf:name', 'user1'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210130000', 'cf:amt', '3000'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210130000', 'cf:time', '2021-12-10 13:00:00'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210140000', 'cf:name', 'user1'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210140000', 'cf:amt', '4000'
put 'xinniu:flow', '02f5adff232b37422fc846cc5c1d8328_20211210140000', 'cf:time', '2021-12-10 14:00:00'
# 查询 某个人在 20211210 日 11 点 到 202112110 日 12:30 间的交易记录
scan 'xinniu:flow', {STARTROW => '02f5adff232b37422fc846cc5c1d8328_2021121011' , STOPROW=> '02f5adff232b37422fc846cc5c1d8328_202112101230'}