云湖湖导读:
Spark与HBase是当今非常火的两个大数据开源项目,一个负责数据的分析处理,一个负责数据的存储。
近年来,Spark on HBase尤其是Spark SQL on HBase成为许多企业云上大数据与AI解决方案的首选。两者的结合,不仅兼顾了计算与存储,还兼顾了易用与性能。本文将会通过以下几点来分享:
1、什么是HBase
2、华为云DLI在Spark SQL on HBase的项目实践
3、查询性能优化思路
4、深度优化:Rowkey的区间范围分析
更多优质内容请关注微信公众号“智能数据湖”
1、什么是HBase
Apache HBase是一个开源的,分布式的,版本化的非关系数据库,其目标是在大型商业硬件集群上能托管非常大的表与数十亿行*百万列的数据。
HBase支持海量列式稀疏数据存储、极易扩展、高并发等特性,HBase有着弹性、灵活、毫秒级点查等优势,它的出现打破了传统数据库存储的界限,使得大数据技术得到了更进一步的发展。
接下来介绍几个会在实践中应用到的基本概念,来重点了解HBase。
1.1Rowkey
Rowkey是HBase中的一级索引,概念和MySQL中的主键是完全一样的,HBase使用Rowkey来唯一区分某一行的数据。
Rowkey是二进制存放数据的,数据根据字节递增排序,且不重复。比如,将数据(”1″, “2”, “11”, “a”, “A”, “b”, “abc”)作为Rowkey存放在HBase中的一张表中,Rowkey是这样排序的:
由于每个字节的范围是0x00~0xFF,0xFF相当于数字-1,因此,在字节的世界里面,0是最小的,-1是最大的。
1.2Region
Region的概念和关系型数据库的分区或者分片差不多。HBase会将一个大表的数据基于Rowkey的不同范围分配到不同的Region中,每个Region负责一定范围的数据访问和存储。这样即使是一张巨大的表,由于被切割到不同的Region,访问起来的时延也很低。
1.3HBase Table
HBase Table会根据数据大小拆分成多个Region存储在不同的Region server中,每个Region在Region server中又根据不同的列簇拆分成不同的HFile文件存放到HDFS中。
并发读取HBase数据,核心点就是Region,它是根据Rowkey进行拆分的,每个Region中的Rowkey都是连续的。在RDD中的getPartition接口里面,最简单的方式就是根据Region的个数来设置并发。
图 Spark并发访问HBase Table
在HBase中Region的描述类TableRegionModel中,有getStartKey与getEndKey两个方法,而在HBase Client中的Scan里面也有setStartRow与setStopRow两个方法。所以,我们可以先获取目的表的Region信息,拿到每一个Region的StartKey与EndKey,并且set到Scan里面来指定HBase表的查找范围。
图 Spark Datasource查询HBase时序图
2、华为云DLI在Spark SQL on HBase的项目实践
2.1场景描述
这是一个车联网的场景,用户可通过输入车辆的id和查询的时间范围来查找规定时间内的车辆信息(比如运行轨迹、车况等等)。由于车辆信息指标项庞大并且各种不同型号的车辆指标项存在差异,因此需要使用适合大宽表与稀疏列存储的HBase来作为该场景解决方案。
图 车联网场景Spark on HBase解决方案的流程图
灰色部分是数据的采集和存储,使用了实时大数据服务。黄色部分是对存储在HBase中的车辆信息历史数据进行查询与计算。
注:在代码解读中,提及的HBase对应华为云上CloudTable,Spark对应华为云上DLI。
2.2项目实践 on DLI
在华为云的解决方案中,黄色部分的Spark SQL模块对应的是华为云数据湖探索(DLI)产品,该企业通过DLI访问HBase数据库来查询相关车辆信息。看了上面场景的流程图之后,很多人会有疑问为什么接收指令后不能直接访问HBase,而需要通过Spark SQL进一步处理?这就来看看DLI于数据查询的优势。
DLI的优势
易用性
DLI封装了Spark SQL的API,并且进行了并发控制与计算下压。对于企业用户来讲,只需要会写SQL就能够做到访问HBase了,不用再过多的去考虑如何访问HBase,如何提高性能。
通用性
DLI的通用性源于云服务背景,除了要支持特定场景的需求,还必须能应用到其他企业项目场景,与不同系统兼容。
在Apache Phoenix中,组合Rowkey是使用一套自己的编解码的,但这里就会有一层限制,就是数据的写入和读取都必须使用Apache Phoenix,否则的话开发一套适配Phoenix编解码的模块代价是非常大的。
场景分析
从实践场景中的流程图中可以看到,数据的写入和读取是两套不同的系统;同时,业务沟通中了解到所需的Rowkey字段都是长度固定的String类型。
综合以上2点考虑,我们确定了两种使用RowKey的方式。
第一种是不限制类型与数据长度的单Rowkey方式:
CREATE TABLE [IF NOT EXISTS] TABLE_NAME (
ATTR1 TYPE,
ATTR2 TYPE,
ATTR3 TYPE)
USING CLOUDTABLE OPTIONS (
‘ClusterId’=’xx’,
‘ZKHost’=’xx’,
‘TableName’=’TABLE_IN_CLOUDTABLE’,
‘RowKey’=’ATTR1’,
‘Cols’=’ATTR2:CF1.C1, ATTR3:CF1.C2’);
第二种是支持String类型定长的组合RowKey:
CREATE TABLE [IF NOT EXISTS] TABLE_NAME (
ATTR1 String,
ATTR2 String,
ATTR3 TYPE)
USING CLOUDTABLE OPTIONS (
‘ClusterId’=’xx’,
‘ZKHost’=’xx’,
‘TableName’=’TABLE_IN_CLOUDTABLE’,
‘RowKey’=’ATTR1:2, ATTR2:10’,
‘Cols’=’ATTR2:CF1.C1, ATTR3:CF1.C2’);
PS:斜体的字段名字冒号后面跟着的数字是字符长度。在实际应用上,结合Spark SQL中的函数,就能够满足绝大部分的场景。
2.3性能优化问题
在一般的SQL on HBase项目中,对查询HBase的性能会做两点通用优化:根据HBase表的Region个数设置并发和过滤条件下压(SingleColumnValueFilter和RowFilter),但是这对查询性能并没有多大帮助。
HBase Table的Region个数设置并发
虽然根据Region个数起并发,但是HBase对于表的Region拆分拥有很高的门槛,一方面是默认情况下往往需要表的大小达到十几到几十G左右才进行自动拆分,每个Region实际上的数据量还是很大;另一方面是由于实际数据的影响,对Region的自动拆分经常会出现数据倾斜,部分Region拥有上百万条数据,而部分Region只有一两条数据。并且让用户手动拆分Region也是一个比较复杂的操作,因为很难评估拆分的点在哪里。这就导致了很多时候,数据量没有达到的情况下,只有一个并发task在执行读取任务。而在并发读取的时候,又因为数据倾斜,导致有些task很快就执行完成了,有些task又执行的特别慢。
图 HBase自动拆分Region易导致数据分布不均
过滤条件下压
1、SingleColumnValueFilter是有一些作用的,但是由于HBase是根据RowKey进行索引的,如果没有确定RowKey,对列的过滤效果还是不够明显(二级索引除外)。
2、RowFilter也是没有明显的效果,因为在HBase中,即使带上了RowFilter,还是会进行全表扫描然后再执行过滤。唯一的作用是,相比较于将所有的数据读取到Spark中做过滤,RowFilter能使HBase中返回的数据量减小,这样减少了Spark与HBase两个组件之间的网络通信压力。
图 不进行过滤,将全表数据拉回Spark会增加网络压力
图 RowFilter下压后,返回过滤后的数据能够减小网络压力
3、优化思路
3.1使用Get,对HBase读取优化
确定RowKey的时候使用Get进行点查。
用一个例子来进行说明:
CREATE TABLE test1 (ID INT, NAME STRING)
USING HBASE OPTIONS (
‘ZKHost’=’xx’,
‘TableName’=’TABLE_IN_CLOUDTABLE’,
‘RowKey’=’ID’,
‘Cols’=’NAME:CF1.C1);
上面SQL建了一张HBase关联表,有ID和NAME两个字段,其中ID是Rowkey。如果查询语句是“select * from test1 where id=1;”,那么在读取的时候,就只需要Get Rowkey等于1的那一行数据即可,而不用使用Scan + RowFilter了。
图 Spark使用Get获取HBase数据示例
再进一步,如果查询语句是“select * from test1 where id=1 or id=2;”或者“select * from test1 where id in (1, 2);”,那么就只要起2个并发task,分别Get Rowkey等于1和Rowkey等于2的数据。
图 Spark并发使用Get获取HBase数据示例
缩小RowKey的查询范围
我们来看一个组合RowKey的例子:
CREATE TABLE test2 (ID STRING, NAME STRING, AGE STRING, VALUE DOUBLE)
USING HBASE OPTIONS (
‘ZKHost’=’xx’,
‘TableName’=’TABLE_IN_CLOUDTABLE’,
‘RowKey’=’ID:2, NAME:4, AGE:2’,
‘Cols’=’VALUE:CF1.C1);
上面这张表,Rowkey由2个字节的ID,4个字节的NAME,2个字节的AGE拼接组成,一共有8个字节。如果需要查询“select * from test2 where id=’01’; ”,又要怎么做呢?
之前我们分析过RowKey是按照字节排序的,0x00是最小的,0xFF是最大的。那么,我们的查询范围其实已经缩小到了从
至
这里面,01后面分别跟上了6个字节的0x00和0xFF代表了4个字节的NAME和2个字节的AGE。所以,在Scan的时候,我们只要设置startKey由‘01’加上6个字节的0x00,endKey由‘01’加上6个字节的0xFF就好,RowFilter都不需要了。
再举个例子,如果需要查询“select * from test2 where id=’01’ or id=’03’; ”或者”select * from test2 where id in (‘01’, ‘03’); ”的时候,就需要起两个并发,分别对startKey设置‘01’加上6个字节的0x00和‘03’加上6个字节的0x00,对endKey设置‘01’加上6个字节的0xFF和‘03’加上6个字节的0xFF就好。
图 缩小Rowkey查询范围后Spark使用Scan获取HBase数据示例
另外,会有很多的特殊情况是需要考虑的,比如”select * from test2 where id=’01’ or name=’abcd’; ”,注意这里是用‘or’过滤条件来连接的。尽管,大多数情况下,这种属于写错的概率很高,但是对计算引擎的开发来讲,还是需要认真对待的。当然,这种情况下,只能进行全表扫描了,因为01abcdxx满足条件,01aaaaxx、02abcdxx、03abcdxx…都满足条件。如果满足id=’01’或者name=’abcd’的数据加在一起还是比较少的话,可以使用上RowFilter。在单RowKey的时候,也会有类似的情况。
最后再来看一个例子,“select * from test2 where id=’01’ and name=’abcd’ and age=’20’; ”。这种情况下,实际只是需要Get Rowkey的值为‘01abcd20’的数据。
于是,有很多适用的场景我们都能迎刃而解了:
值得注意的是,Get的效率远远高于Scan,所以建议能用Get的时候就不要使用Scan了。
基于上面的单Rowkey与组合Rowkey的例子,我们能得到一个一致的结论:对where条件进行分析,判断出Rowkey的查询范围,尽量缩小它,并尽量使用Get。那么,如何对where条件进行分析呢?下面会给出一个方法。
4、深度优化:Rowkey的区间范围分析
4.1String类型与单RowKey的说明
对于String类型来讲,在SparkSQL中是允许做“大于”、“小于”的比较的,它所比较的是字典序,比如’1’ < ‘2’、‘11’ < ‘2’、‘a’ < ‘b’等等。
再来看一个单Rowkey的例子:
CREATE TABLE test1 (ID STRING, NAME STRING)
USING HBASE OPTIONS (
‘ZKHost’=’xx’,
‘TableName’=’TABLE_IN_CLOUDTABLE’,
‘RowKey’=’ID’,
‘Cols’=’NAME:CF1.C1);
这里的ID类型是String。如果遇到查询“select * from test1 where id > ‘01’; ”的时候,是否可以缩小Rowkey的查询范围呢?答案是肯定的。首先我们可以设置startKey为‘01’,虽然id=’01’的结果也会被获取到,但是无论是下压RowFilter还是在Spark中做过滤,都会将id=’01’的结果过滤掉,在大数据的场景下,多处理一条记录对性能的影响是微乎其微的。
再来谈一谈endKey。因为在Scan HBase表的时候可以不用设置Rowkey的startKey和endKey,如果startKey默认为空的话,Scan会从头开始扫描,endKey默认为空的话,Scan会一直扫描到结束,因此上面的例子其实完全可以不用设置endKey。
最后,再讨论一下关于ID是数值型的话,要如何处理。回到2.1中的例子,表test1中的字段ID类型为INT。那对于“id > 1”的条件判断,是需要多加注意的。因为在字节的世界里面,数字的大小是这样的(以单个字节为例):
0 < 1 < 2 < … < 127 < -128 < -127 < … < -2 < -1
所以,“id > 1”的区间范围应该是(注意是Int,4字节):
既“1 < id < Int.MinValue(2147483648)”。
4.2过滤条件列表
在这里,我们来归纳一下能够进行分析Rowkey区间范围的条件。
单RowKey
以3.1中的表test1为例,ID是String类型。
表 String类型的单Rowkey部分过滤条件对应的查询范围
组合RowKey
以3.2的表test2为例,’RowKey’=’ID:2, NAME:4, AGE:2’,ID、NAME、AGE 3个字段都是String类型。
表 String类型组合Rowkey部分过滤条件对应的查询范围
其实除了以上列举的几个基本过滤条件以外,还有一些条件也是可以做区间范围分析的,比如“Not”条件,就是取相反的区间,而‘like’条件语句和前缀匹配(prefix)也是可以做分析的。本文就暂时介绍一些基本的过滤条件,对于其他的过滤条件,大家其实可以举一反三,在这里就不详细说明了。
4.3Spark中的Filter
在Spark SQL中有一个Filter类,是Spark中所有过滤条件的基类,它可以转换成各种不同的条件实例:
基本的实现思路在上面代码的注释中已经说明了,遇到‘or’或者‘and’的表达式,就递归处理,遇到基本条件的时候就按照3.2中的列表设置startKey和endKey。
有了思路之后,具体的实现其实大家都已经了然了,各自都有各自的方式,条条大路通罗马,这里就不再做太详细的描述。
总结
在所有关于RowKey查询优化的场景中,中心思想就是缩小RowKey的查询范围。除此之外还有一些其他的手段,比如Region的数据合理分配、提高并发等等。本文中的优化手段虽然很大一部分是围绕着本文的车联网案例来的,但是优化思路还是可以应用到其他不同的场景中的。除了String加定长的这种数据格式,分隔符或者其他的数据格式都可以考虑这种优化手段。