Kylin实战(四):rowkey调优

普遍情况下,Kylin以HBase作为存储引擎,因此HBase的查询效率对于Kylin的查询响应时间尤为关键,而rowkey作为HBase最重要的设计自然是优化的重中之重。合适的rowkey设计不仅可以节省cube存储空间和降低膨胀率,而且能减小查询扫描的范围并提高聚合计算速度从而加速查询。为此,Kylin在”Advanced Setting”页面提供了rowkey的配置供用户调整,具体可以分为维度的编码方式、维度的顺序和分片维度设置三类。

维度的编码方式

众所周知,HBase以字节数组的形式存储数据,编码方式需要应用负责。Kylin可以在维度级别设置编码方式,包括dict、fixed_length、fixed_length_hex、integer等(编码方式在不同版本有所差,本文基于1.6.0版本),供用户针对不同维度的数据类型、基数、长度等特性进行调优。下面按照编码方式的通常使用频率逐一分析。

  1. dict编码
    Dict编码是最为常见的编码方式,它将维度的唯一值提取出来在内存中构成字典,并将在rowkey中对应维度的替换为字段的key。这种方式的优点是存储较为紧凑,减少了维度值的冗余,代价是每次查询相关维度都需要查一遍字典以获取维度的值,是时间换空间的做法,但由于字典常驻内所以存检索很快,对查询时间并没有太大的影响。另外,字典保存在内存里带来的另外一个限制是字典不能超出缓存大小,否则会在cube build期间发生缓存溢出异常。官方默认的dict编码字段基数在5,000,000以内,虽然具体上限可以通过kylin.dictionary.max.cardinality来修改,但比起调整基数上限,更好的做法是换成其他类型的编码。综上不难看出dict编码适用于维度基数较小、重复出现较多的列,比如地区信息(国家、省份、城市)。

  2. fixed_length编码
    对于并不适用于dict编码的高基数维度列,比如USER_ID、IP之类的维度,fixed_length编码会是个不错的选择。顾名思义,fixed_length编码以固定长度来存储维度值,需要预设一个长度表示字段的最大字节数,超过该长度的部分会被截断。但要注意对于像COMMENT、TWEET这种特别长的字段,设置一个比较大的N会造成rowkey过长,导致性能下降。如果不能截断这些字段,推荐做法是将其存为raw形式的指标。

  3. integer编码
    不少维度列实际上是整数的形式,比如之前的提到的USER_ID就很有可能是整型或者长整型,如果使用fix_length编码则非数字的编码空间会被白白浪费,为此Kylin还提供了integer编码来存储数字类型的数据。Integer编码支持长度为1-8个字节,也就是[-2^(8N-1), 2^(8N-1)],一般来说存储整型数值将长度设为4字节就可以。

  4. fixed_length_hex编码
    这种编码特别为十六进制的字段设计,用于存储byte数组很自然,适合Base64转化后的字段。

  5. boolean编码
    用一个byte表示布尔值,适用于字段值为: (true/false, TRUE/FALSE, True/False, t/f, T/F, yes/no, YES/NO, Yes/No, y/n, Y/N, 1/0)。

维度的顺序

HBase是以rowkey顺序存储行的,这意味着在rowkey中越靠前的维度,相近的值越为集中(需要查询更少的Region),从而越容易检索。因此,在查询条件中出现频率越高的维度应该越靠前。

另一方面,rowkey是HBase的聚集索引,对于索引来说区分度是个重要的指标,我们不希望走完索引后发现还有大量的行需要遍历,这反应到索引列上就是基数越大越好。因此,基数大的列放在rowkey靠前的位置也是个推荐的做法。这样做还有一个不可忽视的好处是减小计算成本:cube构建阶段或查询没有命中物化的cuboid时,Kylin需要选择一个父cuboid来进行聚合得出子cuboid。而父cuboid的选择算法是选出cuboid id最小的一个,这意味着排在rowkey较后的维度列会优先被选中做上卷操作。如果基数小的维度在后,那么上卷的计算量会相应地减小。

当这两个优化方向出现冲突时,比如A维度基数大但查询几率小,B维度基数小但查询几率大,这时就要结合HBase的物理存储来分析了。我的经验是看region会在第几个维度上出现区分度(可以在HBase的管理里面看到Region的startkey和endkey),在该维度以后的维度顺序并不太重要,因为根据它们去检索数据时都需要扫描全部的region,所以只需要考虑基于维度基数的规则二。否则应该更倾向于用基于查询频率的规则一,因为查询的效率比离线cube build的效率更为重要,而查询没有命中cuboid的情况也属于少数。

分片维度

Cube支持在构建阶段进行分片以提高效率,可以并只可以将一个维度设为分片(shard by)维度(注意分片维度和cube的partition维度不同,前者作用于构建阶段,后者是用于分割时间段以支持增量构建)。Cube build第二步需要将中间表的数据重新分布到HDFS各个节点上,默认的partition方式是随机,如果指定了分片维度,则改为使用分片维度进行partition。重新分布数据的意义在于防止中间表文件大小相差太大造成数据倾斜,因此分片维度应该是高基数列以保证分片的粒度足够小,可以较为均匀地分布到各个节点上,这能大大加速之后的MapReduce任务。

写在最后的话

Rowkey设计向来是HBase应用最为重要的一点。Kylin作为对于HBase依赖较大的系统,在框架内提供了从rowkey编码、字段顺序和输入数据分布三大方向的配置项,基本满足了常见的HBase调优需求。 然后因为在rowkey中出现的字段是由cube设计决定,rowkey的字段必须和cube设计的维度一致,导致指标其实是没有办法作为rowkey的一部分的(除非将指标列同时设为维度,但在2.0以上版本不支持这样做)。一般来说指标出现在rowkey的确不符合OLAP的理念,但是如果有需求是根据指标做过滤,Kylin目前的rowkey设计是满足不了的。这个问题在涉及原始指标的过滤时会被放大,比如计算月销售量大于100的员工的销售额总和,对应的SQL为SELECT SUM(sales_amount) FROM tbl_sales WHERE sales_amount > 100,其实是需要扫描所有base cuboid来满足的。所以如果可以让用户选择某些指标放入rowkey,并在聚合组设置中禁止加入指标列(相当于只用于过滤不用于聚合),或许是个不错的方法。

参考文献

[1]Apache Kylin核心团队.Kylin权威指南[M].机械工业出版社, 2017.
[2]Kylin Documentation: howto_optimize_cubes, Apache.
[3]Kyligence知乎专栏: Apache Kylin 优化利器KyBot: Rowkey一键优化, Zhihu.

本文是原创文章,转载请注明:时间与精神的小屋 - Kylin实战(四):rowkey调优