因为对HBase的使用比较多,包括之前的博客里,也分享了不少关于HBase的使用和踩过的坑
本篇博客总结过程中,会或多或少借鉴以前的内容,重点在于
实际工作中的HBase优化
要点如下:
表设计优化
创建表格的时候合理预分区
说到表结构优化,首先就是在建表的时候进行合理的预分区
那么预分区能有多大的效率提升?
应用实例:
未分区之前 :
在使用bulkload入库过程中,需要写入的表没有进行预分区,
map阶段一切正常,但是reduce阶段在界面上监控只有一个!!
相当于是单线程入库,效率可想而知(我当时需要入库的数据量是1.5T~~ 真按照这种方式写数据,估计数据还没写完,我已经被公司开除了)
分区之后 :
发现这个问题之后,查找了相关资料,根据我们的业务需要对表进行了预分区(分了200个分区)
reduce阶段从界面上监控201个!! 一个小时,数据写完!(100多G内存)
所以,在极端的情况下(预分区合理,并且集群资源足够),预分区的数目有多少个,效率就能快多少倍,甚至更多!!
(因为不进行预分区的场景,随着数据量的不断增加,此region已经不能承受不断增长的数据量,会进行split,分成2个region。
在此过程中,会产生两个问题:
1.数据往一个region上写,会有写热点问题。
2.region split会消耗宝贵的集群I/O资源) 会严重的影响数据写入效率,甚至让集群崩溃)
预分区方法:
a.使用shell脚本进行预分区:
create 'XUE_BULKLOAD','info',{SPLITS => [ '1','2','3', '4','5','6','7','8','9']}
这样就成功的将表名为 ‘XUE_BULKLOAD’,列簇名为’info’的表在建表时预分了10个分区
界面显示如下 :
b.使用java代码在建表的时候进行预分区:
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum","linux01:2181,linux02:2181,linux03:2181");
Connection conn = ConnectionFactory.createConnection(conf);
Admin admin = conn.getAdmin();
HTableDescriptor hbaseTable = new HTableDescriptor(TableName.valueOf("XUE_BULKLOAD"));
HTableDescriptor columnDesc = new HColumnDescriptor("info"); // 这个方法实际上相当于传入一个Byte数组
hbaseTable.addFamily(columnDesc);
columnDesc.setMaxVersions(1);
byte[][] regions =new byte[][] { Bytes.toBytes("0"), Bytes.toBytes("1"), Bytes.toBytes("2"), Bytes.toBytes("3"),Bytes.toBytes("4"), Bytes.toBytes("5"), Bytes.toBytes("6"), Bytes.toBytes("7"), Bytes.toBytes("8"), Bytes.toBytes("9") };
admin.createTable(hbaseTable, regions);
这段代码实现的功能和上述linux脚本实现的功能一致,关键在于admin.createTable(hbaseTable, regions);
即,建表的时候不是采用Admin的如下api
default void createTable(TableDescriptor desc)
Creates a new table.
而是使用下述api来实现建表的时候预分区的功能
default void createTable(TableDescriptor desc, byte[][] splitKeys)
Creates a new table with an initial set of empty regions defined by the specified split keys.
注意,建表的时候进行预分区还可以采用如下api来 创建具有指定分区的新表
void createTable(TableDescriptor desc, byte[] startKey, byte[] endKey, int numRegions)
Creates a new table with the specified number of regions.
关于HBase相关相关的api,更多的细节可以参照HBase官网文档,链接如下:
想要进一步了解HBase预分区相关内容的,可以参考这位大神的博客,在我最初接触HBase的时候,给了很大的启发 :
大数据查询——HBase 读写设计与实践
行键设计
提到HBase表设计,另一个老生常谈的话题就是行键的设计,行键可以说是整个HBase的核心内容,
HBase的写入,查询,存储都和行键密不可分,豪不夸张的说,行键设计的好坏,直接影响HBase的性能
即使是我上面提到的预分区,和行键的设计也是息息相关的
试想,就算我分1000个区,但是行键设计不合理,所有的数据都落在一个或几个分区,依然会严重影响写入,查询的效率
那么,HBase的行键应该如何来设计呢?
唯一性
Hbase是根据行键来进行检索和数据写入的,首先需要保证的就是行键的唯一性
方便检索
这一点,在HBase行键的优化上,很少被提及,但我觉得确实非常重要的
有很多时候,为了保证数据能够均匀的落在每一个分区里,在行键设计上采用了单纯的随机散列方式.
这种情形存储的时候数据是能够保证数据的均匀,但是该如何检索呢?
假设我这个表里面存储的是某个月全部用户的全量交易数据,
然后收到了一些用户的投诉,想要找到这部分用户发生投诉那天的交易数据,
因为随机散列的原因,这些用户的数据大概率散落在各个分区,而且通过行键查询根本就无从下手, 通过其它的方式,在数据量很大的情况下,效率慢的吓人
啰嗦这么久,想要强调的就是: 很多时候,我们在考虑架构的时候,也要考虑业务的便捷性
比如,上述的场景我们可以这样设计 :
分区 : 00,01, … 99分为100个
行键设计 : 4位尾号 + 用户号码 +交易时间(yyyyMMddHHmmss)
设计原理 : 用户的四位尾号可以认为是几乎随机的,这样数据基本上就均匀的分布在了每一个分区内
并且,同一个用户的数据一定在一个分区,
这样查询某个用户数据的时候,就只需要检索一个分区,大大降低了检索的成本
拿上述的投诉案例来说 :
我只需要通过投诉用户的手机号,限定起始时间为startKey,结束时间为endKey,查询这部分的数据,响应时间是毫秒级的!
长度设计
在满足唯一性,业务合理性的基础上,建议是越短越好
why?
- HBase是按照Key-Value存储数据的,Rowkey过长,会占用存储空间,影响HFile的存储效率
- 在形成HFile之前,HBase的数据会以memstore的形式存储在内存里,Rowkey过长,同样的内存存储的数据量就会降低,比如说原来内存可以缓存100W条数据,现在可能只能够缓存70W条,这样会降低检索效率
- 最优的方案当然是设计成8的正整数倍,因为现在的操作系统是64位, 内存8字节对齐. 但是这个只是理想状态,实际工作中很难做到,依旧拿我刚才设置的行键举例子,之前的行键是29个字节,如果我为了凑8的整数倍, 强行搞成32个字节,那显然是脑子有泡,如果我强行缩成24个字节,又很难符合我们的业务场景,所以这个就仁者见仁,智者见智了
保证数据均匀的分布在每个分区
这个是HBase行键设计的核心,行键设计好,才能体现出预分区的价值,才能体现出HBase在大批量数据写入和查询的优秀性能
实际中主要考虑以下几种情景:
1. 行键设计应尽量避免时间热点
假设一张存储全年交易量的大表,按照天分为365个分区,
设计的时候,行键按照时间递增,保证每天的数据存放在其中的一个分区里
这样的设计乍一看没有问题,但是如果考虑到时间热点情况呢?
比如说淘宝双11当天的数据量, 可能是平时数据量的十几倍甚至几十倍?
按照这种方式设计行键,一定会在双11那天发生严重的负载不均.
存储双11数据的那个分区会发生频繁的split,严重影响集群性能,甚至是导致集群IO过高挂掉
所以,为了避免这种问题发生,不要将时间放在二进制码的最前面,前面可以加一些散列字段,但最好是和业务密切相关.
但取值随机的字段(这样是为了方便后续根据行键查询),这样将提高数据均衡分布在每个Regionserver的概率.
2. 逻辑上相关,经常放在一起查询的数据,行键应尽可能相近
假设表里面有1亿条数据,分为100个分区, 我需要查询的数据量大约是10万条(这10万条数据查询的逻辑是统一的)
那么假设这10W条数据是随机分布的, 也就是说我要检索100个分区才能找齐这10W条数据,这个检索效率可想而知.
反之,如果我把这些数据想办法放在一起,是不是只要检索一个分区的数据就够了呢?
列簇设计
1. 列簇数量尽可能少
其实在大多数场景下,设计一个列簇就足够了
在HBase中,高表比宽表性能好,可以设计多张表来满足需求
2. 经常一起查询的列放在一个列簇里
把查询经常用到的列放在一个列簇里
查询不经常用到的列放在令一个列簇里
使用命名空间进行权限控制
在关系型数据库里,我们常常见到这样的场景,某个用户只对指定的某个或者某几个库有操作权限,这是权限控制的常用手段之一
在HBase,也可以通过类似的方式来实现权限管理
比如说 :
我们公司有好几个业务小组都在使用HBase, 但是这几个业务小组公用一个HBase集群.
试想,如果不把权限区分开的话,如果某个同事在操作中误删了其余小组的数据,你说说该找谁说理去?
这种情形下,就可以使用命名空间来规避这个问题,每个小组负责管理一个命名空间,并且仅对该命名空间下的表具有操作权限
命名空间的使用如下 :
create_namespace 'xmr_ns' #创建一个命名空间
create 'xmr_ns:testtable3','info' #在刚刚创建的命名空间下面建表,列簇名为 : info
既然命名空间主要是为了权限控制所做的,那它的授权操作一定要叙述下了!
grant 'hmaster','W','@xmr_ns' #授予用户hmaster对于命名空间xmr_ns的写权限
# 其中权限包括:RWXCA
revoke 'hmaster', '@xmr_ns' #回收用户hmaster对于命名空间xmr_ns的全部权限
HBase的类似shell操作,我在之前写过的一篇博客有过介绍,有兴趣的可以去看下,博客链接如下 :
HBase的高阶shell操作
使用优化
写入优化
选择合适的写入方式
Hbase写入的方式有四种 :
单条put
批量put
MapReduce
bulkload
如果是大规模离线数据的批量写入,想都不用想, bulkload
如果对实时要求性非常高的场景, 单条put
准实时的写入场景,批量put
关于这几种写入方式的实现和对比,我在之前的一篇博客中进行过比较详细的介绍,
感兴趣的同学可以参考一下,博客链接如下:
MapReduce入库优化
这种入库方式,优化思路可以参考普通的MapReduce,我在之前的一篇博客有过比较详细的介绍
bulkload入库优化
主要包括两点 : 预分区,以及必要的参数调整
预分区在该篇博客的上半部分已经有所介绍
但是使用bulkload方式,在不对集群的参数进行任何调整的前提下,在入库大批量数据时(超过32G)经常会碰到如下问题 :
Trying to load more than 32 hfiles to one family of one region
18/01/18 23:20:36 ERROR mapreduce.LoadIncrementalHFiles: Trying to load
more than 32 hfiles to family info of region with start key
Exception in thread "main" java.io.IOException: Trying to load more than
32 hfiles to one family of one region
at org.apache.hadoop.hbase.mapreduce.LoadIncrementalHFiles.doBulkLoad
(LoadIncrementalHFiles.java:377)
at hbase_Insert.Hbase_Insert.main(Hbase_Insert.java:241)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(
NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(
DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.apache.hadoop.util.RunJar.run(RunJar.java:221)
at org.apache.hadoop.util.RunJar.main(RunJar.java:136)
原因 : HBase在Bulk Load时默认一个region的hfile个数是32,当hfile文件个数超过32个时则会报上述错误。
这种情况下,就需要调整两个关键的参数,来保证入库的成功
hbase.hregion.max.filesize
单个ColumnFamily的region大小,若按照ConstantSizeRegionSplitPolicy策略,超过设置的该值则自动split 默认的大小是1G
hbase.mapreduce.bulkload.max.hfiles.perRegion.perFamily
允许的hfile的最大个数,默认配置是32
也就是说:这两个参数的默认值决定了,单个分区每次批量入库的数据量不能超32个G,超过这个数量就会导致入库失败
可以在代码里,或者在hbase安装路径下conf目录下的hbase-site.xml里面针对这两个参数进行设置
为了一劳永逸,我选择在hbase-site.xml里面进行设置,设置结果如下:
<property>
<name>hbase.hregion.max.filesize</name>
<value>10737418240</value>
</property>
<property>
<name>hbase.mapreduce.bulkload.max.hfiles.perRegion.perFamily</name>
<value>3200</value>
</property>
分区大小 : 10G , HFile文件个数 : 3200个
因为我们的数据量比较大,所以这两个值设置的很大.
正常情况下,分区大小按照默认的1G就基本上能够满足日常需求的
查询优化
优先选用行键查询
即: 能通过行键查询获取结果就不要考虑其它方式
依旧拿我之前提到的场景举例 : 行键设置为 : 手机尾号后四位 + 手机号 + 交易时间
同时,手机号也是我表里面的一个列
这时, 如果我想查某个用户号码的数据,可以选择根据列来查询,也可以选择根据行键来查询
但是,实际查询的时候你会发现, 二者的效率天差地别, 数据量很大的情况下,行键是秒级,按列查可能需要好几十分钟
所以,能用行键查到的,就别想着其他的方式了
如果业务场景需要根据多条件查询,也可以在行键设计里补充这些查询维度,以提供更快的检索效率
使用行键查询有两种方式 :
1、按指定RowKey获取唯一一条记录,get方法(org.apache.hadoop.hbase.client.Get)
2. 使用Scan对象,通过setStartRow与setEndRow来限定范围([start,end)start是闭区间,end是开区间)。
范围越小,性能越高。
通过巧妙的RowKey设计使我们批量获取记录集合中的元素挨在一起(应该在同一个Region下),
可以在遍历结果时获得很好的性能。
实际工作中通常是使用第二种方式 , 因为毕竟只查一条记录只适用于很少的业务场景
使用StartKey和EndKey一个简单的实例如下 :
String startkey = "00_460075097670490_1534925332480";
String endkey = "00_460075097670490_1534925432480"
Scan scan = new Scan();
scan.setStartRow(startkey.trim().getBytes());
scan.setStopRow(endkey.trim().getBytes());
更丰富的HBase查询的shell和java api操作,我在之前的一篇博客里面详细的介绍过 :
Sacn查询使用setCaching与setBatch方法
其实就是以空间换时间的策略
scan中的setCaching与setBatch方法的区别是什么呢?
setCaching设置的值为每次rpc的请求记录数,默认是1;
cache大可以优化性能,但是太大了会花费很长的时间进行一次传输。
setBatch设置每次取的column size;
有些row特别大,所以需要分开传给client,就是一次传一个row的几个column。
batch和caching和hbase table column size共同决意了rpc的次数。
通过下表可以看出caching/batch/rpc次数的关系:
实例详情 : 10 rows, 2 families, 10column per family,total:200 cell
缓存 | 批量处理 | Result个数 | RPC次数 | 说明 |
---|---|---|---|---|
1 | 1 | 200 | 201 | 每个列都作为一个Result实例返回。最后还多一个RPC确认扫描完成 |
200 | 1 | 200 | 2 | 每个Result实例都只包含一列的值,不过它们都被一次RPC请求取回 |
2 | 10 | 20 | 11 | 批量参数是一行所包含的列数的一半,所以200列除以10,需要20个result实例。同时需要10次RPC请求取回。 |
5 | 100 | 10 | 3 | 对一行来讲,这个批量参数实在是太大了,所以一行的20列都被放入到了一个Result实例中。同时缓存为5,所以10个Result实例被两次RPC请求取回。 |
5 | 20 | 10 | 3 | 同上,不过这次的批量值与一行列数正好相同,所以输出与上面一种情况相同 |
10 | 10 | 20 | 3 | 这次把表分成了较小的result实例,但使用了较大的缓存值,所以也是只用了两次RPC请求就返回了数据 |
要计算一次扫描操作的RPC请求的次数,用户需要先计算出行数和每行列数的乘积。
然后用这个值除以批量大小和每行列数中较小的那个值。
最后再用除得的结果除以扫描器缓存值。
用数学公式表示如下:
RPC请求的次数=(行数x每行的列数)/
Min(每行的列数,批量大小)/扫描器缓存
并发优化
HBase的使用场景,绝大多数都要面临着高并发的写入和读取
因此在并发场景中更好的使用HBase,也是HBase优化中很重要的一环
使用同一个HBaseConfiguration来创建Table实例
实际开发中我们经常碰到如下的写法 :
Configuration conf = HBaseConfiguration.create();
Connection conn = ConnectionFactory.createConnection(conf);
Table hTable = conn.getTable(TableName.valueOf('tableName'));
Configuration conf2 = HBaseConfiguration.create();
Connection conn2 = ConnectionFactory.createConnection(conf2);
Table hTable2 = conn2.getTable(TableName.valueOf('tableName'));
粗看上面我们可能觉得比较奇怪,但实际中我们经常会涉及到多次操作同一张表的情形,
在不特别注意的情况下,就会经常出现上述的写法
实际上,这两个Table对象虽然操作同一个table,但是建立了两个connection,它们的socket不是公用的,在多线程的情况下,zk的链接达到一定的阈值,新建立的链接会挤掉原先的connection,导致线程不安全
所以,应该采用下面的写法来规避上述问题:
Configuration conf = HBaseConfiguration.create();
Connection conn = ConnectionFactory.createConnection(conf);
Table hTable = conn.getTable(TableName.valueOf('tableName'));
Table hTable2 = conn.getTable(TableName.valueOf('tableName'));
参数优化
参数优化,只挑我用过的来介绍:
参数名称 | 参数含义 | 默认配置 | 线上配置 |
---|---|---|---|
zookeeper.session.timeout | 客户端与zk连接超时时间 | 180000(3min) | 1200000(20min) |
hbase.cluster.distributed | 集群的模式,分布式还是单机模式,如果设置成false的话,HBase进程和Zookeeper进程在同一个JVM进程。 | false | true |
hbase.regionserver.handler.count | regionserver处理IO请求的线程数 | 10 | 50 |
hbase.client.write.buffer | 客户端写buffer,设置autoFlush为false时,当客户端写满buffer才flush | 2097152(2M) | 8388608(8M) |
hbase.hregion.max.filesize | 单个ColumnFamily的region大小,若按照ConstantSizeRegionSplitPolicy策略,超过设置的该值则自动split | 1073741824(1G) | 10737418240(10G) |
hbase.hregion.memstore.block.multiplier | 超过memstore大小的倍数达到该值则block所有写入请求,自我保护 | 2 | 8(内存够大可以适当调大一些,出现这种情况需要客户端做调整) |
hbase.regionserver.maxlogs | regionserver的hlog数量 | 32 | 128 |
hbase.regionserver.hlog.blocksize | hlog大小上限,达到该值则block,进行roll掉 | hdfs配置的block大小 | 536870912(512M) |
hbase.hstore.compaction.min | 进入minor compact队列的storefiles最小个数 | 3 | 10 |
hbase.hstore.compaction.max | 单次minor compact最多的文件个数 | 10 | 30 |
hbase.hstore.blockingStoreFiles | 当某一个region的storefile个数达到该值则block写入,等待compact | 7 | 100 |
hbase.hregion.majorcompaction | 触发major compact的周期 | 86400000(1d) | 0(关掉major compact) |
hbase.regionserver.thread.compaction.large | large compact线程池的线程个数 | 1 | 5 |
hbase.regionserver.thread.compaction.small | small compact线程池的线程个数 | 1 | 5 |
hbase.rpc.timeout | RPC请求timeout时间 | 60000(10s) | 300000(5min) |
hbase.mapreduce.bulkload.max.hfiles.perRegion.perFamily | 单分区单列簇允许的hfile的最大个数 | 32 | 3200 |