HBase行健的设计
在设计HBase表的时候,行健是唯一重要的事情。应该基于预期的访问模式来为行健进行建模
行健决定了访问HBase表时可以得到的性能。这个结论根植于两个事实:
1、region基于行健为一个区间的行提供服务,并且负责区间内的每一行。
2、HFile在硬盘上存储有序的行。
当region刷写留在内存中的行时生成了HFile,此时这些行已经经过排序了,也会有序的刷写到硬盘上。HBase表的有序特性和底层的存储格式也可以让我们根据如何设计行健以及吧什么放入列限定符来推理性能表现。
对于关系型数据库来说,我们可以在多个列上建立索引,但是HBase只能在键上建立索引,访问的唯一办法就是使用行健。如果不知道要访问的行健,则就需要扫描若干行,即使不是整个表。对于行健的设计,可以针对不同的访问模式进行优化。
IO考虑
HBase表的有序特性虽然是能够让我们在最短的时间内扫描一小组行,但是当我们往表中写一堆时间序列的数据时,同样的有序特性会带来负面的影响(比如说热点问题)。
例如,在使用时间戳做行健的时候,因为对于时间戳来说,它是天然单调递增的,所以说在写入的时候,总是写入到负责这个时间戳范围的一个region上。这样的话不仅使整个集群受限于单个region能够处理的吞吐量,而且还会承担由于单个机器过载而同时集群里其他机器限制的风险。所以针对特定的访问模式来做指定的优化。
为写优化
当往HBase表写入大量的数据的时候,我们总是希望能够在RegionServer上分散负载来进行优化。要实现这种方式,并不难,只需要将写入数据的行健进行稍作处理,避免使用连续的行健(例如使用时间戳,在写入的时候会写入到单个的region上,造成热点问题),但是这样的话,我们可能就会在读模式下做出点牺牲,因为对rowkey做了处理,所以数据都分布在不同的region上,所以如果需要使用scan扫描一组有序的数据的时候,可能就不是那么理想了。
但是,在很多场景下,我们并不需要基于单个时间戳访问数据,可能我们需要运行一个作业在一个"条件区间上"来做聚合计算,如果说对时间延迟不是特敏感的,可以考虑跨多个region并行扫描来完成任务,例如下面这样的代码:
val hisData = isContainsHisData.mapPartitions(iter => { val hbaseConf = HBaseConfiguration.create val con = ConnectionFactory.createConnection(hbaseConf) val tn = TableName.valueOf(BASIC_DATA_TABLE_NAME) val table = con.getTable(tn) val scan = new Scan() scan.setCaching(500) iter.map(x => { conditions1 = (mD5Util.md5Encode(x._1._1 + x._1._2, 16) + "#" + x._2).replace("-", "") conditions2 = (mD5Util.md5Encode(x._1._1 + x._1._2, 16) + "#" + String.valueOf(LocalDate.parse(x._2).plusDays(-1))).replace("-", "") scan.setStartRow(Bytes.toBytes(conditions2 + "#0")) scan.setStopRow(Bytes.toBytes(conditions1 + "#9~")) val tmpR = table.getScanner(scan).iterator() val resArr = new ArrayBuffer[Result]() while (tmpR.hasNext) { resArr += tmpR.next() } resArr }).flatMap(x => x) })
但是具体如何对行间进行处理,才能让数据分散到不同的region上呢?有如下几项可以考虑:
-
散列
如果我们在行健中不假如时间戳信息的话,那么使用原始拼接数据的散列值作为行健是一种解决方案,如上面代码所示,我一般会对我拼接的rowkey做一个16位的md5(所谓的16位MD5值只是取其32位md5值的8-24位值)。这样的话,每当我们访问这个散列值为行健的数据的时候,就需要精确的知道拼接的rowkey。
但是对于时间序列而言的话,一般不会这样处理,你也不大可能能够记得精确的时间戳。
对于计算散列值的方法,常用的有MD5、SHA-1或其他提供的随机分布的散列算法函数
碰撞 散列算法有一个非零碰撞概率。有些算法比其他算法高,当用于大型数据集是需要小心,要尽量使用低碰撞概率的散列算法。例如,在这方面的话,SHA-1要由于MD5,某些情况下,SHA-1可能是个更好的选择,即使性能上有些许。
-
slating
在思考行健的构成的时候,salting是另一种技巧。
假设你在读取的时候之后时间范围,但是不想做全表扫描(一般情况下,我们都不会对HBase表做全表扫描,除非不得已的情况下,否则千万别),对时间戳做散列运算,然后把散列值作为行健的做法需要全表扫描,这是非常低效的。尤其是你有办法限制扫描范围的时候。使用散列值作为行健在这里不是办法,但是你可以在时间戳前面加上一个随机前缀。
例如,可以先计算时间戳的散列码然后用RegionServer的数量取模来生成随机salt数:
int salt = new Integer(md5(new Long(timestamp))).shorValue() % <number of region server>
取得salt之后,加到时间戳的前面生成行健,像这样:
byte[] rowkey = Bytest.toBytest(salt+"|"+timestamp)
现在行健如下所示:
0|timestamp1
0|timestamp5
0|timestamp6
1|timestamp2
1|timestamp9
2|timestamp4
2|timestamp8
此时,这些行会基于键的第一部分分部在不同的region上,也就是说salt相同的会分部在同一个region上,除非发生region的拆分。
不过先在,如果要读取的话,那就需要将扫描分散在不同的region上来查找相应的行。因为它们不在存储在一起,所以,此时一个短的扫描不能够解决问题。故这是一种权衡
为读优化
对于HBase的读取,要充分利用行健的特性,在获取数据之前务必想清楚,获取数据的方式,常用的获取数据的方式有三种:scan、get、批量get
- scan:在开发过程中应该尽量避免全表scan,使用的过程中务必设置startRow和stopRow。
行健:md5(aaa)#20190102#001
md5(aaa)#20190103#101
md5(aaa)#20190104#221
md5(aaa)#20190105#431
...
如上rowkey,如果需要获取aaa对应的20190103之后的所有数据,正确的做法应该是:
scan.setStartRow(Bytes.toBytes(md5(aaa)+"#20190103"+"#0"))
scan.setStopRow(Bytes.toBytes(md5(aaa)+"#9~"))
table.getScanner(scan)
错误的做法通常都是,不对scan进行设置startRow与stopRpw,全表扫描,然后在使用过滤器,这种做法是非常的低效的。
- get:如果说我们在获取数据之前明确的知道对应的rowkey,则可以使用get的方式,这种方式简单,高效,但是这种方式对于获取单条记录简单,如果我们要获取1000个rowkey的数据的,难道我们要get1000次么?当然不是,这个时候就需要用到下面的方式。
- List<get>:批量get。该种方式在明确rowkey并且在需要获取的rowkey较多的时候非常适用。
List<Get> gets = new ArrayList<Get>(20); Get get = null; for (String rowkey : rowkeyMaps.keySet()) { get = new Get(Bytes.toBytes(rowkey); gets.add(get); } Result[] reslut = table.get(gets);
基数和行健结构
行健结构至关重要。有效的行健设计不仅要考虑把什么放入到行健中,而且要考虑它们在行健中的位置。
假设现在系统有种三个用户,分别是:TheRealMT、TheFakeMT和Olivia,现在考虑时间区间1-10之内倒序时间戳。如果说把用户ID放在第一部分,行健如下所示(按照它们在HBase表里的存储顺序):
Olivia1
Olivia2
Olivia5
Olivia7
Olivia9
TheFakeMT2
TheFakeMT3
TheFakeMT4
TheFakeMT5
TheFakeMT6
TheRealMT1
TheRealMT2
TheRealMT5
TheRealMT8
但是,如果调换键的顺序,把倒序时间戳放在第一部分,行健排序变为:
1Olivia
2TheFakeMT
2Olivia
2TheRealMT
3TheFakeMT
4TheFakeMT
5Olivia
5TheFakeMT
5TheRealMT
6TheFakeMT
7Olivia
8TheRealMT
9Olivia
因为不能在指定用户ID作为扫描键的起始键,因此要想获取指定用户的最新数据,都需要扫描整个时间范围。
所以说,在设计行健的时候,行健里的位置和选择放入什么信息同等重要