Apache Kylin 从零开始构建Cube(含优化策略)

2022-04-12 00:00:00 数据 基数 构建 聚合 维度

前言

Apache Kylin采用“预计算”的模式,用户只需要提前定义好查询维度,Kylin将帮助我们进行计算,并将结果存储到HBase中,为海量数据的查询和分析提供亚秒级返回,是一种典型的“空间换时间”的解决方案。


Kylin架构

  • Hadoop/Hive:Kylin是一个MOLAP系统,将hive中的数据进行预计算,利用MR或者SPARK来进行实现

  • HBase:kylin用来存储OLAP分析的cube数据的地方,实现多维数据集的交互式查询

  • Rest Server:提供restful接口

  • Query Engine:使用开源的calcite框架实现sql的解析,是sql引擎层

  • Routing:负责将解析生成的执行计划转换层cube缓存的查询

  • Metadata:Kylin中大部分元数据信息的存储

  • Cube Build Engine:负责kylin预计算中创建cube


一.概念

数据仓库
Data Warehouse,简称DW,中文名数据仓库,是商业智能(BI)中的核心部分。数据仓库中存储的则主要是历史数据,主要是将不同数据源的数据整合到一起,目的是为企业决策提供支持,所以可能存在大量数据冗余,但利于多个维度查询,为决策者提供更多观察视角。


OLAP
OLAP(Online Analytical Process),联机分析处理,以多维度的方式分析数据,一般带有主观的查询需求,多应用在数据仓库,侧重于提供决策支持。与之对应的是OLTP(Online Transaction Process),联机事务处理,侧重于数据库的增删查改等常用业务操作。

OLAP以多维度的方式分析数据,而且能够弹性地提供以下几种操作

  • 钻取:在维的不同层次间的变化,从上层降到下一层,或者说将汇总数据拆分到更细节的数据

  • 上卷:钻取的逆操作,即从细粒度数据向更高汇总层的聚合

  • 切片:选择维中特定的值进行分析

  • 切块:选择维中特定区间的数据或者某批特定值进行分析

  • 旋转:维的位置互换,就像是二维表的行列转换

OLAP操作


维度和度量

  • 维度是指审视数据的角度,它通常是数据记录的一个属性,例如时间、地点等。

  • 度量是基于数据所计算出来的考量值;它通常是一个数值,如总销售额、不同的用户数等。

事实表和维度表

  • 事实表(Fact Table)是指存储有事实记录的表,如系统日志、销售记录、传感器数值等;

  • 维度表(Dimension Table)或维表,也叫做查找表(Lookup Table),是与事实表相对应的一种表;它保存了维度的属性值,可以跟事实表做关联;相当于将事实表上经常重复的属性抽取、规范出来用一张表进行管理。如日期表,地区表

模型概念

  • 星形模型:特点是只有一张事实表,以及零到多个维度表,事实表与维度表通过主外键相关联,维度表之间没有关联;

  • 雪花模型:就是将星形模型中的某些维表抽取成更细粒度的维表,然后让维表之间也进行关联;

  • 星座模型:具有多个事实表,维表可以在不同事实表之间共用,这种模型被称为星座模型;


二.构建准备

1.在Hive中准备数据

需要被分析的数据必须先保存为Hive表的形式,然后Kylin才能从Hive中导入数据,创建Cube。Cube支持从Hive视图中构建,基于这个特点,可以将原始数据做一定的处理,如增加维度或者做一些预处理,生成相应的视图,基于视图来构建Cube。

2.维度表设计
  • 维度的基数不宜过大

  • 主键

  • 维度表好不是Hive的视图


维度的基数,维度的基数体现了Cube的复杂程度,维度基数过大,会增加Cube的膨胀程度,使用Count-Distinct来对一个维度的基数做一个统计,可以保证能够设计合理的Cube。


Kylin支持增量Cube构建,通常是按事件属性来增量的从Hive表中抽取数据。因此Hive表好按时间属性分区,这样可以避免全量数据的扫描,减少读写操作对集群的压力,节省Cube构建的时间。


UHC 代表 Ultra High Cardinality,即超高基数。基数表示维度不同值的数量。通常,维度的基数从数十到数百万。如果超过百万,我们将其称为超高基维度,Kylin 支持超高基维度,但是在 Cube 设计中额外注意超高基维度,它们可能会使 Cube 体积非常大、查询变慢。


Cube 的大物理维度数量 (不包括衍生维度) 是 63,但是不推荐使用大于 30 个维度的 Cube,会引起维度灾难。


三.设计Cube的全过程

1.导入Hive表

创建或者选择一个已有的Project,将Hive中表的定义导入到Kylin中,Web界面的操作如下,Mode->DataSource->Load Hive Table。

LoadTable

导入Hive表

之后Kylin会触发一个MR或者Spark任务,计算此表基于每个列的基数,这里Kylin对基数的计算方法采用的是HyperLogLog近似算法,与值有误差,但是作为参考值已经足够了。


2.创建数据模型Data Model

数据模型是构建Cube的基础,该数据模型可以描述为一个星型模型或者一个雪花模型,有了模型定义Cube的时候,可以在此模型定义的表和列中进行选择,基于一个模型可以创建多个Cube,减少了用户的重复性工作。

在Web界面,点击New->New Model,开始创建数据模型。


创建数据模型

填写好基本信息之后,开始构建模型。

首先选择事实表,然后添加维度表,添加维度表需要选择连接的类型,是Inner还是Left,然后选择连接的主键和外键。

添加维度表

接下来会选择用作维度或者度量的列,这里只是选择一个范围,不代表这些列将来一定会用作Cube的构建,在这里可以把可能会用到的列都添加进来,创建Cube的时候,将只能从这些列中选择。

选择维度

度量列只能来自事实表,维度列可以来自维度表和事实表。

后一步是,为模型补充分割时间的列和过滤条件,如果此模型中的事实表的记录是按照时间来增加的,可以指定一个日期或者时间列作为模型的分割时间列,从而可以让Cube按此列做增量构建。

除此之外,可以指定过滤条件。Kylin在向Hive请求数据的时候,会带上此过滤条件。


3.设计Cube

1)首先选择要使用的数据模型,并为此Cube输入一个的名称,添加一些描述信息。

2)然后选择Cube的维度

Add Dimension逐个添加维度,可以是普通维度,可以是衍生(Derived)维度。
需要为每一个维度起个名字,然后选择表和列,如果是衍生维度,则必须是来自某个维度表,一次可以选择多个列,这些列值都可以从该维度表的主键衍生出来。

添加维度

3)创建度量
Kylin默认会创建一个Count(1)的度量。可以单击“+Measure”按钮来添加新的度量。Kylin支持的度量有:SUM、MIN、MAX、COUNT、COUNT DISTINCT、TOP_N、RAW等。Kylin可以支持在一个Cube中添加多达上百个的度量。

4)关于Cube数据刷新的设置。在这里可以设置自动合并的阈值、数据保留的短时间,以及个Segment的起点时间(如果Cube有分割时间列的话)

Cube数据刷新的设置

设置Auto Merge Thresholds:合并的阈值可以设置多个层级,当大阈值不能满足时,尝试下一个稍小的阈值。

设置Volatile Range:如何你不想Kylin自动合并近某个时间段的Segment,可以设置改属性。

设置Retention Threshold:如果你想只保留近1年的Segment中的数据,可以设置该值为365。

5)设置。在此页面上可以设置聚合组和Rowkey
Kylin默认会把所有维度都放在同一个聚合组中;如果维度数较多(例如>10),那么建议用户根据查询的习惯和模式,单击“New Aggregation Group+”,将维度分为多个聚合组。

设置聚合组

在HBase中Key的存储方式?
Kylin以Key-Value的方式将Cube存储到HBase中。HBase的key,也就是Rowkey,是由各维度的值拼接而成的;为了更高效地存储这些值,Kylin会对它们进行编码和压缩;每个维度均可以选择合适的编码(Encoding)方式,默认采用的是字典(Dictionary)编码技术;除了字典以外,还有整数(Int)和固定长度(Fixed Length)的编码等。

字典编码是将此维度下的所有值构建成一个从string到int的映射表;Kylin会将字典序列化保存,在Cube中存储int值,从而大大减小存储的大小。

字典编码的优势是产生的编码非常紧凑,尤其在维度值的基数较小且长度较大的情况下,特别节约空间。由于产生的字典是在查询时加载入构建引擎和查询引擎的,所以在维度的基数大、长度也大的情况下,容易造成构建引擎或查询引擎的内存溢出。


Kylin支持的编码方式还有以下几种:

  • Date编码:日期编码,使用三个字节进行编码

  • Time编码:Time编码仅仅支持到秒,每个维度仅仅使用4个字节

  • Integer编码:需要提供一个额外的参数“Length”来代表需要多少个字节

  • Fixed_length编码:采用一段固定长度的字节来存储代表维度值的字节数组。


各维度在Rowkeys中的顺序如何设置?
各维度在Rowkeys中的顺序,对于查询的性能会产生较明显的影响。通常建议将 mandantory 维度放在开头, 然后是在过滤 ( where 条件)中起到很大作用的维度;如果多个列都会被用于过滤,将高基数的维度(如 user_id)放在低基数的维度(如 age)的前面。这样做的好处是,充分利用过滤条件来缩小在HBase中扫描的范围,从而提高查询的效率。


其余需要主要的设置?

  • Mandatory Cuboids: 维度组合白名单。确保你想要构建的 cuboid 能被构建。

  • Cube Engine: cube 构建引擎。有两种:MapReduce 和 Spark。如果你的 cube 只有简单度量(SUM, MIN, MAX),建议使用 Spark。如果 cube 中有复杂类型度量(COUNT DISTINCT, TOP_N),建议使用 MapReduce。

  • Advanced ColumnFamily: 如果有超过一个的COUNT DISTINCT 或 TopN 度量, 可以将它们放在更多列簇中,以优化与HBase 的I/O。


Advanced ColumnFamily

6)设置
为Cube配置参数。和其他Hadoop工具一样,Kylin使用了很多配置参数以提高灵活性,用户可以根据具体的环境、场景等配置不同的参数进行调优。

单击“+Property”按钮,然后输入参数名和参数值,如指定kylin.hbase.region.cut=2,这样此Cube在存储的时候,Kylin将会为每个HTable Region分配2GB来创建一个HTable Region。当Segment中一些Cuboid的大小总和超出一定的阈值时,系统会将这些Cuboid的数据分片到多个分区中以实现Cuboid数据读取的并行化,从而优化Cube的查询速度。

类型的配置还有:kylin.hbase.region.count.minkylin.hbase.region.count.max 决定每个Segment少或多被划分成多少个分区。

后再单击“Save”按钮进行保存,一个Cube就设计完成了。


四.Cube的构建

Cube的构建方式通常有两种:全量构建和增量构建;两者的构建步骤是完全一样的,区别只在于构建时读取的数据源是全集还是子集

Cube的构建是如何由任务引擎来调度执行的?

  • 创建临时的Hive平表(将数据从源Hive表提取出来,和所有join的表一起,并插入到一个中间平表)

  • 重新分配平面表(解决不平衡的文件分布导致之后的MR任务出现数据倾斜的问题)

  • 创建事实表的dictinct columns文件:计算每一个出现在事实表中的维度和度量的dictinct值

  • 构建维度字典。

  • 保存Cuboid的统计数据

  • 创建HTable

  • 构建Basic Cuboid

  • 构建N维Cuboid

  • 基于内存构建Cube

  • 将Cube的计算结果转成HFile

  • 加载HFile到HBase

  • 更新Cube元数据

  • 垃圾回收,清理构建过程中生成的临时文件等垃圾,释放集群资源。


构建N维Cuboid的详细说明

构建N维Cuboid的过程,每一步以前一步的输出作为输入,然后去掉一个维度以聚合得到一个子Cuboid。举个例子,Cuboid ABCD去掉A得到BCD,去掉B得到ACD。

有些Cuboid可以从一个以上的父Cuboid聚合得到,这种情况下,Kylin会选择小的一个父Cuboid。举例,AB可以从ABC(id:1110)和ABD(id:1101)生成,则ABD会被选中,因为它的比ABC要小。在这基础上,如果D的基数较小,聚合运算的成本就会比较低。所以,当设计rowkey序列的时候,请记得将基数较小的维度放在末尾。这样不仅有利于cube构建,而且有助于cube查询,因为预聚合也遵循相同的规则。

通常来说,从N维到(N/2)维的构建比较慢,因为这是Cuboid数量爆炸性增长的阶段:N维有1个cuboid,(N-1)维有N个cuboid,(N-2)维有N*(N-1)个cuboid,以此类推。经过(N/2)维构建的步骤,整个构建任务会逐渐变快。


五.Cube的剪枝优化

一般来说,Cube的膨胀率应该在0%~1000%之间,通常,膨胀率高有以下几个方面的原因:

  • Cube中的维度数量较多,且没有进行很好的Cuboid剪枝优化,导致Cuboid数量极多

  • Cube中存在较高基数的维度,导致包含这类维度的每一个Cuboid占用的空间都很大,这些Cuboid累积造成整体Cube体积变大。

  • 存在比较占用空间的度量,如Count Distinct,因此需要在Cuboid的每一行中都为其保存一个较大的寄存器


剪枝优化策略如下所示:

1.使用衍生维度

衍生维度用于在有效维度内将维度表上的非主键维度排除掉,并使用维度表的主键(其实是事实表上相应的外键)来替代它们。Kylin会在底层记录维度表主键与维度表其他维度之间的映射关系,以便在查询时能够动
态地将维度表的主键“翻译”成这些非主键维度,并进行实时聚合。

例如:
原始组合:ABC,AB,AC,BC,A,B,C
当定义B维度从A维度衍生时的组合:AC,A,C

可见从7种组合变成了3种组合。

假设原始的维度表这样定义


A B C
1 a ?
2 b ?
3 c ?
4 b ?

如果我们想根据B维度来进行查询,如select count(*) from tbl inner join lookup_tbl group by lookup_tbl.B
则Kylin会对该查询进行优化,使其由A维度进行分组。

A COUNT(*)
1 1
2 1
3 1
4 1

之后根据B维度从A维度衍生出来的映射关系,将A替换为B,则如下所示

B COUNT(*)
a 1
b 1
c 1
b 1

后会对以上结果进行聚合操作,如下所示

B COUNT(*)
a 1
b 2
c 1

因此,如果从维度表主键到某个维度表维度所需要的聚合工作量非常大,那么定义一个普通的维度可能是一种更好的选择。


2.使用聚合组

聚合组假设一个Cube的所有维度均可以根据业务需求划分成若干组。每个分组的维度集合均是Cube所有维度的一个子集,不同的分组各自拥有一套维度集合,它们可能与其他分组有相同的维度,也可能没有相同的维度。构建引擎会保证每一个Cuboid无论在多少个分组中出现,它都只会被物化一次。


通过使用多个聚合组,可以大大降低Cube中的Cuboid数量。下面来举例说明,如果一个Cube有(M+N)个维度,那么默认它会有2^(m + n) 个  Cuboid;如果把这些维度分为两个不相交的聚合组,那么Cuboid的数量将被减少为2^m + 2^n。


在单个聚合组中,可以对维度设置属性,如Mandatory、Hierarchy、Joint等。这几种属性都是为优化Cube的计算而设计的。


Mandatory  必要维度,总是出现的维度。指的是那些总是会出现在Where条件或Group By语句里的维度;通过将某个维度指定为Mandatory,此聚合组产生的所有Cuboid中每一个Cuboid都会包含该维度,Kylin就可以不用预计算那些不包含此维度的Cuboid,从而减少计算量。


Hierarchy  层级维度,例如 “国家” -> “省” -> “市” 是一个层级;不符合此层级关系的 cuboid 可以被跳过计算,例如 [“省”], [“市”]. 定义层级维度时,将父级别维度放在子维度的左边。通过指定Hierarchy,Kylin可以省略不满足此模式的Cuboid。假设一个层级中包含D1,D2…Dn这n个维度,那么在该分组产生的任何Cuboid中,这n个维度只会以(),(D1),(D1,D2)…(D1,D2…Dn)这n+1种形式中的一种出现。


Joint 联合维度,其通常适用于如下两种情形。总是会在一起查询的维度,基数非常接近(有1:1映射关系)。如果某些列形成一个联合,那么在该分组产生的任何Cuboid中,这些联合维度要么一起出现,要么都不出现。


高基数维度使用聚合组控制Cube的膨胀率的思想?
高基数维度的Cuboid在行数和体积上往往非常庞大,这会导致整个Cube的膨胀率变大。如果根据业务需求知道这个高基数的维度只会与若干个维度(而不是所有维度)同时被查询到,那么就可以通过聚合组对这个高基数维度做一定的“隔离”。


把这个高基数的维度放入一个单独的聚合组,再把所有可能会与这个高基数维度一起被查询到的其他维度也放进来。


这样,这个高基数的维度就被“隔离”在一个聚合组中了,所有不会与它一起被查询到的维度都没有和它一起出现在任何一个分组中,因此也就不会有多余的Cuboid产生。这点也大大减少了包含该高基数维度的Cuboid的数量,可以有效地控制Cube的膨胀率。

                   

本文作者:叫我不矜持

文章来源:https://www.jianshu.com/p/7906f428aaec


— THE END —


相关文章