Kylin cuboid算法修改

来源:互联网 发布:ab post json请求 编辑:程序博客网 时间:2024/06/05 15:52

缘由

    近期由于发现线上cube的构建时间太慢(一个项目的cube构建前一天的数据一般需要170分钟左右),目前我们接入的应用才三个,如果后期接入更多的cube之后会导致更慢的cube构建速度,于是深入了解了一下cuboid是如何确定的,看了代码之后发现和我们预想的不一样,于是经过咨询社区之后也觉得之前的算法是存在一定的问题(2.x版本已经对此做了修改),因此就准备对cuboid的计算进行修改。

Kylin原有cuboid算法

    了解了kylin中如何对cube进行优化(参见OLAP引擎——Kylin介绍和Kylin使用之创建Cube和高级设置)之后,下面来看一下kylin是确定哪些cuboid需要计算,哪些是不需要计算的呢?

    在Kylin中保存cube的时候需要对cuboid进行校验,通过三种方案计算出生成cuboid的数量,然后对比三种方法生成的数量是否相同,如果不相同这说明cube的定义存在问题(例如一个hierarchy组的不如维度分布在不同的group中)。在Kylin中,使用位图来计算cuboid,每一个维度在位图上使用一个位置(所以维度超过64就会出现问题),每一个cuboid的值是一个long值,它的二进制中为0的位置表示对应的维度不出现在这个cuboid中,而为1的表示该cuboid是这些维度的组合。保存cube的时候会通过三种方式计算可能产生的cuboid个数,根据个数是否相同来判断cube的定义是否正确:

  • 根据树的根节点(base cuboid)计算它的spanning cuboid然后再一次计算每一个spanning cuboid的spanning cuboid,并将它们加入到set中,但是需要保证每一个cuboid的spanning cuboid是不相同的,因为如果重复可能会导致cuboid被重复计算,每一个cuboid在计算spanning cuboid是从它的child cuboid中过滤掉它的兄弟cuboid的child cuboid,这样就能够保证每一个cuboid的spanning是唯一的。
  • 从0开始,递增直到base cuboid(位图上所有维度对应的位置都为1),依次递增,对每一个cuboid判断它是否需要被计算,然后统计所有需要计算的cuboid个数。这种方法可能随着维度数的增加变得性能很差,试想32个维度的cube需要2的32次方的遍历。
  • 通过数学的方法计算需要计算的cuboid个数。

    首先看一下kylin中如何验证一个cuboid是否需要计算(第二种计算方案利用了这种方式判断满足分组的cuboid的计数):

public static boolean isValid(CubeDesc cube, long cuboidID) {    RowKeyDesc rowkey = cube.getRowkey();    if (cuboidID < 0) {        throw new IllegalArgumentException("Cuboid " + cuboidID + " should be greater than 0");    }    if (checkBaseCuboid(rowkey, cuboidID)) {        return true;    }    if (checkMandatoryColumns(rowkey, cuboidID) == false) {        return false;    }    if (checkAggregationGroup(rowkey, cuboidID) == false) {        return false;    }    if (checkHierarchy(rowkey, cuboidID) == false) {        return false;    }    return true;}

可以看出kylin会检查一个cuboid多个属性,按照如下步骤:

  • 查看是否大于0,由于使用位图,所以所有的cuboid都必须是大于0的值
  • 查看是否base cuboid
  • 查看它是否包含所有mandatory维度,并且除去mandatory维度之外还必须包含至少一个其它维度,也就是除去mandatory之后为0的cuboid不需要预计算
  • 查看这个cuboid是否符合分组的定义
  • 查看这个cuboid是否符合hierarchy维度组的定义。

    之所以没有检查derived维度组,是因为derived维度组并不是约束cuboid的,而使用的是替换的方式将一个维度表中的derived维度替换成使用主键的维度。

重点来看一下检查是否满足分组约束的,其他的检查都是比较简单的:

private static boolean checkAggregationGroup(RowKeyDesc rowkey, long cuboidID) {    long cuboidWithoutMandatory = cuboidID & ~rowkey.getMandatoryColumnMask();    long leftover;    for (AggrGroupMask mask : rowkey.getAggrGroupMasks()) {        if ((cuboidWithoutMandatory & mask.uniqueMask) != 0) {            leftover = cuboidWithoutMandatory & ~mask.groupMask;            return leftover == 0 || leftover == mask.leftoverMask;        }    }    leftover = cuboidWithoutMandatory & rowkey.getTailMask();    return leftover == 0 || leftover == rowkey.getTailMask();}

从这段代码看出,每一个维度组保存了三个mask信息:

  • groupMask:每一个组中所有的维度对应的位置都置为1的值。
  • uniqueMask:只包含在本组而不包含在后面所有组的那些维度对应位置都置为1的值。
  • leftoverMask:不包含在本组中,但是包含在后面其余组中的所有维度对应的位置都置为1的值。

除此之外,leftover表示不包含在所有的mask同时也不是mandatory维度的那些维度。

   在检查一个cuboid是否符合组约束的时候首先去除了mandatory维度,然后检查每一个分组,如果该cuboid中有一个维度是某一个group独有的(包含在uniqueMask中),那么说明只需要在该组中检查就可以了,此时判断这个cuboid再去除所有该组的维度之后是否不包含任何维度(说明除去mandatory维度以外不包含任何该组外的维度了)或者它还包含了不在本组但是在后面所有组的所有的维度。最后,如果它不包含在任何组中,那么只需要查看它是否等于leftover就可以了。从这里可以推断出,所有的cuboid包含这两部分:1、每一个组内成员的任意组合(全部成员都包含在一个组里面),2、只有部分维度包含在一个组里面,其余的维度等于这个组的leftoverMask。

优化cuboid算法

   但是这种计算cuboid的策略和我们上面分析的不一致,并且这种算法总是考虑每一个组的leftoverMask,所以会导致两个问题:1、分组的顺序影响计算的cuboid,2、分组的时候需要考虑到每一个组的leftover有哪些维度,不容易和查看进行匹配。总体来讲,这是一个较为复杂的逻辑,这会导致我们不能根据可能查询的SQL轻易地推断出如何进行分组,因此我们考虑简化这部分逻辑,目标只有一个:减小cuboid计算量,不再计算第二部分cuboid。
修改之后的isValid函数保持相同的逻辑,而checkAggregationGroup如下:

private static boolean checkAggregationGroup(RowKeyDesc rowkey, long cuboidID) {    long cuboidWithoutMandatory = cuboidID & ~rowkey.getMandatoryColumnMask();    long leftover;    for (AggrGroupMask mask : rowkey.getAggrGroupMasks()) {        //all in one mask group        leftover = cuboidWithoutMandatory & ~mask.groupMask;        if (leftover == 0)            return true;    }    return false;}

修改之后的组规则只会考虑这个cuboid是否属于某一个分组,这样就边的清晰明了了。

除了这里的修改之外,还有比较重要的修改在于计算每一个cuboid的spanning cuboid,目前采取的策略如下:

  • 如果是base cuboide,那么它的spanning cuboid就是所有组的groupMask。
  • 查看该cuboid所有小于它的sibling cuboid(1的个数相同,但是位置不同),并将每一个sibing cuboid的child cuboid加入到一个set中。
  • 查看该cuboid的child cuboid,如果在2中计算的set中存在则不作为spanning cuboid,否则加入到结果集中。
  • 在计算cuboid的child cuboid的时候会遍历所有组,如果属于某个组则从这个cuboid中去掉该组中的某个维度作为它的child cuboid。

    通过数学方法计算cuboid数量也需要相应的修改,根据我们优化之后的cuboid计算方法,这个计算可以演化成在多个分组中如果计算出不同的组合个数,例如[1001101, 10001100, 00101101]这三个mask,如果计算包含在其中一个或者多个mask的数的个数,最简单的计算就是分别计算出每一个mask可能的组合数,然后三个masj的组合数相加,在减去他们之间的交集,只不过这里面的交集还有交集,所以需要递归的进行,代码如下:

private static int mathCalculateGroupCount(RowKeyDesc rowkey, long[] groups) {    int sum = 0;    for(int i = 0 ; i < groups.length ; ++ i) {        sum += mathCount(rowkey, groups, i, groups.length - 1);    }    return sum;}private static int mathCount(RowKeyDesc rowkey, long[] groups, int cur, int end) {    long current = groups[cur];    if(current == 0)        return 0;    //ignore all 0 cuboid    int count = mathCalcCuboidCount_combination(rowkey, current) - 1;    long[] next = new long[end - cur];    int index = 0;    for(int i = cur + 1 ; i <= end ; ++ i) {        long com = current & groups[i];        next[index++] = com;    }    return count - mathCalculateGroupCount(rowkey, next);}

但是cuboid的计算主要是在计算cube的时候使用的,所以需要保证如下几点:

  • 新算法计算出的cuboid需要是之前算法计算出的cuboid的子集,如果不能满足,则可能出现查询定位到新算法计算出的cuboid而实际上老数据并没有计算这个cuboid导致返回数据为空,通过分析可以看出新的算法只是在组规则中加强了约束条件。
  • 通过新老算法计算出的cuboid在merge的时候会不会出现问题,由于1的保证,在merge的时候会导致merge之后的数据包含在新算法的cuboid包含了新老数据,而其他的cuboid都只包含老数据,所以需要保证查询的时候不会查到老的cuboid,这点是通过cuboid为匹配的情况下往树的上层查找的过程中只会查找已计算的cuboid保证,因此最终还是会查找到新算法计算出的cuboid,就不会导致查询数据为空了。

这种优化带来的好处:

  • 缩短每次build的时间,每层需要计算的key变少了,同时下一层的输入也变小了。
  • 减小hbase中存储cuboid的空间。
  • 对老数据没有任何影响。

缺点:

  • 如果没有某一次查询不能命中到某一个分组,需要从base cuboid中扫描,可能导致更大的扫描范围,性能降低。
  • 代码修改可能会带来未知的bug。

总结

    总体来说,本次对cuboid算法的修改是具有可行性的,但是相对比较冒进的,如果查询的SQL大部分情况下都是确定的,那么这样的修改带来的好处远大于它所带来的查询的影响,目前修改之后运行比较稳定。

0 0
原创粉丝点击