Kylin |3.Cube的优化
Kylin Cube的优化
在没有采取任何优化措施的情况下,Kylin会对每一种维度的组合进行预计算,每种维度的组合的预计算结果被称为Cuboid。假设有4个维度,我们最终会有24 =16个Cuboid需要计算。
但在现实情况中,用户的维度数量一般远远大于4个。假设用户有10 个维度,那么没有经过任何优化的Cube就会存在210 =1024个Cuboid;而如果用户有20个维度,那么Cube中总共会存在220 =1048576个
Cuboid。虽然每个Cuboid的大小存在很大的差异,但是单单想到Cuboid的数量就足以让人想象到这样的Cube对构建引擎、存储引擎来说压力有多么巨大。因此,在构建维度数量较多的Cube时,尤其要注意
Cube的剪枝优化(即减少Cuboid的生成)。优化核心即剪掉没必要的cuboid,把它的数量减少点就会更快
1. 维度表与事实表
1、维度表
- 要具有数据一致性,主键值必须是唯一的(否则 Kylin 构建过程会报错);
- 维度表越小越好,因为 Kylin 会将维度表加载到内存中供查询,过大的表不适合作为维度表,默认的阈值是 300MB;
- 改变频率低,Kylin 会在每次构建中试图重用维度表的快照(Snapshot),如果维度表经常改变的话,重用就会失效,这就会导致要经常对维度表创建快照;
- 维度表最好不要是 Hive 视图(View),因为每次都需要将视图进行物化,从而导致额外的时间开销。
2、事实表
- 移除不参与 Cube 构建的字段,可以提升构建速度,降低 Cube 构建结果的大小;
- 尽可能将事实表进行维度拆分,提取公用的维度;
- 保证维度表与事实表的映射关系,过滤无法映射的记录。
如果维度与事实表无法映射,某些字段(数据类型为 number 系列)会遇到构建失败的问题(
numberFormatException('\N')
,\N
是为 Hive 中 NULL 的实际存储内容);此外,在进行 Left Join 的时候会产生大量的 NULL,这些 NULL 值在真正查询中根本没有任何作用。
3、分区表
Hive 表支持多分区(Partition),简单地说,一个分区就是一个文件目录,存储了特定的数据文件。当有新的数据生成的时候,可以将数据加载到指定的分区,读取数据的时候也可以指定分区。对于 SQL 查
询,如果查询中指定了分区列的属性条件,则 Hive 会智能地选择特定分区(也就是目录),从而避免全量数据的扫描,减少读写操作对集群的压力。
Kylin 支持增量的 Cube 构建,通常是按时间属性来增量地从 Hive 表中抽取数据。如果 Hive 表正好是按此时间属性做分区的话,那么就可以利用到 Hive 分区的好处,每次在 Hive 构建的时候都可以直接跳过不
相干日期的数据,节省 Cube 构建的时间。这样的列在 Kylin 里也称为分割时间列(Partition Time Column),通常它应该也是 Hive 表的分区列。
2. Cube 构建优化
1、维度优化
- 分析查询条件,不参与 Group By 或者 Where 过滤的条件维度一定不要勾选;
- 一般而言,Left Join 右侧表里面的字段均可以作为 Derived (衍生)维度;
- 必需维度、层级维度、联合维度、衍生维度四种维度优化方案;
- 聚合组进一步对维度组合进行优化。
必需维度、层级维度、联合维度、衍生维度以及聚合组。
使用聚合组
聚合组(Aggregation Group)是一种强大的剪枝工具。聚合组假设一个Cube的所有维度均可根据业务需求划分成若干组(当然也可以是一个组),由于同一个组内的维度可能同时被同一个查询用到,因
此会表现出更加紧密的内在关联。每个分组的维度集合均是Cube所有维度的一个子集,不同的分组各自拥有一套维度集合,它们可能与其他分组有相同的维度,也可能没有相同的维度。每个分组各自独立地根
据自身的规则贡献出一批需要被物化的Cuboid,所有分组贡献的Cuboid的并集就成为了当前Cube中所有需要物化的Cuboid的集合。不同的分组有可能会贡献出相同的Cuboid,构建引擎会察觉到这点,并且保证
每一个Cuboid无论在多少个分组中出现,它都只会被物化一次。
聚合组的设计非常灵活,甚至可以用来描述一些极端的设计。假设我们的业务需求非常单一,只需要某些特定的Cuboid,那么可以创建多个聚合组,每个聚合组代表一个Cuboid。具体的方法是在聚合组中先包
含某个Cuboid所需的所有维度,然后把这些维度都设置为强制维度。这样当前的聚合组就只能产生我们想要的那一个Cuboid了。
再比如,有的时候我们的Cube中有一些基数非常大的维度,如果不做特殊处理,它就会和其他的维度进行各种组合,从而产生一大堆包含它的Cuboid。包含高基数维度的Cuboid在行数和体积上往往非常庞大,
这会导致整个Cube的膨胀率变大。如果根据业务需求知道这个高基数的维度只会与若干个维度(而不是所有维度)同时被查询到,那么就可以通过聚合组对这个高基数维度做一定的“隔离”。我们把这个高基数的
维度放入一个单独的聚合组,再把所有可能会与这个高基数维度一起被查询到的其他维度也放进来。这样,这个高基数的维度就被“隔离”在一个聚合组中了,所有不会与它一起被查询到的维度都没有和它一起出
现在任何一个分组中,因此也就不会有多余的Cuboid产生。这点也大大减少了包含该高基数维度的Cuboid的数量,可以有效地控制Cube的膨胀率。
对于每个分组内部的维度,用户可以使用如下三种可选的方式定义,它们之间的关系,具体如下。
1)强制维度(Mandatory),如果一个维度被定义为强制维度,那么这个分组产生的所有Cuboid中每一个Cuboid都会包含该维度。每个分组中都可以有0个、1个或多个强制维度。如果根据这个分组的业务逻
辑,则相关的查询一定会在过滤条件或分组条件中,因此可以在该分组中把该维度设置为强制维度。
在后续的查询中是否每次group by都会包含某维度
2)层级维度(Hierarchy),每个层级包含两个或更多个维度。假设一个层级中包含D1,D2…Dn这n个维度,那么在该分组产生的任何Cuboid中, 这n个维度只会以(),(D1),(D1,D2)…(D1,D2…
Dn)这n+1种形式中的一种出现。每个分组中可以有0个、1个或多个层级,不同的层级之间不应当有共享的维度。如果根据这个分组的业务逻辑,则多个维度直接存在层级关系,因此可以在该分组中把这些维度
设置为层级维度。层级之间是包含关系。
用户选择的维度中常常会出现具有层级关系的维度。例如对于国家(country)、省份(province)和城市(city)这三个维度,从上而下来说国家/省份/城市之间分别是一对多的关系。
也就是说,用户对于这三个维度的查询可以归类为以下三类: 1. group by country 2. group by country, province(等同于group by province) 3. group by country, province, city(等同于 group by city) 如果ABCD 四个维度中ABC这三个被设置为层级维度, abc=ac=bc 则 abcd=acd=bcd,所以剪掉acd,bcd,保留abcd足以, 则最终生成的cube:
3)联合维度(Joint),每个联合中包含两个或更多个维度,如果某些列形成一个联合,那么在该分组产生的任何Cuboid中,这些联合维度要么一起出现,要么都不出现。每个分组中可以有0个或多个联合,但
是不同的联合之间不应当有共享的维度(否则它们可以合并成一个联合)。如果根据这个分组的业务逻辑,多个维度在查询中总是同时出现,则可以在该分组中把这些维度设置为联合维度。
A B C为联合维度,3个维度在一块才有意义;
用户有时并不关心维度之间各种细节的组合方式,例如用户的查询语句中仅仅会出现 group by A, B, C,而不会出现 group by A, B 或者 group by C 等等这些细化的维度组合。这一类问题就是联合维度所解决的问题。例如将维度 A、B 和 C 定义为联合维度,Apache Kylin 就仅仅会构建 Cuboid ABC,而 Cuboid AB、BC、A 等等Cuboid 都不会被生成。
A B C为联合维度,则要么都在,要么都不在,最终的Cuboid 数目从 16 减少到 4。
4)衍生(推导)维度
衍生(推导)维度必须来自从表(维表),这类维度的意思是可推导的维度,需要该维度对应的一个或者多个列可以和维度表的主键是一对一的,是一种强映射,这种维度可以大大减少cuboid个数
normal维度来自主表,derived从表的维度默认会被外键(与主键一一对应)推导出来;如果在某种情况下不合理可以改edit改为normal,推导维度没必要选聚合组,默认也不会让我们选,;
Fact表:A(a,b,c)
Lookup表:B(x,y,z)
如果维度c 中,每种情况都唯一对应一种 x,y。即 abc==abxy,或者说所有xy的组合都可以替换为c。所以可以将 x,y 设置为derived维度,可以减少cuboid的个数。
在查询时:select xx from xx group by x,y;会被kylin等价转换为select ... group by c;
并发粒度优化
当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。这样无论Segment的大小如何变化,它的分区数量最小都不会低于2,最大都不会超过100。相应地,这个Segment背后的存储引擎(HBase)为了存储这个
Segment,也不会使用小于两个或超过100个的分区。我们还调整了默认的kylin.hbase.region.cut,这样50GB的Segment基本上会被分配到50个分区,相比默认设置,我们的Cuboid可能最多会获得5倍的并发量。
3. 其他优化
- RowKeys 顺序:Mandatory 维度、where 过滤条件中出现频率较多的维度、高基数维度、低基数维度;
- ShardBy 设置:建议选择基数较大的列作为 ShardBy 列,以使得数据可以均匀分布;
- 数据压缩:Kylin 针对维度字典以及维度表快照采用了特殊的压缩算法,对于 HBase 中的聚合计算数据利用了 Hadoop 的 LZO 或者是 Snappy 等压缩算法,从而保证存储在 HBase 以及内存中的数据尽可能地小;
- 对于大的事实表可以采用分区来增量构建,然后设置定期自动合并(Merge)操作;
- 事实表中日期数据类型为尽可能设置为 date;
- ShardBy 字段有助于全部数据分散分布在各个 Region 中,有助于防止出现数据倾斜等问题;
- ShardBy 字段为 True 后,同一个值的数据会存储在一起,便于批量捞取数据。
4. kylin构建工具对cube的分析
----(对构建完成后的cube进行分析)
Apache Kylin提供了一个简单的工具,供用户检查Cube中哪些Cuboid 最终被预计算了,称其为被物化(Materialized)的Cuboid。同时,这种方法还能给出每个Cuboid所占空间的估计值。由于该工具需要
在对数据进行一定阶段的处理之后才能估算Cuboid的大小,因此一般来说只能在Cube构建完毕之后再使用该工具。目前关于这一点也是该工具的一大不足,由于同一个Cube的不同Segment之间仅是输入数据不
同,模型信息和优化策略都是共享的,所以不同Segment中哪些Cuboid被物化哪些没有被物化都是一样的。因此只要Cube中至少有一个Segment,就能使用如下的命令行工具去检查这个Cube中的Cuboid状态:
[kris@hadoop101 kylin]$ bin/kylin.sh org.apache.kylin.engine.mr.common.CubeStatsReader cube_partition
Sampling percentage: 100 Mapper overlap ratio: 1.0 Mapper number: 1 Length of dimension DEFAULT.EMP_PARTITION.JOB is 1 Length of dimension DEFAULT.EMP_PARTITION.MGR is 1 Length of dimension DEFAULT.EMP_PARTITION.DEPTNO is 1 Length of dimension DEFAULT.EMP_PARTITION.HIRE_DATE is 1 |---- Cuboid 1111, est row: 14, est MB: 0 |---- Cuboid 0111, est row: 14, est MB: 0, shrink: 100% |---- Cuboid 0011, est row: 14, est MB: 0, shrink: 100% ##收缩比例为100%,相当于父维度0111是没有收缩的,应该去掉这个节点,他和0011是一模一样 |---- Cuboid 0001, est row: 13, est MB: 0, shrink: 92.86% ##两个列分组更精细即父维度更精细,子维度相对于父维度应该差别更大,如果差别小就应该砍掉,砍的维度越多,Cuboid越少。 |---- Cuboid 0010, est row: 3, est MB: 0, shrink: 21.43% |---- Cuboid 0101, est row: 14, est MB: 0, shrink: 100% |---- Cuboid 0100, est row: 7, est MB: 0, shrink: 50% |---- Cuboid 0110, est row: 9, est MB: 0, shrink: 64.29% |---- Cuboid 1011, est row: 14, est MB: 0, shrink: 100% |---- Cuboid 1001, est row: 14, est MB: 0, shrink: 100% |---- Cuboid 1000, est row: 5, est MB: 0, shrink: 35.71% |---- Cuboid 1010, est row: 9, est MB: 0, shrink: 64.29% |---- Cuboid 1101, est row: 14, est MB: 0, shrink: 100% |---- Cuboid 1100, est row: 8, est MB: 0, shrink: 57.14% |---- Cuboid 1110, est row: 10, est MB: 0, shrink: 71.43%
从以上分析结果的下半部分可看到,所有的Cuboid及它的分析结果都以树状的形式打印了出来。
在这棵树中,每个节点代表一个Cuboid,每个Cuboid都由一连串1或0的数字组成,如果数字为0,则代表这个Cuboid中不存在相应的维度;如果数字为1,则代表这个Cuboid中存在相应的维度。有几个1代表有几个维度。
除了最顶端的Cuboid之外,每个Cuboid都有一个父亲Cuboid,且都比父亲Cuboid少了一个“1”。其意义是这个Cuboid就是由它的父亲节点减少一个维度聚合而来的(上卷)。
最顶端的Cuboid称为Base Cuboid,它直接由源数据计算而来。 每行Cuboid的输出中除了0和1的数字串以外,后面还有每个Cuboid 的的行数与父亲节点的对比(Shrink值),比如 row: 13 / row: 14 = 92.86%
所有Cuboid行数的估计值之和应该等于Segment的行数估计值,每个Cuboid都是在它的父亲节点的基础上进一步聚合而成的,因此从理论上说每个Cuboid无论是行数还是大小都应该小于它的父亲。
在这棵树中,我们可以观察每个节点的Shrink值,如果该值接近100%,则说明这个Cuboid虽然比它的父亲Cuboid少了一个维度,但是并没有比它的父亲Cuboid少很多行数据。
换而言之,即使没有这个Cuboid, 我们在查询时使用它的父亲Cuboid,也不会有太大的代价。那么我们就可以对这个Cuboid进行剪枝操作。
5. 膨胀率(Expansion Rate)来分析
还有一种更为简单的方法可以帮助我们判断Cube是否已经足够优化。在Web GUI的Model页面选择一个READY状态的Cube,当我们把光标移到该Cube的Cube Size列时,Web GUI会提示Cube的源数据大小,
以及当前Cube的大小除以源数据大小的比例,称为膨胀率(Expansion Rate),如图
一般来说,Cube的膨胀率应该在0%~1000%之间,如果一个Cube的膨胀率超过1000%,那么Cube管理员应当开始挖掘其中的原因。通常,膨胀率高有以下几个方面的原因。
1)Cube中的维度数量较多,且没有进行很好的Cuboid剪枝优化,导致Cuboid数量极多;
2)Cube中存在较高基数的维度,导致包含这类维度的每一个Cuboid占用的空间都很大,这些Cuboid累积造成整体Cube体积变大;
3)存在比较占用空间的度量,例如Count Distinct,因此需要在Cuboid的每一行中都为其保存一个较大的寄存器,最坏的情况将会导致Cuboid中每一行都有数十KB,从而造成整个Cube的体积变大;
6. 注意事项
1、哪些维度参与了 Cuboid 构建?
很多人会误以为只有聚合组 Includes 里面被选择的维度才参与 Cube 构建的计算,其实真正参与计算是 Dimensions 设置界面选择为 Default 的那些维度,还有 Left Join 的字段(如果其 Derived 字段被勾选)。
简单来说,在 RowKeys 排序界面看到的维度都会参与 Cuboid 的构建,如果在 RowKeys 看到不想参与计算的维度,可以到 Dimensions 设置界面进行修改。
2、结合业务设置层级维度
层级维度在设置的时候一定要深刻理解业务,例如:企业到底归属到行业类型下还是归属到区域类型下?从层级结构的角度来讲都是可以的,深层次挖掘业务需求后我们会发现,凡是查询企业的时候均附带有区域的过滤条件,因此将企业归属到区域的层级下比归属到行业类型下更加有效。
3、善于尝试
在实践中,可能会遇到各种不确定的思路,最佳的做法是去实践,多建立 Model 和 Cube,从不同层次去优化,去尝试,在实践中不断总结经验。
Kylin的角色为两台all、一台query
在kylin-3.1.0/conf/kylin.properties配置文件中如下的不同:
kylin.server.mode=all #角色为all。
kylin.server.mode=query #角色为query。