编者按:本文节选自华章科技大数据技术丛书 《Apache Kylin 权威指南(第 2 版)》一书中的部分章节。
Cuboid 剪枝优化
维度的组合
由之前的章节可以知道,在没有采取任何优化措施的情况下,Kylin 会对每一种维度的组合进行聚合预计算,维度的一种排列组合的预计算结果称为一个 Cuboid。如果有 4 个维度,结合简单的数学知识可知,总共会有 24=16 种维度组合,即最终会有 24=16 个 Cuboid 需要计算,如图 1 所示。其中,最底端的包含所有维度的 Cuboid 称为 Base Cuboid,它是生成其他 Cuboid 的基础。
图 1 四维 Cube
在现实应用中,用户的维度数量一般远远大于 4 个。假设用户有 10 个维度,那么没做任何优化的 Cube 总共会存在 210=1024 个 Cuboid,而如果用户有 20 个维度,那么 Cube 中总共会存在 220=1048576 个 Cuboid!虽然每个 Cuboid 的大小存在很大差异,但是仅 Cuboid 的数量就足以让人意识到这样的 Cube 对构建引擎、存储引擎来说会形成巨大的压力。因此,在构建维度数量较多的 Cube 时,尤其要注意进行 Cube 的剪枝优化。
检查 Cuboid 数量
Apache Kylin 提供了一种简单的工具供用户检查 Cube 中哪些 Cuboid 最终被预计算了,将其称为被物化(materialized)的 Cuboid。同时,这种工具还能给出每个 Cuboid 所占空间的估计值。该工具需要在 Cube 构建任务对数据进行一定的处理之后才能估算 Cuboid 的大小,具体来说,就是在构建任务完成“Save Cuboid Statistics”这一步骤后才可以使用该工具。
由于同一个 Cube 的不同 Segment 之间仅是输入数据不同,模型信息和优化策略都是共享的,所以不同的 Segment 中被物化的 Cuboid 是相同的。因此,只要 Cube 中至少有一个 Segment 完成了“Save Cuboid Statistics”这一步骤的构建,那么就能使用如下的命令行工具去检查这个 Cube 中的 Cuboid 的物化状态:
该命令的输出如图 2 所示。
图 2 CubeStatsReader 的输出
在该命令的输出中,会依次打印出每个 Segment 的分析结果,不同 Segment 的分析结果基本趋同。在上面的例子中 Cube 只有一个 Segment,因此只有一份分析结果。对于该结果,自上而下来看,首先能看到 Segment 的一些整体信息,如估计 Cuboid 大小的精度(hll precision)、Cuboid 的总数、Segment 的总行数估计、Segment 的大小估计等。
Segment 的大小估算是构建引擎自身用来指导后续子步骤的,如决定 mapper 和 reducer 数量、数据分片数量等的依据,虽然有的时候对 Cuboid 的大小的估计存在误差(因为存储引擎对最后的 Cube 数据进行了编码或压缩,所以无法精确预估数据大小),但是整体来说,对于不同 Cuboid 的大小估计可以给出一个比较直观的判断。由于没有编码或压缩时的不确定性因素,因此 Segment 中的行数估计会比大小估计来得更加精确一些。
在分析结果的下半部分可以看到,所有的 Cuboid 及其分析结果以树状的形式打印了出来。在这棵树中,每个节点代表一个 Cuboid,每个 Cuboid 的 ID 都由一连串 1 或 0 的数字组成,数字串的长度等于有效维度的数量,从左到右的每个数字依次代表 Cube 的 Rowkeys 设置中的各个维度。如果数字为 0,则代表这个 Cuboid 中不存在相应的维度,如果数字为 1,则代表这个 Cuboid 中存在相应的维度。
除了最顶端的 Cuboid 之外,每个 Cuboid 都有一个父 Cuboid,且都比父 Cuboid 少了一个“1”。其意义是这个 Cuboid 是由它的父节点减少一个维度聚合得来的(上卷,即 roll up 操作)。最顶端的 Cuboid 称为 Base Cuboid,它直接由源数据计算而来。Base Cuboid 中包含了所有的维度,因此它的数字串中所有的数字均为 1。
每行 Cuboid 的输出除了 0 和 1 的数字串以外,后面还有每个 Cuboid 的具体信息,包括该 Cuboid 行数的估计值、该 Cuboid 大小的估计值,以及该 Cuboid 的行数与其父节点的对比(Shrink)。所有的 Cuboid 的行数的估计值之和应该等于 Segment 的行数估计值。同理,所有的 Cuboid 的大小估计值之和等于该 Segment 的大小估计值。
每个 Cuboid 都是在它的父节点的基础上进一步聚合产生的,因此理论上来说每个 Cuboid 无论是行数还是大小都应该小于它的父 Cuboid。但是,由于这些数值都是估计值,因此偶尔能够看到有些 Cuboid 的行数反而还超过其父节点、Shrink 值大于 100%的情况。在这棵“树”中,可以观察每个节点的 Shrink 值,如果该值接近 100%,说明这个 Cuboid 虽然比它的父 Cuboid 少了一个维度,但是并没有比它的父 Cuboid 少很多行数据。换言之,即使没有这个 Cuboid,在查询时使用它的父 Cuboid,也不会花费太大的代价。
关于这方面的详细内容将在后续 3.1.4 节中详细展开。
检查 Cube 大小
还有一种更为简单的方法可以帮助我们判断 Cube 是否已经足够优化。在 Web GUI 的“Model”页面中选择一个 READY 状态的 Cube,当把光标移到该 Cube 的“Cube Size”列时,Web GUI 会提示 Cube 的源数据大小,以及当前 Cube 的大小与源数据大小的比例,称之为膨胀率(Expansion Rate),如图 3 所示。
图 3 查看 Cube 的膨胀率
一般来说,Cube 的膨胀率应该为 0%~1000%,如果一个 Cube 的膨胀率超过 1000%,Cube 管理员应当开始挖掘其中的原因。通常,膨胀率高有以下几个方面的原因:
Cube 中的维度数量较多,且没有进行很好的 Cuboid 剪枝优化,导致 Cuboid 数量极多;
Cube 中存在较高基数的维度,导致包含这类维度的每一个 Cuboid 占用的空间都很大,这些 Cuboid 累积造成整体 Cube 体积过大;
存在比较占用空间的度量,如 Count Distinct 这样的度量需要在 Cuboid 的每一行中都保存一个较大的寄存器,最坏的情况会导致 Cuboid 中每一行都有数十千字节,从而造成整个 Cube 的体积过大;
……
因此,遇到 Cube 的膨胀率居高不下的情况,管理员需要结合实际数据进行分析,可灵活地运用本章接下来介绍的优化方法对 Cube 进行优化。
空间与时间的平衡
理论上所有能用 Cuboid 处理的查询请求,都可以使用 Base Cuboid 来处理,就好像所有能用 Base Cuboid 处理的查询请求都能够通过直接读取源数据的方式来处理一样。但是 Kylin 之所以在 Cube 中物化这么多的 Cuboid,就是因为不同的 Cuboid 有各自擅长的查询场景。
面对一个特定的查询,使用精确匹配的 Cuboid 就好像是走了一条捷径,能帮助 Kylin 最快地返回查询结果,因为这个精确匹配的 Cuboid 已经为此查询做了最大程度的预先聚合,查询引擎只需要做很少的运行时聚合就能返回结果。每个 Cuboid 在技术上代表着一种维度的排列组合,在业务上代表着一种查询的样式;为每种查询样式都做好精确匹配是理想状态,但那会导致很高的膨胀率,进而导致很长的构建时间。所以在实际的 Cube 设计中,我们会考虑牺牲一部分查询样式的精确匹配,让它们使用不是完全精确匹配的 Cuboid,在查询进行时再进行后聚合。这个不精确匹配的 Cuboid 可能是 3.1.2 节中提到的 Cuboid 的父 Cuboid,甚至如果它的父 Cuboid 也没有被物化,Kylin 可能会一路追溯到使用 Base Cuboid 来回答查询请求。
使用不精确匹配的 Cuboid 比起使用精确匹配的 Cuboid 需要做更多查询时的后聚合计算,但是如果 Cube 优化得当,查询时的后聚合计算的开销也没有想象中的那么恐怖。以 3.1.2 节中 Shrink 值接近 100%的 Cuboid 为例,假设排除了这样的 Cuboid,那么只要它的父 Cuboid 被物化,从它的父 Cuboid 进行后聚合的开销也不大,因为父 Cuboid 没有比它多太多行的记录。
从这个角度来说,Kylin 的核心优势在于使用额外的空间存储预计算的结果,来换取查询时间的缩减。而 Cube 的剪枝优化,则是一种试图减少额外空间的方法,使用这种方法的前提是不会明显影响查询时间的缩减。在做剪枝优化的时候,需要选择跳过那些“多余”的 Cuboid:有的 Cuboid 因为查询样式永远不会被查询到,所以显得多余;有的 Cuboid 的能力和其他 Cuboid 接近,因此显得多余。但是 Cube 管理员不是上帝,无法提前甄别每一个 Cuboid 是否多余,因此 Kylin 提供了一系列简单工具来帮助完成 Cube 的剪枝优化。
图书简介:https://item.jd.com/12566389.html
相关阅读:
Apache Kylin权威指南(五):Getting Started
评论