实际工作中的HBase优化

因为对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官网 api

想要进一步了解HBase预分区相关内容的,可以参考这位大神的博客,在我最初接触HBase的时候,给了很大的启发 :
大数据查询——HBase 读写设计与实践

行键设计

提到HBase表设计,另一个老生常谈的话题就是行键的设计,行键可以说是整个HBase的核心内容,
HBase的写入,查询,存储都和行键密不可分,豪不夸张的说,行键设计的好坏,直接影响HBase的性能
即使是我上面提到的预分区,和行键的设计也是息息相关的
试想,就算我分1000个区,但是行键设计不合理,所有的数据都落在一个或几个分区,依然会严重影响写入,查询的效率
那么,HBase的行键应该如何来设计呢?

唯一性

Hbase是根据行键来进行检索和数据写入的,首先需要保证的就是行键的唯一性

方便检索
	这一点,在HBase行键的优化上,很少被提及,但我觉得确实非常重要的
	有很多时候,为了保证数据能够均匀的落在每一个分区里,在行键设计上采用了单纯的随机散列方式.
	这种情形存储的时候数据是能够保证数据的均匀,但是该如何检索呢?
	
	假设我这个表里面存储的是某个月全部用户的全量交易数据,
	然后收到了一些用户的投诉,想要找到这部分用户发生投诉那天的交易数据,
	因为随机散列的原因,这些用户的数据大概率散落在各个分区,而且通过行键查询根本就无从下手, 通过其它的方式,在数据量很大的情况下,效率慢的吓人

啰嗦这么久,想要强调的就是: 很多时候,我们在考虑架构的时候,也要考虑业务的便捷性

比如,上述的场景我们可以这样设计 :

分区 : 00,01, … 99分为100个
行键设计 : 4位尾号 + 用户号码 +交易时间(yyyyMMddHHmmss)

设计原理 : 用户的四位尾号可以认为是几乎随机的,这样数据基本上就均匀的分布在了每一个分区内
                 并且,同一个用户的数据一定在一个分区,
                 这样查询某个用户数据的时候,就只需要检索一个分区,大大降低了检索的成本

拿上述的投诉案例来说 :
我只需要通过投诉用户的手机号,限定起始时间为startKey,结束时间为endKey,查询这部分的数据,响应时间是毫秒级的!

长度设计

在满足唯一性,业务合理性的基础上,建议是越短越好

why?

  1. HBase是按照Key-Value存储数据的,Rowkey过长,会占用存储空间,影响HFile的存储效率
  2. 在形成HFile之前,HBase的数据会以memstore的形式存储在内存里,Rowkey过长,同样的内存存储的数据量就会降低,比如说原来内存可以缓存100W条数据,现在可能只能够缓存70W条,这样会降低检索效率
  3. 最优的方案当然是设计成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
关于这几种写入方式的实现和对比,我在之前的一篇博客中进行过比较详细的介绍,
感兴趣的同学可以参考一下,博客链接如下:

hbase批量入库遇到的坑

MapReduce入库优化

这种入库方式,优化思路可以参考普通的MapReduce,我在之前的一篇博客有过比较详细的介绍

实际工作中的 Map Reduce优化

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操作,我在之前的一篇博客里面详细的介绍过 :

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次数说明
11200201每个列都作为一个Result实例返回。最后还多一个RPC确认扫描完成
20012002每个Result实例都只包含一列的值,不过它们都被一次RPC请求取回
2102011批量参数是一行所包含的列数的一半,所以200列除以10,需要20个result实例。同时需要10次RPC请求取回。
5100103对一行来讲,这个批量参数实在是太大了,所以一行的20列都被放入到了一个Result实例中。同时缓存为5,所以10个Result实例被两次RPC请求取回。
520103同上,不过这次的批量值与一行列数正好相同,所以输出与上面一种情况相同
1010203这次把表分成了较小的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进程。falsetrue
hbase.regionserver.handler.countregionserver处理IO请求的线程数1050
hbase.client.write.buffer客户端写buffer,设置autoFlush为false时,当客户端写满buffer才flush2097152(2M)8388608(8M)
hbase.hregion.max.filesize单个ColumnFamily的region大小,若按照ConstantSizeRegionSplitPolicy策略,超过设置的该值则自动split1073741824(1G)10737418240(10G)
hbase.hregion.memstore.block.multiplier超过memstore大小的倍数达到该值则block所有写入请求,自我保护28(内存够大可以适当调大一些,出现这种情况需要客户端做调整)
hbase.regionserver.maxlogsregionserver的hlog数量32128
hbase.regionserver.hlog.blocksizehlog大小上限,达到该值则block,进行roll掉hdfs配置的block大小536870912(512M)
hbase.hstore.compaction.min进入minor compact队列的storefiles最小个数310
hbase.hstore.compaction.max单次minor compact最多的文件个数1030
hbase.hstore.blockingStoreFiles当某一个region的storefile个数达到该值则block写入,等待compact7100
hbase.hregion.majorcompaction触发major compact的周期86400000(1d)0(关掉major compact)
hbase.regionserver.thread.compaction.largelarge compact线程池的线程个数15
hbase.regionserver.thread.compaction.smallsmall compact线程池的线程个数15
hbase.rpc.timeoutRPC请求timeout时间60000(10s)300000(5min)
hbase.mapreduce.bulkload.max.hfiles.perRegion.perFamily单分区单列簇允许的hfile的最大个数323200
参考资料

HBase官网 api

大数据查询——HBase 读写设计与实践

HBase的高阶shell操作

hbase批量入库遇到的坑

实际工作中的 Map Reduce优化

hbase shell及 java api的过滤器操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值