阿里云安全(en)

带你读《Apache Kylin权威指南》之三:Cube优化(en)

2019-11-07 00:00:00 mimukeji

点击查看第一章
点击查看第二章

第3章

Cube优化

Apache Kylin的核心思想是根据用户的数据模型和查询样式对数据进行预计算,并在查询时直接利用预计算结果返回查询结果。
相比普通的大规模并行处理解决方案,Kylin具有响应时间快、查询时资源需求小、吞吐量大等优点。用户的数据模型包括维度、度量、分区列等基本信息,也包括用户通过Cube优化工具赋予其的额外的模型信息。
例如,层级(Hierarchy)是用来描述若干个维度之间存在层级关系的一种优化工具,提供层级信息有助于预计算跳过多余的步骤,减少预计算的工作量,最终减少存储引擎所需要存储的Cube数据的大小。
数据模型是数据固有的属性,除此之外,查询的样式如果相对固定,有助于Cube优化。例如,如果知道客户端的查询总是会带有某个维度上的过滤(Filter)条件,或者总是会按照这个维度进行聚合(Group By),那么所有的不带这个维度的场景下的预计算都可以跳过,因为即使为这些场景进行了预计算,这些预计算结果也不会被用到。
总的来说,在构建Cube之前,Cube的优化手段提供了更多与数据模型或查询样式相关的信息,用于指导构建出体积更小、查询速度更快的Cube。可以看到Cube的优化目标始终有两个大方向:空间优化和查询时间优化。

3.1 Cuboid剪枝优化

3.1.1 维度的组合

由之前的章节可以知道,在没有采取任何优化措施的情况下,Kylin会对每一种维度的组合进行聚合预计算,维度的一种排列组合的预计算结果称为一个Cuboid。如果有4个维度,结合简单的数学知识可知,总共会有24=16种维度组合,即最终会有24=16个Cuboid需要计算,如图3-1所示。其中,最底端的包含所有维度的Cuboid称为Base Cuboid,它是生成其他Cuboid的基础。
image.png
在现实应用中,用户的维度数量一般远远大于4个。假设用户有10个维度,那么没做任何优化的Cube总共会存在210=1024个Cuboid,而如果用户有20个维度,那么Cube中总共会存在220=1048576个Cuboid!虽然每个Cuboid的大小存在很大差异,但是仅Cuboid的数量就足以让人意识到这样的Cube对构建引擎、存储引擎来说会形成巨大的压力。因此,在构建维度数量较多的Cube时,尤其要注意进行Cube的剪枝优化。

3.1.2 检查Cuboid数量

Apache Kylin提供了一种简单的工具供用户检查Cube中哪些Cuboid最终被预计算了,将其称为被物化(materialized)的Cuboid。同时,这种工具还能给出每个Cuboid所占空间的估计值。该工具需要在Cube构建任务对数据进行一定的处理之后才能估算Cuboid的大小,具体来说,就是在构建任务完成“Save Cuboid Statistics”这一步骤后才可以使用该工具。
由于同一个Cube的不同Segment之间仅是输入数据不同,模型信息和优化策略都是共享的,所以不同的Segment中被物化的Cuboid是相同的。因此,只要Cube中至少有一个Segment完成了“Save Cuboid Statistics”这一步骤的构建,那么就能使用如下的命令行工具去检查这个Cube中的Cuboid的物化状态:
bin/kylin.sh org.apache.kylin.engine.mr.common.CubeStatsReader CUBE_NAME

CUBE_NAME 想要查看的Cube的名称
该命令的输出如图3-2所示。
image.png
在该命令的输出中,会依次打印出每个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节中详细展开。

3.1.3 检查Cube大小

还有一种更为简单的方法可以帮助我们判断Cube是否已经足够优化。在Web GUI的“Model”页面中选择一个READY状态的Cube,当把光标移到该Cube的“Cube Size”列时,Web GUI会提示Cube的源数据大小,以及当前Cube的大小与源数据大小的比例,称之为膨胀率(Expansion Rate),如图3-3所示。
image.png
一般来说,Cube的膨胀率应该为0%~1000%,如果一个Cube的膨胀率超过1000%,Cube管理员应当开始挖掘其中的原因。通常,膨胀率高有以下几个方面的原因:

  • Cube中的维度数量较多,且没有进行很好的Cuboid剪枝优化,导致Cuboid数量极多;
  • Cube中存在较高基数的维度,导致包含这类维度的每一个Cuboid占用的空间都很大,这些Cuboid累积造成整体Cube体积过大;
  • 存在比较占用空间的度量,如Count Distinct这样的度量需要在Cuboid的每一行中都保存一个较大的寄存器,最坏的情况会导致Cuboid中每一行都有数十千字节,从而造成整个Cube的体积过大;
    ……

因此,遇到Cube的膨胀率居高不下的情况,管理员需要结合实际数据进行分析,可灵活地运用本章接下来介绍的优化方法对Cube进行优化。

3.1.4 空间与时间的平衡

理论上所有能用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的剪枝优化。

3.2 剪枝优化工具

3.2.1 使用衍生维度

首先观察下面这个维度表,如图3-4所示。

image.png

这是一个常见的时间维度表,里面充斥着各种用途的时间维度,如每个日期对应的星期,每个日期对应的月份等。这些维度可以被分析师用来灵活地进行各个时间粒度上的聚合分析,而不需要进行额外的上卷操作。但是如果为了这个目的一下子引入这么多维度,会导致Cube中Cuboid的总数量呈现爆炸式的增长,往往得不偿失。
在实际使用中,可以在维度中只放入这个维度表的主键(在底层实现中,我们更偏向使用事实表上的外键,因为在Inner Join的情况下,事实表外键和维度表主键是一致的,而在Left Join的情况下事实表外键是维度表主键的超集),也就是只物化按日期(CAL_DT)聚合的Cuboid。当用户需要在更高的粒度如按周、按月来进行聚合时,在查询时会获取按日期聚合的Cuboid数据,并在查询引擎中实时地进行上卷操作,那么就达到了牺牲一部分运行时性能来节省Cube空间占用的目的。
Kylin将这样的理念包装成一个简单的优化工具—衍生维度。将一个维度表上的维度设置为衍生维度,则这个维度不会参与预计算,而是使用维度表的主键(其实是事实表上相应的外键)来替代它。Kylin会在底层记录维度表主键与维度表其他维度之间的映射关系,以便在查询时能够动态地将维度表的主键“翻译”成这些非主键维度,并进行实时聚合。虽然听起来有些复杂,但是使用起来其实非常简单,在创建Cube的Cube designer第二步添加维度的时候,选择“Derived”而非“Normal”,如图3-5所示。
image.png
衍生维度在Cube中不参加预计算,事实上如果前往Cube Designer的Advanced Setting,在Aggregation Groups和Rowkeys部分也完全看不到这些衍生维度,甚至在这些地方也找不到维度表KYLIN_CAL_DT的主键,因为如前所述,Kylin实际上是用事实表上的外键作为这些衍生维度背后真正的有效维度的,在前面的例子中,事实表与KYLIN_CAL_DT通过以下方式连接:
Join Condition:
DEFAULT.KYLIN_SALES.PART_DT = DEFAULT.KYLIN_CAL_DT.CAL_DT
因此,在Advanced Setting的Rowkeys部分就会看到PART_DT而看不到CAL_DT,更看不到那些KYLIN_CAL_DT表上的衍生维度,如图3-6所示。
image.png
虽然衍生维度具有非常大的吸引力,但也并不是说所有的维度表上的维度都得变成衍生维度,如果从维度表主键到某个维度表维度所需的聚合工作量非常大,如从CAT_DT到YEAR_BEG_DT基本上需要365 : 1的聚合量,那么将YERR_BEG_DT作为一个普通的维度,而不是衍生维度可能是一种更好的选择。这种情况下,YERR_BEG_DT会参与预计算,也会有一些包含YERR_BEG_DT的Cuboid被生成。

3.2.2 聚合组

聚合组(Aggregation Group)是一个强大的剪枝工具,可以在Cube Designer的Advanced Settings里设置不同的聚合组。聚合组将一个Cube的所有维度根据业务需求划分成若干组(当然也可以只有一个组),同一个组内的维度更可能同时被同一个查询用到,因此表现出更加紧密的内在关联。不同组之间的维度在绝大多数业务场景里不会用在同一个查询里,因此只有在很少的Cuboid里它们才有联系。所以如果一个查询需要同时使用两个聚合组里的维度,一般从一个较大的Cuboid在线聚合得到结果,这通常也意味着整个查询会耗时较长。
每个分组的维度集合是Cube的所有维度的一个子集,分组之间可能有相同的维度,也可能完全没有相同的维度。每个分组各自独立地根据自身的规则产生一批需要被物化的Cuboid,所有分组产生的Cuboid的并集就形成了Cube中全部需要物化的Cuboid。不同的分组有可能会贡献出相同的Cuboid,构建引擎会察觉到这点,并且保证每一个Cuboid无论在多少个分组中出现,都只会被物化一次,如图3-7所示。

image.png

举例来说,假设有四个维度A、B、C、D,如果知道业务用户只会进行维度AB的组合查询或维度CD的组合查询,那么该Cube 可以被设计成两个聚合组,分别是聚合组AB和聚合组 CD。如图3-8所示,生成的Cuboid的数量从24=16个缩减成8个。
image.png
假设创建了一个分析交易数据的Cube,它包含以下维度:顾客 ID(buyer_id)、交易日期(cal_dt)、付款的方式(pay_type)和买家所在的城市(city)。有时分析师需要通过分组聚合 city 、cal_dt和pay_type 来获知不同消费方式在不同城市的情况;有时分析师需要通过聚合city、cal_dt和buyer_id,来查看不同城市的顾客的消费行为。在上述实例中,推荐建立两个聚合组,包含的维度和方式如图3-9所示。
聚合组1:包含维度 [cal_dt, city, pay_type]
聚合组2:包含维度 [cal_dt, city, buyer_id]
image.png
可以看到,这样设置聚合组后,组之间会有重合的Cuboid(上图浅灰色部分),对于这些Cuboid只会构建一次。在不考虑其他干扰因素的情况下,这样的聚合组设置将节省不必要的3个Cuboid: [pay_type, buyer_id]、[city, pay_type, buyer_id]和[cal_dt, pay_type, buyer_id],这样就节省了存储资源和构建的执行时间。
在执行查询时,分几种情况进行讨论:
情况1(分组维度在同一聚合组中):
SELECT cal_dt, city, pay_type, count(*) FROM table GROUP BY cal_dt, city, pay_type
将从Cuboid [cal_dt, city, pay_type]中获取数据。
情况2(分组维度在两个聚合组交集中):
SELECT cal_dt, city count(*) FROM table GROUP BY cal_dt, city
将从Cuboid [cal_dt, city]中获取数据,可以看到这个Cuboid同时属于两个聚合组,这对查询引擎是透明的。
情况3 如果有一条不常用的查询(分组维度跨越了两个聚合组):
SELECT pay_type, buyer_id, count(*) FROM table GROUP BY pay_type, buyer_id
没有现成的完全匹配的Cuboid,此时,Kylin会先找到包含这两个维度的最小的Cuboid,这里是Base Cuboid [pay_type,cal_dt,city,buyer_id],通过在线聚合的方式,从Case Cuboid中计算出最终结果,但会花费较长的时间,甚至有可能造成查询超时。

3.2.3 必需维度

如果某个维度在所有查询中都会作为group by或者where中的条件,那么可以把它设置为必需维度(Mandatory),这样在生成Cube时会使所有Cuboid都必须包含这个维度,Cuboid的数量将减少一半。
通常而言,日期维度在大多数场景下可以作为必需维度,因为一般进行多维分析时都需要设置日期范围。
再次,如果某个查询不包含必需维度,那么它将基于某个更大的Cuboid进行在线计算以得到结果。

3.2.4 层级维度

如果维度之间有层级关系,如国家–省–市这样的层级,我们可以在Cube Designer的Advanced Settings里设置层级维度。注意,需要按从大到小的顺序选择维度。
查询时通常不会抛开上级节点单独查询下级节点,如国家–省–市的维度组合,查询的组合一般是「国家」「国家,省」「国家,省,市」。因为城市会有重名,所以不会出现「国家,市」或者[市]这样的组合。因此将国家(Country)、省(Province)、市(City)这三个维度设为层级维度后,就只会保留Cuboid[Country, Province,City],[Country, Province],[Country]这三个组合,这样能将三个维度的Cuboid组合数从 8个减至3个。
层级维度的适用场景主要是一对多的层级关系,如地域层级、机构层级、渠道层级、产品层级。
如果一个查询没有按照设计来进行,如select Country,City,count(*) from table group by Country,City,那么这里不能回答这个查询的Cuboid会从最接近的Cuboid[Country, Province, City]进行在线计算。显而易见,由于[Country, Province, City]和[Country, City]之间相差的记录数不多,这里在线计算的代价会比较小。

3.2.5 联合维度

联合维度(Joint Dimension)一般用在同时查询几个维度的场景,它是一个比较强力的维度剪枝工具,往往能把Cuboid的总数降低几个数量级。
举例来说,如果用户的业务场景中总是同时进行A、B、C三个维度的查询分析,而不会出现聚合A、B或者聚合C这些更上卷的维度组合,那么这类场景就是联合维度所适合的。可以将维度 A、B和C定义为联合维度,Kylin就仅仅会构建Cuboid [A,B,C],而Cuboid A,B [A]等都不会被生成。最终的Cube结果如图3-10所示,Cuboid的数量从16个减至4个。

image.png

假设创建一个交易数据的Cube,它具有很多普通的维度,像是交易日期cal_dt、交易的城市city、顾客性别sex_id和支付类型pay_type等。分析师常用的分析总是同时聚合交易日期cal_dt、交易的城市city 和顾客性别sex_id,有时可能希望根据支付类型进行过滤,有时又希望看到所有支付类型下的结果。那么,在上述实例中,推荐设立一组聚合组,并建立一组联合维度,所包含的维度和组合方式下:
聚合组(Aggregation Group):[cal_dt, city, sex_id,pay_type]
联合维度(Joint Dimension): [cal_dt, city, sex_id]
情况1(查询包含所有的联合维度):
SELECT cal_dt, city, sex_id, count(*) FROM table GROUP BY cal_dt, city, sex_id
它将从Cuboid [cal_dt, city, sex_id]中直接获取数据。
情况2(如果有一条不常用的查询,只聚合了部分联合维度):
SELECT cal_dt, city, count(*) FROM table GROUP BY cal_dt, city
没有现成的完全匹配的 Cuboid,Kylin会通过在线计算的方式,从现有的 Cuboid [cal_dt, city, sex_id中计算出最终结果。
联合维度的适用场景:

  • 维度经常同时在查询where或group by条件中同时出现,甚至本来就是一一对应的,如customer_id和customer_name,将它们组成一个联合维度。
  • 将若干个低基数(建议每个维度基数不超过10,总的基数叉乘结果小于10000)的维度合并组成一个了联合维度,可以大大减少Cuboid的数量,利用在线计算能力,虽然会在查询时多耗费有限的时间,但相比能减少的存储空间和构建时间而言是值得的。
  • 必要时可以将两个有强关系的高基维度组成一个联合维度,如合同日期和入账日期。
  • 可以将查询时很少使用的若干维度组成一个联合维度,在少数查询场景中承受在线计算的额外时间消耗,但能大大减少存储空间和构建时间。

以上这些维度剪枝操作都可以在Cube Designer的Advanced Setting中的Aggregation Groups区域完成,如图3-11所示。
image.png
从图3-11中可以看到,目前Cube中只有一个分组,点击左下角的 “New Aggregation Group”按钮可以添加一个新的分组。在某一分组内,首先需要指定这个分组包含(Include)哪些维度,然后才可以进行必需维度、层级维度和联合维度的创建。除了“Include”选项,其他三项都是可选的。此外,还可以设置“Max Dimension Combination”(默认为0,即不加限制),该设置表示对聚合组的查询最多包含几个维度,注意一组层级维度或联合维度计为一个维度。在生成聚合组时会不生成超过“Max Dimension Combination”中设置的数量的Cuboid,因此可以有效减少Cuboid的总数。
聚合组的设计非常灵活,甚至可以用来描述一些极端的设计。假设我们的业务需求非常单一,只需要某几个特定的Cuboid,那么可以创建多个聚合组,每个聚合组代表一个Cuboid。具体的方法是在聚合组中先包含某个Cuboid所需的所有维度,然后把这些维度都设置为强制维度。这样当前的聚合组就只包含我们想要的那一个Cuboid了。
再如,有时我们的Cube中有一些基数非常大的维度,如果不做特殊处理,它会和其他维度进行各种组合,从而产生大量包含它的Cuboid。所有包含高基数维度的Cuboid在行数和体积上都会非常庞大,这会导致整个Cube的膨胀率过大。如果根据业务需求知道这个高基数的维度只会与若干个维度(而不是所有维度)同时被查询,那么就可以通过聚合组对这个高基数维度做一定的“隔离”。
我们把这个高基数的维度放入一个单独的聚合组,再把所有可能会与这个高基数维度一起被查询到的其他维度也放进来。这样,这个高基数的维度就被“隔离”在一个聚合组中了,所有不会与它一起被查询到的维度都不会和它一起出现在任何一个分组中,也就不会有多余的Cuboid产生。这大大减少了包含该高基数维度的Cuboid的数量,可以有效地控制Cube的膨胀率。

3.3 并发粒度优化

当Segment中的某一个Cuboid 的大小超出一定阈值时,系统会将该Cuboid的数据分片到多个分区中,以实现Cuboid数据读取的并行化,从而优化Cube的查询速度。具体的实现方式如下。
构建引擎根据Segment估计的大小,以及参数“kylin.hbase.region.cut”的设置决定Segment在存储引擎中总共需要几个分区来存储,如果存储引擎是HBase,那么分区数量就对应HBase中的Region的数量。kylin.hbase.region.cut的默认值是5.0,单位是吉字节(GB),也就是说,对于一个大小估计是50GB的Segment,构建引擎会给它分配10个分区。用户还可以通过设置kylin.hbase.region.count.min(默认为1)和kylin.hbase.region.count.max(默认为500)两个配置来决定每个Segment最少或最多被划分成多少个分区。
由于每个Cube的并发粒度控制不尽相同,建议在Cube Designer的Configuration Overwrites中为每个Cube量身定制控制并发粒度的参数。在下面的例子中,将把当前Cube的kylin.hbase.region.count.min设置为2,把kylin.hbase.region.count.max设置为100,如图3-12所示。这样,无论Segment的大小如何变化,它的分区数量最小不会低于2,最大不会超过100。相应地,这个Segment背后的存储引擎(HBase)为了存储这个Segment,也不会使用小于2个或者超过100个分区(Region)。我们将kylin.hbase.region.cut调整为1,这样,50GB的Segment基本上会被分配到50个分区,相比默认设置,我们的Cuboid可能最多会获得5倍的并发量。
image.png

3.4 Rowkey优化

前面章节的侧重点是减少Cube中Cuboid的数量,以优化Cube的存储空间和构建性能,统称以减少Cuboid的数量为目的的优化为Cuboid剪枝。在本节中,将重点通过对Cube的Rowkey的设置来优化Cube的查询性能。
Cube的每个Cuboid中都包含大量的行,每个行又分为Rowkey和Measure两个部分。每行Cuboid数据中的Rowkey都包含当前Cuboid中所有维度的值的组合。Rowkey中的各个维度按照Cube Designer→Advanced Setting→RowKeys中设置的顺序和编码进行组织,如图3-13所示。
image.png
在Rowkeys设置页面中,每个维度都有几项关键的配置,下面将一一道来。

3.4.1 调整Rowkey顺序

在Cube Designer→Advanced Setting→Rowkeys部分,可以上下拖动每一个维度来调节维度在Rowkey中的顺序。这种顺序对于查询非常重要,因为目前在实现中,Kylin会把所有的维度按照显示的顺序黏合成一个完整的Rowkey,并且按照这个Rowkey升序排列Cuboid中所有的行,参照前一章的图2-16。
不难发现,对排序靠前的维度进行过滤的效果会非常好,比如在图2-16中的Cuboid中,如果对D1进行过滤,它是严格按照顺序进行排列的;如果对D3进行过滤,它仅是在D1相同时在组内顺序排列的。
如果在一个比较靠后的维度进行过滤,那么这个过滤的执行就会非常复杂。以目前的HBase存储引擎为例,Cube的Rowkey就对应HBase中的Rowkey,是一段字节数组。我们目前没有创建单独的每个维度上的倒排索引,因此对于在比较靠后的维度上的过滤条件,只能依靠HBase的Fuzzy Key Filter来执行。尽管HBase做了大量相应的优化,但是在对靠后的字节运用Fuzzy Key Filter时,一旦前面维度的基数很大,Fuzzy Key Filter的寻找代价就会很高,执行效率就会降低。所以,在调整Rowkey的顺序时需要遵循以下几个原则:

  • 有可能在查询中被用作过滤条件的维度,应当放在其他维度的前面。
    a) 对于多个可能用作过滤条件的维度,基数高的(意味着用它进行过滤时,较多的行被过滤,返回的结果集较小)更适合放在前面;

b) 总体而言,可以用下面这个公式给维度打分,得分越高的维度越应该放在前排:
排序评分=维度出现在过滤条件中的概率*用该维度进行过滤时可以过滤掉的记录数。

  • 将经常出现在查询中的维度,放在不经常出现的维度的前面,这样,在需要进行后聚合的场景中查询效率会更高。
  • 对于不会出现在过滤条件中的维度,按照其基数的高低,优先将低基数的维度放在Rowkey的后面。这是因为在逐层构建Cuboid、确定Cuboid的生成树时,Kylin会优先选择Rowkey后面的维度所在的父Cuboid来生成子Cuboid,那么基数越低的维度,包含它的父Cuboid的行数就越少,聚合生成子Cuboid的代价就越小。

3.4.2 选择合适的维度编码

2.4.3节介绍过,Apache Kylin 支持多种维度编码方式,用户可以针对数据特征,选择合适的编码方式,从而减小数据的存储空间。在具体使用过程中,如果用错了编码方式,可能会导致构建和查询的一系列问题。这里要注意的事项包括:

  • 字典(Dictionary)编码(默认的编码)不适用于高基数维度(基数值在300万以上)。主要原因是,字典需要在单节点内存中构建,并在查询的时候加载到Kylin内存;过大的字典不但会使得构建变慢,还会在查询时占用很多内存,导致查询缓慢或失败,因此应该避免对高基数维度使用字典编码。如果实际中遇到高基数维度,首先思考此维度是否要引入Cube中,是否应该先对其进行泛化(Generalization),使其变成一个低基数维度;其次,如果一定要使用,那么可以使用Fixed_length编码,或Integer(如果这列的值是整型)编码。
  • Fixed_length编码是最简单的编码,它通过补上空字符(如果维度值长度小于指定长度))或截断(如果维度值长度大于指定长度),从而将所有值都变成等长,然后拼接到Rowkey中。它比较适合于像身份证号、手机号这样的等长值维度。如果某个维度长度变化区间比较大,那么你需要选择一个合适的长度:长度过短会导致数据截断从而失去准确性,长度过长则导致空间浪费。

3.4.3 按维度分片

在3.3节中介绍过,系统会对Cuboid中的数据在存储时进行分片处理。默认情况下,Cuboid的分片策略是对于所有列进行哈希计算后随机分配的。也就是说,我们无法控制Cuboid的哪些行会被分到同一个分片中。这种默认的方法固然能够提高读取的并发程度,但是它仍然有优化的空间。按维度分片提供了一种更加高效的分片策略,那就是按照某个特定维度进行分片(Shard By Dimension)。简单地说,当你选取了一个维度用于分片后,如果Cuboid中的某两行在该维度上的值相同,那么无论这个Cuboid最终被划分成多少个分片,这两行数据必然会被分配到同一个分片中。
这种分片策略对查询有着极大的好处。我们知道,Cuboid的每个分片会被分配到存储引擎的不同物理机器上。Kylin在读取Cuboid数据的时候会向存储引擎的若干机器发送读取的RPC请求。在RPC请求接收端,存储引擎会读取本机的分片数据,并在进行一定的预处理后发送RPC回应(如图3-14所示)。以HBase存储引擎为例,不同的Region代表不同的Cuboid分片,在读取Cuboid数据的时候,HBase会为每个Region开启一个Coprocessor实例来处理查询引擎的请求。查询引擎将查询条件和分组条件作为请求参数的一部分发送到Coprocessor中,Coprocessor就能够在返回结果之前对当前分片的数据做一定的预聚合(这里的预聚合不是Cube构建的预聚合,是针对特定查询的深度的预聚合)。
image.png
如果按照维度划分分片,假设是按照一个基数比较高的维度seller_id进行分片的,那么在这种情况下,每个分片承担一部分seller_id,各个分片不会有相同的seller_id。所有按照seller_id分组(group by seller_id)的查询都会变得更加高效,因为每个分片预聚合的结果会更加专注于某些seller_id,使得分片返回结果的数量大大减少,查询引擎端也无须对各个分片的结果做分片间的聚合。按维度分片也能让过滤条件的执行更加高效,因为由于按维度分片,每个分片的数据都更加“整洁”,便于查找和索引。

3.5 Top_N度量优化

在生活中我们总能看到“世界500强公司” “销量最好的十款汽车”等标题的新闻报道,Top_N 分析是数据分析场景中常见的需求。在大数据时代,由于明细数据集越来越大,这种需求越来越明显。在没有预计算的情况下,得到一个分布式大数据集的 Top_N 结果需要很长时间,导致点对点查询的效率很低。
Kylin v1.5以后的版本中引入了 Top_N 度量,意在进行Cube 构建的时候预计算好需要的 Top_N,在查询阶段就可以迅速地获取并返回Top_N记录。这样,查询性能就远远高于没有Top_N预计算结果的Cube,方便分析师对这类数据进行查询。
这里的 Top_N 度量是一个近似的实现,如你想要了解其近似度,需要在本节之后的内容中更多地了解 Top_N 背后的算法和数据分布结构。
让我们用Kylin中通过sample.sh生成的项目“learn_kylin” 对Top_N进行说明。我们将重点使用其中的事实表“kylin_sales”。
这张样例表 “default.kylin_sales” 模拟了在线集市的交易数据,内含多个维度和度量列。这里仅用其中的四列即可:“PART_DT” “LSTG_SITE_ID” “SELLER_ID”和“PRICE”。表3-1所示为这些列的内容和基数简介,显而易见 “SELLER_ID” 是一个高基列。
image.png
假设电商公司需要查询特定时段内,特定站点交易额最高的100位卖家。查询语句如下:

SELECT SELLER_ID, SUM(PRICE) FROM KYLIN_SALES
 WHERE 
    PART_DT >= date'2012-02-18' AND PART_DT < date'2013-03-18' 
       AND LSTG_SITE_ID in (0) 
    group by SELLER_ID 
    order by SUM(PRICE) DESC limit 100

方法1:在不设置Top_N度量的情况下,为了支持这个查询,在创建Cube时设计如下:定义“PART_DT”“LSTG_SITE_ID”“SELLER_ID”作为维度,同时定义 “SUM(PRICE) ”作为度量。Cube 构建完成之后,Base Cuboid 如表3-2所示。
image.png
假设这些维度是彼此独立的,则Base Cuboid中行数为各维度基数的乘积:730 50 1 million = 36.5 billion = 365亿。其他包含“SELLER_ID”字段的Cuboid也至少有百万行。由此可知,由“SELLER_ID”作为维度会使得Cube的膨胀率很高,如果维度更多或基数更高,则情况更糟。但真正的挑战不止如此。
我们可能还会发现上面那个SQL查询并不能正常执行,或者需要花费特别长的时间,原因是这个查询拥有太大的在线计算量。假设你想查30天内site_00销售额排前100名的卖家,则查询引擎会从存储引擎中读取约3000万行记录,然后按销售额进行在线排序计算(排序无法用到预计算结果),最终返回排前100名的卖家。由于其中的关键步骤没有进行预计算,因此虽然最终结果只有100行,但计算耗时非常长,且内存和其中的控制器都在查询时被严重消耗了。
反思以上过程,业务关注的只是销售额最大的那些卖家,而我们存储了所有的(100万)卖家,且在存储时是根据卖家ID而不是业务需要的销售额进行排序的,因此在线计算量非常大,因而此处有很大的优化空间。
方法2:为了得到同一查询结果。如果在创建Cube时,对需要的Top_N进行了预计算,则查询会更加高效。如果在创建Cube时设计如下:不定义“SELLER_ID”为维度,仅定义“PART_DT”“LSTG_SITE_ID”为维度,同时定义一个Top_N度量,如图3-15所示。
image.png
“PRICE”定义在“ORDER|SUM by Column”,“SELLER_ID”定义在“Group by Column”。
新Cube的Base Cuboid 如表3-3所示,Top_N度量的单元格中存储了按seller_id进行聚合且按sum(price) 倒序排列的seller_id和sum(price)的组合。
image.png
image.png
现在Base Cuboid中只有730 * 50 = 36500行。在度量的单元格中,预计算的Top_N结果以倒序的方式存储在一个容器中,而序列尾端的记录已经被过滤掉。
现在,对于上面那个Top_100 seller的查询语句,只需要从内存中读取30行,Kylin将会从Top_N Measure的容器中抽出“SELLER_ID”和“SUM(PRICE)”,然后将其返回客户端(并且是已经完成排序的)。现在查询结果就能以亚秒级返回了。
一般来说,Kylin在Top_N的单元格中会存储100倍的Top_N定义的返回类型的记录数,如对于Top100,就存储10000条seller00xxxx:xx.xx记录。这样一来,对于Base Cuboid的Top_N查询总是精确的,不精确的情况会出现在对于其他Cuboid的查询上。举例来说,对于Cuboid[PART_DT],Kylin会将所有日期相同而站点不同的TOP_N单元格进行合并,这个合并后的结果会是近似的,尤其是在各个站点的前几名卖家相差较多的情况下。比如,如果site_00中排第100名的卖家在其他站点中都排在第10000名以后,那么它的Top_N记录在其他站点都会被舍去,Kylin在合并TOP_N Measure时发现其他站点里没有这个卖家的值,于是会赋予这个卖家在其他站点中的sum(price)一个估计值,这个估计值既可能比实际值高,也可能比实际值低,最后它在Cuboid[PART_DT]中的Top_N单元格中存储的是一个近似值。

3.6 Cube Planner优化

Kylin自v2.3.0以后的版本引入了Cube Planner功能,自动地对Cube的结构进行优化。如图3-16所示,在用户定义的Aggregation Group等手动优化基础上,Cube Planner能根据每个Cuboid的大小和它对整个Cube产生的查询增益,结合历史查询数据对Cuboid进行进
image.png
一步剪枝。Cube Planner使用贪心算法和基因算法排除不重要和不必要的Cuboid,不对这些Cuboid进行预计算,从而大大减少计算量、节约存储空间,从而提高查询效率。
Cube Planner优化分为两个阶段。第一阶段发生在初次构建Cube时:Cube Planner会利用在“Extract Fact Table Distinct Columns”步骤中得到的采样数据,预估每个Cuboid的大小,进而计算出每个Cuboid的效益比(该Cuboid的查询成本 / 对应维度组合物化后对整个Cube的所有查询能减少的查询成本)。Cube Planner只会对那些效益比更高的维度组合进行预计算,而舍弃那些效益比更低的维度组合。第二阶段作用于已经运行一段时间的Cube。在这一阶段,Cube Planner会从System Cube中获取该Cube的查询统计数据,并根据被查询命中的概率给Cuboid赋予一定权重。当用户触发对Cube的优化操作时,那些几乎不被查询命中的Cuboid会被删除,而那些被频繁查询却尚未被预计算出的Cuboid则会被计算并更新到Cube中。
在Kylin v2.5.0及以后的版本中,Cube Planner默认开启,第一阶段的过程对用户透明,而使用第二阶段则需要事先配置System Cube并由用户手动触发优化。关于Cube Planner的具体实现原理、使用方法及相关配置会在本书第6章中详述。

3.7 其他优化

3.7.1 降低度量精度

有一些度量有多种精度可供选择,但是精度较高的度量往往需要付出额外的代价,这就意味着更大的空间占用和更多的运行及构建成本。以近似值的Count Distinct度量为例,Kylin提供多种精度的选择,让我们选择其中几种进行对比,如表3-4所示。
image.png
从表3-4中可以看出,HLLC 16类型占用的空间是精度最低的类型的64倍!而即使是精度最低的Count Distinct度量也已经非常占用空间了。因此,当业务可以接受较低精度时,用户应当考虑Cube空间方面的影响,尽量选择低精度的度量。

3.7.2 及时清理无用Segment

第4章提及,随着增量构建出来的Segment慢慢累积,Cube的查询性能将会下降,因为每次跨Segment查询都需要从存储引擎中读取每一个Segment的数据,并且在查询引擎中对不同Segment的数据进行再聚合,这对于查询引擎和存储引擎来说都是巨大的压力。从这个角度来说,及时使用第4章介绍的Segment碎片清理方法,有助于优化Cube的使用效率。

3.8 小结

本章从多个角度介绍了Cube的优化方法:从Cuboid剪枝的角度、从并发粒度控制的角度、从Rowkey设计的角度,还有从度量精度选择的角度。总的来说,Cube优化需要Cube管理员对Kylin有较为深刻的理解和认识,这也无形中提高了使用和管理Kylin的门槛。对此,我们在较新的Kylin版本中通过对数据分布和查询样式的历史进行分析,自动化一部分优化操作,帮助用户更加方便地管理Kylin中的数据,详见第6章。长度“12”

(en)

阿里云优惠新机+优惠券

本文转载自网络,如有侵权,请联系我们删除。

Home

About

product

success

news

form

bbs

contact

工单(en)

阿里云报价咨询(en)