剪枝的同时别减掉了性能
“随着维度数目的增加,Cuboid 的数量会爆炸式地增长。为了缓解 Cube 的构建压力,Apache Kylin 引入了一系列的高级设置,帮助用户筛选出真正需要的 Cuboid。这些高级设置包括聚合组(Aggregation Group)、联合维度(Joint Dimension)、层级维度(Hierachy Dimension)和必要维度(Mandatory Dimension)等。”
正如上述官方文档提到的,在维度过多时,合理的使用聚合组能解决 Cube 膨胀率过大的问题。听起来那么美好,但是,不合理的聚合组设置将对性能产生灾难性影响。
剪枝原理
Apache Kylin 的主要工作就是为源数据构建 N 个维度的 Cube,实现聚合的预计算。从理论上说,构建 N 个维度的 Cube 就会生成 2^N 个 Cuboid。
所以,只要降低最终 Cuboid 的数量,就可以减小膨胀率,达到对 Cube 剪枝的效果。
构建一个 4 个维度(A,B,C, D)的 Cube,就需要生成 16 个Cuboid。
那么问题来了,如果这 4 个维度(A,B,C, D),能够根据某业务逻辑找出一个隐藏的规律,即:当进行聚合时,用户仅仅关注维度 AB 组合和维度 CD 组合(即只会通过维度 A 和 B 或者 C 和 D 进行聚合,而不会通过 A 和 C、B 和 C、A 和 D、B 和 D 进行聚合),那么就可以通过设置聚合组,使生成的 Cuboid 数目从 16 个缩减成 8 个(大大降低 Cube 膨胀率),如图 2 所示。
上面这段内容来自 Kylin 公众号的 【技术帖】Apache Kylin 高级设置:聚合组(Aggregation Group)原理解析 这篇文章中,值得对聚合组还不太了解的同学读一读。
但是,这里好像完全没有提到用于过滤数据(而不是聚合)的维度字段,应该怎么处理?
问题产生
某年某月某日,某业务人员突然发现某张报表的打开速度极其缓慢,并上报给系统管理人员。随后,通过对该报表产生的 SQL 进行筛查,发现了如下一条嫌疑重大的 SQL 语句,拖慢了整个报表的打开速度。
select "A","B",sum("VALUE")
from test_agg_group
where "D" = 1
group by 1,2;
Kylin 日志信息:
==========================[QUERY]===============================
Query Id: 7fe300c2-211c-9429-eebf-b4cc57bfd679
SQL: select "A","B",sum("VALUE")
from test_agg_group
where "D" = 1
group by 1,2;
User: ADMIN
Success: true
Duration: 4.891
Project: 0000_reserved
Realization Names: [CUBE[name=test_agg_group]]
Cuboid Ids: [15]
Total scan count: 1000000
Total scan bytes: 51000000
Result row count: 100000
Accept Partial: true
Is Partial Result: false
Hit Exception Cache: false
Storage cache used: false
Is Query Push-Down: false
Is Prepare: false
Trace URL: null
Message: null
==========================[QUERY]===============================
因为这是在测试环境(数据量不大)执行的 SQL,所以执行时间为 4.891 秒,生产环境真实的 SQL 执行时间已超过 40 秒,Total scan count 为千万级。但是问题出现的原理和线上是一样的。
问题定位
对于这种极慢的 SQL,我通常会观察日志信息中的 Total scan count 与 Result row count 数值差异是否巨大。
如果差异极大(例如上述 SQL 的差异已经达到 10 倍),那就意味着该条 SQL 扫描了很多不会被作为最终结果的无用数据。
此时我发现只要删掉那个 where 条件就可以很快的得到响应:
select "A","B",sum("VALUE")
from test_agg_group
group by 1,2
Kylin 日志信息:
==========================[QUERY]===============================
Query Id: 2a9d7422-7268-2805-f1ac-a0fc544602c9
SQL: select "A","B",sum("VALUE")
from test_agg_group
group by 1,2
User: ADMIN
Success: true
Duration: 0.628
Project: 0000_reserved
Realization Names: [CUBE[name=test_agg_group]]
Cuboid Ids: [12]
Total scan count: 100000
Total scan bytes: 4900000
Result row count: 100000
Accept Partial: true
Is Partial Result: false
Hit Exception Cache: false
Storage cache used: false
Is Query Push-Down: false
Is Prepare: false
Trace URL: null
Message: null
==========================[QUERY]===============================
很明显,相比原 SQL,查询的响应时间就提升了好几个数量级。值得注意的是,Total scan count 也从原来的 100w 降到了 10w。
如果是一个传统 RDBMS 的 DBA 看到这一幕,一定会感到疑惑,添加了 where 条件的 SQL 扫描的行数竟然比没有 where 条件的 SQL 扫描的行数更多,简直不可思议。
问题根源
看到这里,有人可能已经逐渐忘记了标题。
回到这个 Cube 上看一看,它教科书般的使用了聚合组进行剪枝操作,完美的将 AB 和 CD 分到了两个聚合组中,将膨胀率降低了一半。
因此,当我们以 AB 维度进行聚合 D 维度进行过滤,Kylin 在搜索哪些行满足 D=1 这个条件时,就无法通过图 3 的方式进行搜索了。
因为不会有任何一个 Cuboid(大约 10w 行)像上面这样包含 ABD 三个维度和预计算好的值。所以最终 Kylin 会扫描下面这个 Cuboid (即包含 ABCD 四个字段的 Cuboid,大约有 100w 行)来获取最终数据。
这是一个在聚合组设置不当 且 运气还很差的情况下才能触发的问题。
运气差在哪?
- C 字段的基数非常大
- D 字段的基数非常小
通过查看 SQL 执行的日志信息我们也能看到。当以 D 字段为过滤条件时,只能使用包含 ABCD 四个字段的 Cuboid 进行扫描。
但是 C 字段的基数非常大,所以该 Cuboid 的行数也就非常多。同时, C 字段并没有进行筛选,使用了基数非常小的 D 字段进行了筛选(一共 1000w 行,D字段有 500w 行是 1,500w 行是 2)。
最终导致要扫描完 Cuboid ABCD 的 100w 行才能得到计算结果。
那么如果筛选字段不是 D 而是 C,我们尝试下估算下需要扫描多少行呢?
select "A","B",sum("VALUE")
from test_agg_group
where "C" = 100000
group by 1,2
Kylin 日志信息:
==========================[QUERY]===============================
Query Id: e304ae37-f7ec-233b-d353-845e2feba908
SQL: select "A","B",sum("VALUE")
from test_agg_group
where "C" = 100000
group by 1,2
User: ADMIN
Success: true
Duration: 0.806
Project: 0000_reserved
Realization Names: [CUBE[name=test_agg_group]]
Cuboid Ids: [15]
Total scan count: 2
Total scan bytes: 102
Result row count: 2
Accept Partial: true
Is Partial Result: false
Hit Exception Cache: false
Storage cache used: false
Is Query Push-Down: false
Is Prepare: false
Trace URL: null
Message: null
==========================[QUERY]===============================
仅需要扫描个位数的行即可,因为 C 字段基数大,包含的重复值很少。而且我们可以看到,这条 SQL 和最初的 SQL 都是用了 Cuboid Id 为 15 的 Cuboid 进行查询,也就是包含了 ABCD 四个字段的 Cuboid。
而仅用了 AB 两个字段,不使用 CD 中任何一个字段进行筛选的 SQL 使用了 Cuboid Id 为 12 的 Cuboid。
总结
分聚合组时,哪怕用户仅仅关注维度 AB 组合和维度 CD 组合,但用户会可能用 D 作为过滤条件来查询 AB 组合,就一定要保证 ABD 要分到同一个聚合组当中。
当然了,如果字段的基数不像例子中这么极端,聚合组随便怎么分对性能影响应该都不大。但是,如果哪天墨菲定律突然上线,希望大家能想起本文。