1 本章目录
- 负载均衡
- 快照
- 拆分
- 数据存储
- 合并
- 刷写
- 读数据流程
- 写数据流程
- 热点问题
- row设计
- 批量导入
- Mr整合Hbase
- 协处理器
1 负载均衡
由Master的LoadBalancer线程周期性的在各个RegionServer间移动region维护负载均衡。
1 经常被并发查询的数据不要存储在同一个RegionServer中 , 避免热点读取问题 .
2 当一个机器上经过大量的插入或者删除数据以后 ,region合并或者分裂 ,那么机器上的region的数量会相差很大 .
3 当新增了节点以后 , 应该去分配一些其他机器上的region数据
4 当某个RegionServer宕机以后 , 这台机器上数据的分配
region的执行
Hbase中负载均衡的开关balanceSwitch默认是开启的 .
hbase.balancer.period是设定负载均衡的参数,默认为每5分钟(300000毫秒)运行一次,参数配置通过hbase-site.xml文件。具体的设置格式如下:
<property>
<name>hbase.balancer.period</name>
<value>300000</value>
</property>
1.1 自动负载均衡流程
)两个有效参数:MIN = floor(average)(表示下限)和MAX=ceil(average)(表示上限)。
2)循环过载机器,将Region卸载到MAX数量,在小于等于MAX时停止排序Region(按时间新旧)。
3)遍历最轻负载机器,分配Region直到Server达到MIN值。在大于等于MIN时停止。这些Region都是之前卸载的。可能没有足够地卸载Region让轻负载的机器达到MIN值,如果这样,在Region数等于neededRegions(轻负载机器的数量)时停止。可能我们分配了卸载的Region到轻负载机器,但是仍然有Region没有分配出去,这种情况下,本步骤完成,在下面步骤中再做处理。
4)如果neededRegions是非零的值,遍历负载最重的机器,从每台机器上卸载一个Region,使得它们的值从MAX到MIN。
5)现在有很多Region等待分配,遍历最轻的负载机器(多台),分配Region到MIN。
6)如果仍然有Region没有分配,遍历最轻的负载机器(多台),这次分配Region到MAX。
7)所有Server的Region数量时MAX或者MIN,另外,所有大于等于MAX的Server保证在均衡完成后都是MAX个Region。从而保证Region移动的最小数量。
其中,轻负载指的是Region数量小于等于AVG,过载指大于等于AVG,所有的RegionServer都按照负载从大到小排序,存放在TreeMap中(保证先遍历过载Server)。
1.2 强制执行负载均衡
) kill 掉不是Hmaster节点的 regionserver(slave2)
2) hbase 16010 查看regionserver在slave2节点
3) 在master节点启动 regionserver(slave2)
master $>hbase-daemons.sh start regionserver
4) hbase 16010 查看regionserver负载不均衡 (多刷新 0 6)
balance负载均衡指令,需使用开关模式,原因负载均衡比较耗费资源:
hbase(main):005:0> balance_switch true //开启负载均衡
hbase(main):005:0> balancer //执行负载均衡
hbase(main):005:0> balance_switch false //关闭负载均衡
hbase(main):005:0> balancer "force" //强制负载均衡
1.3 人为的移动
当发生经常性的查询热点问题时, 您的请求总是请求到一个服务节点上 ,我们可以把这个机器中的数据理解成热数据 ,可以任务的拆分以后手动的移动到不同的机器上 实现查询热点的负载均衡
使用命令
1 切分
Examples:
split 'tableName'
split 'namespace:tableName'
split 'regionName' # format: 'tableName,startKey,id'
split 'tableName', 'splitKey'
split 'regionName', 'splitKey'
页面查看region情况 ,或者使用list_regions "tb_name"
2 移动 将某个region移动到某个机器上
move "a84afc7eb10797188c0af1534e143eb9" , "linux03,16020,1591844369905"
2 快照
2.1 快照原理(基于HDFS的快照)
Hdfs的快照(snapshot)是在某一时间点对指定文件系统拷贝,快照采用只读模式,可以对重要数据进行恢复、防止用户错误性的操作。
快照分两种:一种是建立文件系统的索引,每次更新文件不会真正的改变文件,而是新开辟一个空间用来保存更改的文件,一种是拷贝所有的文件系统。Hdfs属于前者。
Hdfs的快照的特征如下:
- 快照的创建是瞬间的,代价为O(1),取决于子节点扫描文件目录的时间。
- 当且仅当做快照的文件目录下有文件更新时才会占用小部分内存,占用内存的大小为O(M),其中M为更改文件或者目录的数量;
- 新建快照的时候,Datanode中的block不会被复制,快照中只是记录了文件块的列表和大小信息。
- 快照不会影响正常的hdfs的操作。对做快照之后的数据进行的更改将会按照时间顺序逆序的记录下来,用户访问的还是当前最新的数据,快照里的内容为快照创建的时间点时文件的内容减去当前文件的内容。
2.2 基本演示
1 snapshot "tb_spl2" , "spl_info" 拍摄快照 参数一表 参数二 快照名
2 hbase(main):019:0> list_snapshots 列举出所有的快照
SNAPSHOT TABLE + CREATION TIME
spl_info tb_spl2 (2020-06-13 11:22:52 +0800)
3 restore_snapshot "spl_info" 恢复快照
3 拆分
3.1 为什么拆分??
HBase是以表的形式存储数据的。一张表被划分为多个regions,regions分布在多个Region Server上,单个region只能分布在一个Region Server节点上,不能跨Region Server存放。
Region的两个重要属性:StartKey和EndKey,分别表示这个region所维护的rowkey范围。当做读/写请求时,寻址到该数据的rowkey落在某个start-end key范围内,那么就会定位到该范围内的region所在的Region Server上进行数据读/写。
随着数据的插入region中管理的数据越来越多 ,出现热点问题的概率大大增加 , 并且一个regionserver的负载过重,将region拆分 均衡到不同的节点上!
3.2 怎么拆分 ??
3.2.1 手动拆分
Examples:
split 'tableName'
split 'namespace:tableName'
split 'regionName' # format: 'tableName,startKey,id'
split 'tableName', 'splitKey'
split 'regionName', 'splitKey'
3.2.2 自动拆分
3.2.2.1 默认按照大小
当region的大小达到一定大小的时候会被拆分
IncreasingToUpperBoundRegionSplitPolicy
split策略实现类
<property>
<name>hbase.regionserver.region.split.policy</name>
<value>
org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicy
</value></property>
当hbase表在regionserver上的region,如果region的大小到达一个阈值,这个region将会分为两个。
计算公式为:
Min{1^3*2*128M 256M
2^3*2*128M 2G
3^3*2*128M 6.75G
10G 10G
(表在一个regionserver上region的数量的立方) *2(the region memstore flush size),
hbase.hregion.max.filesize(默认是10GB)
}
如果默认值情况下,一个表在一个regionserver上split的阈值是:
256MB(第一次split),2GB(第二次),6.75GB(第三次),10GB(第四次),10GB... 10GB<property>
<name>hbase.hregion.max.filesize</name>
<value>10737418240</value>
</property>
<property>
<name>hbase.regionserver.regionSplitLimit</name>
<value>1000</value>
</property>
3.2.2.2 keyPrefixRegionSplitPolicy(自定义) key的前缀
这种拆分是在原来的拆分基础上 ,增加了拆分点(splitPoint,拆分点就是Region被拆分时候的rowkey)的定义,保证有相同前缀的rowkey不会被拆分到不同的Region上
参数是 keyPrefixRegionSplitPolicy.prefix_length rowkey:前缀长度
问题是 : 可能相同主题的数据会被分到不同的regionserver中 ,查询数据来源于不同的regionserver
3.2.2.3 DelimitedKeyPrefixRegionSplitPolicy key的分隔符
和上一种查分策略一致 , 上一种是按照key的固定长度拆分的 , 这种按照的是分割符
DelimitedKeyPrefixRegionSplitPolicy.delimiter参分割符
指定一个分隔符 能保证相同的主题数据在一个regionserver中
3.2.3 建表时指定预分region
shell客户端
hbase> create 'ns1:t1', 'f1', SPLITS => ['10', '20', '30', '40']
hbase> create 't1', 'f1', SPLITS => ['10', '20', '30', '40']
java客户端
byte[][] keys = new byte[][]{"e".getBytes(), "m".getBytes(), "t".getBytes()};
// 建表 指定预分region的key(数组)
admin.createTable(tableDescriptor, keys);
4 Hbase如何实现CRUD
hbase是分布式列式存储数据库 , 其具有的基本功能就是对数据的增删改查 , 那么Hbase的数据是存储在HDFS上的,我们知道在HDFS中的数据是不允许随机修改的!这与HDFS的功能相违背!
hbase数据底层存储下HDFS系统中,一Hfile的形式存储的!在HDFS中Hfile的具体位置
查看Hfile文件中数据内容 hbase hfile -p -f 解析Hfile中的内容
其实在hbase中所有的操作都是写操作 ,在所有的操作中有对应的put delete 标记 !查询数据的时候不会显示delete标记的数据 !当Hfile合并的时候才会将删除的数据真正的删除掉!
5 合并
5.1 hfile合并
数据加载到memstore,数据越来越多直到memstore占满,再写入硬盘storefile中,每次写入形成一个单独Hfile,当Hfile达到一定的数量后,就会开始把小Hfile合并成大Hfile,因为Hadoop不擅长处理小文件,文件越大性能越好。
触发compaction的方式有三种:Memstore刷盘、后台线程周期性检查、手动触发。
1.Memstore Flush:
应该说compaction操作的源头就来自flush操作,memstore flush会产生HFile文件,文件越来越多就需要compact。因此在每次执行完Flush操作之后,都会对当前Store中的文件数进行判断,一旦文件数大于配置,就会触发compaction。需要说明的是,compaction都是以Store为单位进行的,而在Flush触发条件下,整个Region的所有Store都会执行compact,所以会在短时间内执行多次compaction。
2.后台线程周期性检查:
后台线程定期触发检查是否需要执行compaction,检查周期可配置。线程先检查文件数是否大于配置,一旦大于就会触发compaction。如果不满足,它会接着检查是否满足major compaction条件,简单来说,如果当前store中hfile的最早更新时间早于某个值mcTime,就会触发major compaction(默认7天触发一次,可配置手动触发),HBase预想通过这种机制定期删除过期数据。
3.手动触发:
一般来讲,手动触发compaction通常是为了执行major compaction,一般有这些情况需要手动触发合并
- 是因为很多业务担心自动major compaction影响读写性能,因此会选择低峰期手动触发;
- 也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发major compaction;
- 是HBase管理员发现硬盘容量不够的情况下手动触发major compaction删除大量过期数据;
在合并的过程中会做什么操作?
在合并的过程中会抛弃删除标识的行和版本过旧的行
(1)可以预先定义版本的个数,超过这个值就抛弃
(2)还可以预先定义版本的时间长短,超过这个时间就抛弃,合并完后形成更大的storefile,当达到数量再次合并,直到storefile容量超过一定阀值后会把当前的Region进行分裂为2个并由Hmaster(hbase数据库主控节点)分配到不同的HRegionServer服务器处理实现负载均衡。
如果在合并过程中恰好有涉及到有关storefile的查询发生的话,我们先是把小storefile加载到内存中进行合并此时如有用户访问可以在内存中检索相关数据返回给用户,我们可以想象在内存中做一个独立镜像备份专门提供被查询需求,另一个主体在另一块内存空间里进行合并,当合并完成后释放备份的内存空间,返回到原来的状态。
shell客户端操作命令 合并hfile
Compact all regions in passed table or pass a region row
to compact an individual region. You can also compact a single column
family within a region.
You can also set compact type, "NORMAL" or "MOB", and default is "NORMAL"
Examples:
Compact all regions in a table:
hbase> compact 'ns1:t1'
hbase> compact 't1'
Compact an entire region:
hbase> compact 'r1'
Compact only a column family within a region:
hbase> compact 'r1', 'c1'
Compact a column family within a table:
hbase> compact 't1', 'c1'
Compact table with type "MOB"
hbase> compact 't1', nil, 'MOB'
Compact a column family using "MOB" type within a table
hbase> compact 't1', 'c1', 'MOB'
5.2 region合并
5.2.1 那为什么需要合并Region呢
这个需要从Region的Split来说。当一个Region被不断的写数据,达到Region的Split的阀值时(由属性hbase.hregion.max.filesize来决定,默认是10GB),该Region就会被Split成2个新的Region。随着业务数据量的不断增加,Region不断的执行Split,那么Region的个数也会越来越多。
一个业务表的Region越多,在进行读写操作时,或是对该表执行Compaction操作时,此时集群的压力是很大的。这里笔者做过一个线上统计,在一个业务表的Region个数达到9000+时,每次对该表进行Compaction操作时,集群的负载便会加重。而间接的也会影响应用程序的读写,一个表的Region过大,势必整个集群的Region个数也会增加,负载均衡后,每个RegionServer承担的Region个数也会增加。
因此,这种情况是很有必要的进行Region合并的。比如,当前Region进行Split的阀值设置为30GB,那么我们可以对小于等于10GB的Region进行一次合并,减少每个业务表的Region,从而降低整个集群的Region,减缓每个RegionServer上的Region压力。
那么我们如何进行Region合并呢?HBase有提供一个合并Region的命令,具体操作如下:
# 合并相邻的两个Region
hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME'
# 强制合并两个Region
hbase> merge_region 'ENCODED_REGIONNAME', 'ENCODED_REGIONNAME', true
5.2.2 Region合并分为两种,minor合并和major合并
3.1 Minor合并:
是把最后生成的几个小的StoreFile文件 合并一个更大的StoreFile, 默认同时合并的文件数为10, 主要控制参数如下;
hbase.hstore.compaction.min最小合并文件 数量大小, 默认为3
hbase.hstore.compaction.max 最大合并文件 数量大小, 默认为10
hbase.hstore.compaction.max.size 最大合并文件 的大小 , 默认值 为Long.MAX_VALUE
hbase.hstore.compaction.min.size 最小合并文件 的大小, 默认值是memStore刷写的大小, 它是一个阈值, 它包含所有小于限制的文件 ,直到达到每次压缩允许的总文件数量
3.2 Major合并:
是把整个Store中所有StoreFile文件 合并成一个单独的StoreFile文件,后台定时任务定期检查, 这个检查周期是由下面这两人个参数决定
hbase.server.thread.wakefrequency * hbase.server.thread.wakefrequency.multiplier(默认值为1000)
6 刷写
一个Store有一个MemStore,保存数据修改。当flush后,当前MemStore就被清理了。MemStorez中的数据按 RowKey 字典升序排序。
HBase需要将写入的数据顺序写入HDFS,但因写入的数据流是未排序的及HDFS文件不可修改特性,所以引入了MemStore,在flush的时候按 RowKey 字典升序排序进行排序再写入HDFS。
充当内存缓存,在更多是访问最近写入数据的场景中十分有效
可在写入磁盘前进行优化,比如有多个对同一个cell进行的更新操作,那就在flush时只取最后一次进行刷盘,减少磁盘IO。
6.1 刷写过程
6.1.1 prepare
遍历当前Region中的所有Memstore,将Memstore中当前数据集kvset做一个快照snapshot,对后来的读请求提供服务,读不到再去BlockCache/HFile中查找。
然后再新建一个新的kvset Memstore(SkipList),服务于后来的写入。
prepare阶段需要加一把写锁对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
6.1.2 flush
遍历所有Memstore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时,但不会影响读写。
当Flush发生时,当前MemStore实例会被移动到一个snapshot中,然后被清理掉。在此期间,新来的写操作会被新的MemStore和刚才提到的备份snapshot接收,直到flush成功后,snapshot才会被废弃。
6.1.3 commit
遍历Region所有的Memstore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,随后把storefile添加到HStore的storefiles列表中。最后清空prepare阶段生成的snapshot。
整个flush过程还可能涉及到compact和split
6.2 刷写时机
Region级别-跨列族
Region内的其中一个MemStore大小达到阈值(hbase.hregion.memstore.flush.size),该Region所有MemStore一起发生Flush,输入磁盘。默认大小是128M !
RegionServer级别
当一个RS内的全部MemStore使用内存总量所占比例达到了阈值(hbase.regionserver.global.memstore.upperLimit),那么会一起按Region的MemStore用量降序排列flush,直到降低到阈值(hbase.regionserver.global.memstore.lowerLimit)以下。
另有一个新的参数hbase.regionserver.global.memstore.size,设定了一个RS内全部Memstore的总大小阈值,默认大小为Heap的40%,达到阈值以后就会阻塞更新请求,并开始RS级别的MemStore flush,和上述行为相同。
HLog-WAL文件
当region server的WAL的log数量达到hbase.regionserver.max.logs,该server上多个region的MemStore会被刷写到磁盘(按照时间顺序),以降低WAL的大小。否则会导致故障恢复时间过长。
手动触发
通过HBase shell或Java Api手动触发MemStore flush
手动刷写命令
hbase> flush 'TABLENAME'
hbase> flush 'REGIONNAME'
hbase> flush 'ENCODED_REGIONNAME'
hbase> flush 'REGION_SERVER_NAME'
7 读数据流程
7.1 基本流程
客户端--ZooKeeper--meta--regionserver--region--memstore--blockfile--storefile
1、首先从zookerper找到meta表的region的位置,然后读取meta表中的数据。而meta中又存储了用户表的region信息
2、根据namespace、表名和rowkey根据meta表中的数据找到写入数据对于的region信息
3、然后找到对于的regionserver
4、查找对应的region
5、先从Memstore找数据,如果没有,再到StoreFile上读
1 客户端请求zookeeper,zookeeper返回meta表所在的位置信息
2 客户端获取meta表的位置信息以后,请求META表所在的RegionServer机器获取存储数据的元数据表META
3 客户端获取META表,并缓存到客端(方便下次使用),这时客户端就知道自己要的数据在哪个Regionserver的哪个Region上
4 客户端根据元数据信息,请求对应的Regionserver上的region获取数据
如果有则将数据写入到缓存块中 ,返回客户端
4.2 如果内存中没有要读取的数据,则去缓存块中去查看是否有自己的要的数据 ,如果有就返回客户端
4.3 如果缓存中依然没有我们要找的数据 ,那么这个时候就去HDFS中的Hfile中读取数据
4.4 从Hfile中获取数据以后 ,也会将查询的结果写到缓存块中 , 再返回给客户端
HFile文件所在的位置具体如下:
7.2 布隆过滤器
使用很少的空间来实现从大量文件中定位数据一定不再某些文件中 ,极可能在某些文件中
每次flush都会在表对应的文件夹中生成Hfile文件 , 可能一个表中的Hfile文件会很多 ,为了快速的确定数据在哪个Hfile中, HBASE引入了布隆过滤器!
如上图所示,布隆过滤器不能明确指出哪一个文件一定包含所查找的行键,布隆过滤器的结果有误差存在。当布隆过滤器判断文件中不包含对应的行时,这个答案是绝对正确的;但是,当布隆过滤器判断得到文件中包含对应行时,这个答案却有可能是错的。也就是说,HBase还是有可能加载了不必要的块。尽管如此,布隆过滤器还是可以帮助我们跳过一些明显不需要扫描的文件。另外,错误率可以通过调整布隆过滤器所占空间的大小来调整,通常设置错误率为1%。
需要注意的是,使用布隆过滤器,并不一定能立即提升个别的get操作性能,因为同一时间可能有多个客户端向HBase发送请求,当负载过大时,HBase的性能受限于读磁盘的效率。但是,使用了布隆过滤器之后,可以减少不必要的块加载,从而可以提高整个集群的吞吐率。并且,因为HBase加载的块数量少了,缓存波动也降低了,进而提高了读缓存的命中率。
布隆过滤器其实就是将目标数据经过布隆大哥自己写的hash算法,
这个算法是将
将一个8位比特位1的数据 也就是8个1(这个是比特位的1)(数量根据需要的成功率会有变化),
然后将每个比特位通过他的算法对应到一个长度位64k的二进制数组上(长度也随成功率自行调整) ,
例如
https://google.com/abc/d 通过布隆过滤器时
可以计算出8个位置
1 2 3 9 15 20 31 32(仅举例)
那么我们就将8个比特位分别插入到这些位置
例如下图
如果 hello ---> hashcode的值是 12 12位置是1 说明hello极大的可能存在
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
如果下次存在经过这个过滤器的时候, 8个比特位完全重复的, 那么大概率是已经通过一次这个过滤器了, 大概率不代表完全确定, 因为可能存在偶然结果, 但是我们可以通过适当增大二进制数组的长度, 来确保尽量可靠
Hbase之所以会用到这个布隆过滤器是因为, region server 的内存在很多情况下会自动将内存中的热数据写入到文件中, 时间一长会造成一个region目录/一个列族目录下存在多个文件, 而且这些个文件可能存在重复数据, 让我们查询数据的时候, 显然盲目遍历的去查找是不科学的, Hbase就选择在写入每个文件时, 同时通过了布隆过滤器, 也就是每个文件都有一个布隆过滤器的二进制数组, 所以当Hbase查询数据时, 它会去选择读取结果全部命中布隆过滤器二进制数组的对应的文件, 这样虽然不能保证100%, 但是最坏的结果也就是多读了几个文件, 相比遍历, 效率显著提高!
判断数据一定不在这个文件中 ,极有有可能在这个文件中(出现hash碰撞可能出现误判) ,
8 写数据流程
1 客户端请求ZK集群获取存储元数据信息的META表所在的机器
2 请求META表所在的机器,下载META表到客户端并缓存在本地
3 客户端解析META表, 获取要读取的数据所在的RegionServer机器
4 请求RegionServe确定读取的数据在哪个region中
5 先从内存对象中获取数据 , 如果没有再查看缓存区域 ,缓存中没有再去Hfile中读取数据 , 为了快速的确定数据在哪个Hfile中, HBASE引入了布隆过滤器
6 如果数据是从Hfile中读取, 会将数据缓存在缓存区中再返回给客户端 , 以便下次查询的时候提高查询效率
9 热点问题
在创建表的时候建议 使用预分region表 (指定了切割点 ,对数据有分组规划)
1. 观察数据特点 注意重点字段(业务)
2 了解具体的表对应的业务,重点字段
3 根据业务 判断数据的读多(查询) 写多(插入)
4 读数据 写数据 (特点) 维度
5 避免热点问题 [插入热点 , 查询热点]
- 138...大量的数据插入到同一个region 一个regionserver在服务 压力过大
- 并发的查询138...数据 一个regionserver在服务 接收所有的并发查询
当表被创建时,HBase默认只会为该表分配一个region,那么,初始状态时所有的请求都会集中在一个Region Server上,当大量数据写入时,该节点将成为热点,甚至产生full gc。
当然,region热点不仅体现在创建表阶段。对于一张拥有很多region的大表来说,其在Region Sever上的分布往往不会十分均匀,region较多的Region Server在有大量数据写入时,它的负载也会大于其他region数少的Region Server,这也是热点。
因此,针对上述两种情况,该如何避免热点发生就是一个常见的话题。
【解决办法】
如果知道了HBase数据表的key分布情况,可以根据需要,规划要拆分成多少个region,每个region的起始key是多少,那么就可以在建表时对HBase进行region的预分区,这样就能避免热点,提高数据写入效率。
合理设计rowkey能让各个region的并发请求平均分配,使IO效率达到最高。比如,key的前几位字符串都是从0001~0010的数字,这样就可以分成10个region,使用SPLISTS,如下:
create't1','f1',{SPLITS=>['0001','0002','0003','0004','0005','0006','0007','0008','0009']}
如果分区分区信息比较多,那么可以通过指定分区文件来做:
create't1','f1',{SPLITS_FILE=>'region_split_info.txt'}
region_split_info.txt文件内容如下,每行都是key取值范围的分割点。文件第一行为第一个region的endkey,每行依次类推,最后一行的0009为倒数第二个region的endkey,同时也是最后一个region的startkey。
cat region_split_info.txt
0001|
0002|
0003|
0004|
0005|
0006|
0007|
0008|
0009|
在HBase Master Web UI页面上查看,可以看到第一个region是没有startkey,最后一个region是没有endkey的。
10 row设计
用来表示唯一一行记录的主键,HBase的数据是按照RowKey的字典顺序进行全局排序的,所有的查询都只能依赖于这一个排序维度。
ROWKEY设计原则
一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于哪个一个预分区的区间内,设计 rowkey 的主要目的 ,就是让数据均匀的分布于所有的 region 中,在一定程度上防止数据倾斜。接下来我们就谈一谈 rowkey 常用的设计方案。
1. rowkey 长度原则
Rowkey 是一个二进制码流,Rowkey 的长度被很多开发者建议说设计在 10~100 个字节,不过建议是越短越好,不要超过 16 个字节,存为byte[]字节数组,一般设计成定长的。
原因如下:
1、数据的持久化文件 HFile 中是按照 KeyValue 存储的,如果 Rowkey 过长比如 100 个字 节,1000 万列数据光 Rowkey 就要占用 100*1000 万=10 亿个字节,将近 1G 数据,这会极大 影响 HFile 的存储效率;
2、MemStore 将缓存部分数据到内存,如果 Rowkey 字段过长内存的有效利用率会降低, 系统将无法缓存更多的数据,这会降低检索效率。因此 Rowkey 的字节长度越短越好。
3、目前操作系统是都是 64 位系统,内存 8 字节对齐。控制在 16 个字节,8 字节的整数 倍利用操作系统的最佳特性。
2. rowkey 散列原则
如果 Rowkey 是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将 Rowkey 的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个 Regionserver 实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有 新数据都在一个 RegionServer 上堆积的热点现象,这样在做数据检索的时候负载将会集中 在个别 RegionServer,降低查询效率。
row key是按照字典序存储,因此,设计row key时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为row key的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE - timestamp作为row key,这样能保证新写入的数据在读取时可以被快速命中。
3. rowkey 唯一原则
必须在设计上保证其唯一性。rowkey 是按照字典顺序排序存储的,因此,设计 rowkey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问 的数据放到一块。
4 避免热点问题
调研数据特点 , 调研业务特点, 是查询多还是写入场景多 ,是否有并发操作 ,并发的频次和周期,并发量 ,根据数据特点和查询的维度设计表的行建! 详细查看热点文解决方案
11 批量导入
bulkloader : 一个用于批量快速导入数据到hbase的工具/方法
用于已经存在一批巨量静态数据的情况!如果不用bulkloader工具,则只能用rpc请求,一条一条地通过rpc提交给regionserver去插入,效率极其低下!
用 Bulk Load 方式由于利用了 HBase 的数据信息是按照特定格式存储在 HDFS 里的这一特性,直接在 HDFS 中生成持久化的 HFile 数据格式文件,然后完成巨量数据快速入库的操作,配合 MapReduce 完成这样的操作,不占用 Regionserver 资源,不会产生巨量的写入 I/O,所以需要较少的 CPU 和网络资源。
Bulk Load 的实现原理是通过一个 MapReduce Job 来生成一个 HBase 的HFile 格式文件,用来形成一个特殊的 HBase 数据表,然后直接将数据文件加载到运行的集群中。与使用HBase API相比,使用Bulkload导入数据占用更少的CPU和网络资源。
原理
Bulkload过程主要包括三部分:
1、从数据源(通常是文本文件或其他的数据库)提取原始数据并上传到HDFS。抽取数据到HDFS和Hbase并没有关系。
2、利用MapReduce作业处理事先准备的数据,使用HFileOutputFormat2来生成HBase数据文件。
3、告诉RegionServers数据的位置并导入数据。使用LoadIncrementalHFiles(更为人所熟知是completebulkload工具),将文件在HDFS上的位置传递给它,它就会利用RegionServer将数据导入到相应的区域。
案例1 shell命令导入csv文件
1 数据文件
uid001,lss,32,F
uid002,zss,32,F
uid003,wss,32,F
uid004,dss,32,F
2 在hbase中创建一个表
create "tb_teacher" , "cf"
3 将user.csv文件上传到linux系统的某个目录中
4 生成Hfile文件
hbase org.apache.hadoop.hbase.mapreduce.ImportTsv -Dimporttsv.separator=, -Dimporttsv.columns='HBASE_ROW_KEY,cf:name,cf:age,cf:gender' -Dimporttsv.bulk.output=/csv/out tb_teacher /csv/user.csv
5 将生成的file文件导入到hbase表中
hbase org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles /csv/out/ tb_teacher
hbase(main):001:0> scan "tb_teacher"
ROW COLUMN+CELL
uid001 column=cf:age, timestamp=1592064585500, value=32
uid001 column=cf:gender, timestamp=1592064585500, value=F
uid001 column=cf:name, timestamp=1592064585500, value=lss
uid002 column=cf:age, timestamp=1592064585500, value=32
uid002 column=cf:gender, timestamp=1592064585500, value=F
uid002 column=cf:name, timestamp=1592064585500, value=zss
uid003 column=cf:age, timestamp=1592064585500, value=32
uid003 column=cf:gender, timestamp=1592064585500, value=F
uid003 column=cf:name, timestamp=1592064585500, value=wss
uid004 column=cf:age, timestamp=1592064585500, value=32
uid004 column=cf:gender, timestamp=1592064585500, value=F
uid004 column=cf:name, timestamp=1592064585500, value=dss
案例2 自定义MR生成HFile
成Hfile基本流程:
1 设置Mapper的输出KV类型:
K: ImmutableBytesWritable(代表行键)
V: KeyValue (代表cell)
2 开发Mapper
读取你的原始数据,按你的需求做处理
输出rowkey作为K,输出一些KeyValue(Put)作为V
3 配置job参数
Zookeeper的连接地址
配置输出的OutputFormat为HFileOutputFormat2,并为其设置参数
4 提交job
Mapper类
ublic static class HFileMapper extends Mapper<LongWritable, Text, ImmutableBytesWritable, KeyValue> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] items = line.split(",", -1);
ImmutableBytesWritable rowkey = new ImmutableBytesWritable(items[0].getBytes());
KeyValue kv = new KeyValue(Bytes.toBytes(items[0]), Bytes.toBytes(items[1]), Bytes.toBytes(items[2]),
System.currentTimeMillis(), Bytes.toBytes(items[3]));
if (null != kv) {
context.write(rowkey, kv);
}
}
}
Job类
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "hdp-01:2181,hdp-02:2181,hdp-03:2181");
Job job = Job.getInstance(conf, "HFile bulk load test");
job.setJarByClass(DemoHFile.class);
job.setMapperClass(HFileMapper.class);
job.setReducerClass(CellSortReducer.class);
job.setMapOutputKeyClass(ImmutableBytesWritable.class);
job.setOutputValueClass(KeyValue.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
TableDescriptor td = TableDescriptorBuilder.newBuilder(TableName.valueOf("wc")).build();
Connection conn = ConnectionFactory.createConnection(conf);
RegionLocator regionLocator = conn.getRegionLocator(TableName.valueOf("wc"));
HFileOutputFormat2.configureIncrementalLoad(job, td, regionLocator);
boolean res = job.waitForCompletion(true);
if (res) {
Table table = conn.getTable(TableName.valueOf("wc"));
Admin admin = conn.getAdmin();
BulkLoadHFilesTool bulkLoadHFilesTool = new BulkLoadHFilesTool(conf);
bulkLoadHFilesTool.doBulkLoad(new Path(args[1]), admin, table, regionLocator);
}
}
12 MR整合HBase
使用MR程序运算处理HBASE中的数据
MapReduce是一个分布式运算框架,可以对存储在HDFS的海量数据进行分布式并行运算 , 在处理海量数据时表现出优异的性能.
mapreduce读、写各类数据系统的由相应的InputFormat和OutputFormat来支持;
Hbase为mapreduce开发了TableInputFormat和TableOutputFormat,因此,在mapreduce程序中访问hbase数据是很轻松的。
21.1 读取hbase一个表中的数据插入到另一张 表中
/**
* 使用MR程序
* 读取hbase一个表中的数据插入到另一张 表中
* @author ThinkPad
*/
public class MR {
static class MRMapper extends TableMapper<Text, Text> {
/**
* 参数一 rowkey 参数二 结果 参数三 输出的key 参数四 输出的value
*/
@Override
protected void map(ImmutableBytesWritable key, Result value,
Mapper<ImmutableBytesWritable, Result, Text, Text>.Context context)
throws IOException, InterruptedException {
// 获取字符串的 rowkey
String k = new Text(new String(key.copyBytes())).toString();
//获取指定的属性的值
String name = Bytes.toString(value.getValue("f".getBytes(), "name".getBytes()));
String gender = Bytes.toString(value.getValue("f".getBytes(), "gender".getBytes()));
System.out.println(k + " " + name);
// 以行建为key 以多个属性组装的结果为value传递 到reduce中
context.write(new Text(k), new Text(name + ":" + gender));
}
}
/**
* reduce端
* 接收传入的key和value
* reduce方法在一个行建执行一次
* @author ThinkPad
*/
static class MRReducer extends TableReducer<Text, Text, ImmutableBytesWritable> {
@Override
protected void reduce(Text key, Iterable<Text> iters,
Reducer<Text, Text, ImmutableBytesWritable, Mutation>.Context context)
throws IOException, InterruptedException {
//创建put对象
Put put = new Put(key.getBytes());
// 获取接收的map的value值
Text next = iters.iterator().next();
// 将value转换成字符串
String v = next.toString();
//处理字符串获取 各个属性的值
String[] split = v.split(":");
String name = split[0];
String gender = split[1];
// 将各个属性的值添加到对应的列中
put.addColumn("f".getBytes(), "name".getBytes(), Bytes.toBytes(name));
put.addColumn("f".getBytes(), "gender".getBytes(), Bytes.toBytes(gender));
// 将put对象写出去
context.write(null, put);
}
}
public static void main(String[] args) throws Exception {
// 获取整合的初始化对象
Configuration conf = HBaseConfiguration.create();
// 连接ZK的位置
conf.set("hbase.zookeeper.quorum", "linux01,linux02,linux03");
// 获取job对象
Job job = Job.getInstance(conf);
// 穿件扫描对象用来扫描源hbase中的所有的数据
Scan scan = new Scan();
// 接收的扫描的数据的行数
scan.setCaching(200);
scan.setCacheBlocks(false);
job.setJarByClass(MR.class);
// TableMapReduceUtil.initTableMapJob("user", "f", MRMapper.class, Text.class,
// Text.class, job);
// 初始化 源表
TableMapReduceUtil.initTableMapperJob("user", scan, MRMapper.class, Text.class, Text.class, job);
// 插入数据的表要存在
TableMapReduceUtil.initTableReducerJob("user2", MRReducer.class, job);
boolean b = job.waitForCompletion(true);
if (b)
System.exit(1);
}
}
12.2 读取普通文件中的数据存储到hbase表中
/**
*使用MR程序
* 读取普通文件中的数据
* 将数据存储到hbase表中
* @author ThinkPad
*/
public class Movie {
// id MB
static class MovieMapper extends Mapper<LongWritable, Text, Text, MovieBean> {
Gson gs = new Gson();
Text k = new Text();
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, MovieBean>.Context context)
throws IOException, InterruptedException {
try {
String line = value.toString();
MovieBean mb = gs.fromJson(line, MovieBean.class);
//这个是rowkey
k.set(mb.getMovie() + mb.getTimeStamp());
context.write(k, mb);// 1 10mb
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
static class MovieReducer extends TableReducer<Text, MovieBean, ImmutableBytesWritable> {
@Override
protected void reduce(Text key, Iterable<MovieBean> iters,
Reducer<Text, MovieBean, ImmutableBytesWritable, Mutation>.Context context)
throws IOException, InterruptedException {
String rk = key.toString();
// List<Put> list = new ArrayList<Put>() ;
// 遍历传递过来的电影数据
for (MovieBean movieBean : iters) {
// 创建一个put对象
Put put = new Put(key.getBytes());
// 获取电影数据中的属性
String movie = movieBean.getMovie();
double rate = movieBean.getRate();
String timeStamp = movieBean.getTimeStamp();
String uid = movieBean.getUid();
// 将数据添加到对应的属性中
put.addColumn("f".getBytes(), "movie".getBytes(), Bytes.toBytes(movie));
put.addColumn("f".getBytes(), "rate".getBytes(), Bytes.toBytes(rate));
put.addColumn("f".getBytes(), "timeStamp".getBytes(), Bytes.toBytes(timeStamp));
put.addColumn("f".getBytes(), "uid".getBytes(), Bytes.toBytes(uid));
// 将数据写出去
context.write(null, put);
}
}
}
public static void main(String[] args) throws Exception {
// 获取整合的初始化对象
Configuration conf = HBaseConfiguration.create();
// 连接ZK的位置
conf.set("hbase.zookeeper.quorum", "linux01,linux02,linux03");
// 获取job对象
Job job = Job.getInstance(conf);
// 设置mapper类
job.setMapperClass(MovieMapper.class);
// 设置map输出的key 和 value 的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(MovieBean.class);
// 穿件扫描对象用来扫描源hbase中的所有的数据
Scan scan = new Scan();
// 接收的扫描的数据的行数
scan.setCaching(200);
scan.setCacheBlocks(false);
job.setJarByClass(Movie.class);
// 输入数据的路径
FileInputFormat.setInputPaths(job, new Path("D:\\data\\movie\\input"));
// 插入数据的表要存在
TableMapReduceUtil.initTableReducerJob("movie", MovieReducer.class, job);
boolean b = job.waitForCompletion(true);
if (b)
System.exit(1);
}
}
13协处理器
13.1 简介
可理解为服务端的拦截器,可根据需求确定拦截点,再重写这些拦截点对应的方法
协处理器允许在region server上运行自己的代码,更准确地说是允许用户执行 region级的操作,并且可以使用与RDBMS中触发器(trigger)类似的功能
协处理器分两种类型,系统协处理器可以全局导入region server上的所有数据表,表协处理器即是用户可以指定一张表使用协处理器。协处理器框架为了更好支持其行为的灵活性,提供了两个不同方面的插件。
一个是观察者(observer),类似于关系数据库的触发器。
另一个是终端(endpoint),动态的终端有点像存储过程。Observer : 在region server上执行,而且是自动执行Endpoint: 在region server上执行,但是要由客户端通过请求来调用执行
观察者(Observers)
RegionObserver:针对Region的观察者,可以监听关于Region的 操作。
RegionServerObserver:针对RegionServer的观察者,可以监听 整个RegionServer的操作。
MasterObserver:针对Master的观察者,可以监听Master进行的 DDL操作。
WALObserver:针对WAL的观察者,可以监听WAL的所有读写操 作。
BulkLoadObserver:BulkLoad是采用MapReduce将大量数据快速 地导入HBase的一种方式。BulkLoadObserver可以监听BulkLoad行为。
EndpointObserver:可以监听EndPoint的执行过程。在此我并没有列出这些接口的所有基本实现类,只列出最常用的两种基本实现类:BaseRegionObserver:实现了RegionObserver接口的所有需要实 现的方法,并给出了最简单实现。
BaseMasterObserver:实现了MasterObserver接口的所有需要实 现的方法,并给出了最简单的实现。
终端
只有一个接口CoprocessorService,并且没有提供基本的实现类。 该接口只有一个方法需要实现:getService,该方法需要返回Protocol Buffers(Google开发的第三方库,用来实现一种数据传输格式,类似 XML,但是比XML更节省传输资源)的Service实例,所以实现Endpoint 之前,还需要了解一下Protocol Buffers的相关知识作为基础。
应用背景
Hbase作为列族数据库最经常被人诟病的特性包括:无法轻易建立“二级索引”,难以执行求和、计数、排序等操作。比如,在旧版本的(<0.92)Hbase中,统计数据表的总行数,需要使用 Counter 方法,执行一次 MapReduce Job 才能得到。虽然 HBase在数据存储层中集成了 MapReduce,能够有效用于数据表的分布式计算。然而在很多情况下,做一些简单的相加或者聚合计算的时候,如果直接将计算过程放置在 server 端,能够减少通讯开销,从而获得很好的性能提升。
13,2 基本使用
编写协处理器打包成jar包上传到HDFS中
加载协处理器
1 静态加载 全局加载
通过修改 hbase-site.xml 这个文件来实现,启动全局 aggregation,能过操纵所有的表上的数据。只需要添加如下代码
<property>
<name>hbase.coprocessor.user.region.classes</name>
<value>org.apache.hadoop.hbase.coprocessor.AggregateImplementation</value>
</property>
2 动态加载
- 先将开发好的协处理器类打成jar包,并上传到hdfs的某个目录
- 禁用目标表 disable ‘star’
- 修改目标表的Coprocessor属性
hbase alter 'users', METHOD => 'table_att', 'Coprocessor'=>'hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar| org.myname.hbase.Coprocessor.RegionObserverExample|1073741823| arg1=1,arg2=2'
参数解释
'users' 拦截的目标表
METHOD => 'table_att' 这个表示alter命令要对users表做属性修改操作
hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar 协处理器类所在的jar包
org.myname.hbase.Coprocessor.RegionObserverExample 自定义的协处理器实现类
1073741823 协处理器的执行顺序号,可以为任意数值,小的会优先执行
arg1=1,arg2=2 自定义协处理器所需要的参数
注意:万一加协处理器导致集群崩溃,可以尝试这样拯救:
修改hbase-site.xml,加入如下配置:
|
3 卸载
- 禁用表 disable 'mytable'
- 卸载 alter 'mytable',METHOD=>'table_att_unset',NAME=>'coprocessor$1'
- 启用表 enable 'mytable'
13,3 二级索引案例
13.3.1什么是二级索引
HBase的查询都是通过RowKey(要把多条件组合查询的字段都拼接在RowKey中显然不太可能),或者全表扫描再结合过滤器筛选出目标数据(太低效),所以通过设计HBase的二级索引来解决这个问题。
多个查询条件构成了多维度的组合查询,需要根据不同组合查询出符合条件的数据。
例如:
按照电影维度查询数据适合,但是按照uid就不适合 , 使用二级索引分两步查询实现高效获取数据,因为两次都是通过ROWKEY查询的数据
13,3.2 创建代码实现
思路 在插入数据的时候 , 如果遇到uid属性 , 那么就在二级索引表中插入 K: uid 和 V:主表的rowkey
/**
* 使用协处理器为电影表建立索引表
* 目的是为了快速的按照uid查询数据
* @author 多易教育-行哥
*
*/
public class Demo implements RegionCoprocessor , RegionObserver{
// 获取协处理器的对象
@Override
public Optional<RegionObserver> getRegionObserver() {
// TODO Auto-generated method stub
return Optional.of(this);
}
/**
* 在put之前执行的方法
* 操作put对象 获取put的值
*/
@Override
public void prePut(ObserverContext<RegionCoprocessorEnvironment> c, Put put, WALEdit edit, Durability durability)
throws IOException {
Configuration configuration = HBaseConfiguration.create();
configuration.set("hbase.zookeeper.quorum", "doit001:2181,doit002:2181,doit003:2181");
Connection connection = ConnectionFactory.createConnection(configuration);
Table table = connection.getTable(TableName.valueOf("index"));
// (一行) 获取put中的单元格对应的所有的列族
NavigableMap<byte[],List<Cell>> map = put.getFamilyCellMap();
// 遍历所有的单元格 ,获取属性名 , 如果属性名是uid 将uid插入到index表中
Set<Entry<byte[],List<Cell>>> set = map.entrySet();
for (Entry<byte[], List<Cell>> entry : set) {
List<Cell> cells = entry.getValue();
// 获取插入的所有的单元格
for (Cell cell : cells) {
// 获取属性
byte[] cloneQualifier = CellUtil.cloneQualifier(cell);
// 当属性值是uid的时候 将数据插入到index表中 uid的值(rk) 行建
if("uid".equals(Bytes.toString(cloneQualifier))) {
// uid的值 index表的key
byte[] rk = CellUtil.cloneValue(cell);
String uid = Bytes.toString(rk);
long timeMillis = System.currentTimeMillis();
String index_rk = uid +"_"+timeMillis ;
// index表的值
byte[] rowkey = CellUtil.cloneRow(cell);
// 创建新的put 将put 插入到index表中
Put p = new Put(index_rk.getBytes());
p.addColumn("f".getBytes(), "mid_time".getBytes(), rowkey);
// index表的对象
table.put(p);
}
}
}
}
// 开启region
@Override
public void start(CoprocessorEnvironment env) throws IOException {
}
// 关闭
@Override
public void stop(CoprocessorEnvironment env) throws IOException {
}
}