业务的发展伴随着事实表的数据体量的增大,查询时间也越来越长,而另一方面,结果集的数据量却没有显著增长,指标的变化也并不频繁。我们希望所有的查询都能在秒级返回查询结果,于是,我们有了一个思路:为什么不能把所有的结果先算好,查询时直接从结果集中取出来呢?这也就是 kylin 设计的初衷。 kylin的核心思想是预计算。 好,现在这个组件已经有了一个准确的定位,那么接下来要解决的问题就是如何实现预计算,这套架子里究竟需要什么组件呢?
以下即为 kylin 的插件式架构。 上图中,蓝色的框框是 kylin 系统的边界,里面是 kylin 的核心功能,蓝色框框外面的部分是 Kylin 的外围插件和适配器。
从模块上来看,大致分为以下几部分:
Data Source Abstraction:数据源抽象层,目前离线的主流实现是 hive,实时的主流实现是 kafka。Cube Builder:也就是构建引擎,目前支持 MapReduce 和 Spark。Storage Abstraction:结果集存储抽象层,目前主流实现是 hbase,也可以以数据流的方式输出到其他大数据生态工具。Metadata:元数据管理,包括 model,cube,job 等元数据的信息,由于 Kylin 的结果表名和 rowkey 都是经过编码的,必须要通过元数据来映射,所以元数据非常关键,若元数据损坏,相对应的结果集也会不可用,表现为表损坏或查询时报错。Rest Server:通过 Restful API,JDBC/ODBC 的方式向外界提供接口。Query Engine:查询引擎,内置 Apache 开源 SQL 解析工具 Calcite,生成执行计划并执行物理查询计划。Routing:路由转换,实时查询情景下,路由指向数据存储层 Hbase,准实时查询情景下,路由指向数据源层 Hive,准实时情景一般是在结果集的数据满足不了查询需求时,Kylin会转而向数据源做查询,也就是所谓的“查询下压”。从数据流上来看,大致分为以下几部分:
红线:离线任务数据流,从数据源 Hive 读取数据,经计算引擎 MapReduce 或 Spark 计算,生成结果集并存储到 Hbase,离线构建任务可以定时,构建任务可通过 Rest API 定时调度。绿色实线:实时查询数据流,从客户端通过 Rest API 或 JDBC/ODBC,经 Rest Server 和 Query Engine 处理,由路由转发至 Hbase,秒级返回查询结果。绿色虚线:准实时查询数据流,查询下压时,通过数据源直接汇总出查询结果,性能视计算复杂程度而定。接下来,我们就要涉及到 Cube 构建的一些概念啦~~
那么,万事俱备,我们开始构建 Cube 吧~
构建Cube的过程设计可以说是 kylin 系统设计的精华,这7个步骤封装了 kylin 绝大多数的实现细节,呈现出来的是简洁精炼的构建步骤。 上图中,有7个步骤,可以分为三个阶段:前四步为构建阶段,5、6步为优化阶段,最后一步为总览。下面我们一个一个详细说一下。
在这一步中,我们需要设置的是 Cube 的基本信息,其中包括:
Model Name:model 名。Cube Name: Cube 名,全局唯一,否则会创建失败。Notification Email List:发生了 Notification Events 时要通知的邮件列表,逗号分隔。Notification Events:发生什么事件时要邮件通知列表中的用户,分为 Success(构建成功)、Error(构建失败)、Discard(构建中止)。Description:描述。设置 Cube 所需的维度,查找表中的维度可以设置为 normal 或 derived,设置为 derived 的维度不会在 rowkey 中出现,而是用其他 normal 维度通过一层映射得到。
这一步骤主要选择 Cube 中要构建的指标。 所有 Cube 都会计算一个默认的指标 COUNT,即数行数,除此之外,我们还可以定义其他指标:
SUM:求和。MIN:最小值。MAX:最大值。COUNT:计数。COUNT_DISTINCT:去重计数,有精确计算和模糊计算两种。一般来说,计算越精确,构建时资源消耗越多,查询时响应速度越慢;计算越模糊,构建时资源消耗越少,查询时响应速度越快。精确计算的 COUNT_DISTINCT 会存储为 bitmap,模糊计算的 COUNT_DISTINCT 会存储为其他的数据结构(如HyperLogLog),无论是哪种数据结构,都会占用一定的存储空间,若数据量大,占用的空间可能会在M级别,所以,若有多个去重指标,最好把它们存储在不同的列簇中,防止 Hbase 检索速度过慢。TOP_N:前 N 位。适用于以下场景: select dimA, measure from table_name where dimB = 'b' and dimC = 'c' order by measure limit N;在不用 TOP_N 的情况下,查询引擎会从 Hbase 中查出所有符合条件的行,并依据其中的某个指标对所有列做排序。例如下面的场景,经过 TOP_N 优化后,Hbase 中数据存储的形式会从这样:
Base Cuboid 的 RowKeySUM(PRICE)20120218_00_seller0000001291.5820120218_00_seller0000002365.1820120218_00_seller0000003135.29……20120218_00_seller1000000272.3120120218_01_seller0000001172.52变成这样:
base cuboid 的 RowKeyTop_N Measure20120218_00sellerOO 10091:1092.21, seller0005002:1090.35,…sellerOOO 1789:891.3720120218_01seller0003012:xx.xx20120218 02seller0004001:xx.xx……20120218 50seIler000699: xx.xx经 TOP_N 优化后存储结构节省了检索的计算成本,免去了排序的计算成本,可提高查询性能,但带来的问题是列簇的空间占用增加,Cube 的体积会变大。且子 Cuboid 的 TOP_N 指标是从父 TOP_N 的指标聚合出来的,会有一定程度的误差。 7) EXTENDED_COLUMN:引申列。适用于筛选时指定某些 id,但显示时却要显示成 name 时的场景,即把一个维度作为另一个维度的引申列。(后续补充引申列的查询方法和存储结构) 8) PERCENTILE:分位数。查询时该函数的语法与 hive 类似,例如 percentile(measure_a, 0.5),即计算 measure_a 的二分位数,即中位数。
这一步骤主要是表的刷新机制和生命周期设置,分为以下几项:
Auto Merge Thresholds:自动合并 Segments,可以设多级。若不合并,查询引擎需要在对各 Segments 查询后,对结果做一个合并。所以合并 Segment 在一定程度上可以提高查询效率。但是 Segment 是 Cube 构建和刷新的最小单位,合并后就无法对原有的某一个 Segment 单独做刷新操作。Volatile Range:最近 N 天的 Segments 不作合并,适用于因数据延迟导致最近 N 天数据会有波动,需要刷新 Segments 的情况。Retention Threshold:生命周期,自动删除 N 天以前的 Segment。Partition Start Date:起始日期,默认 1970-01-01。前四步走完,相当于 Cube 的构建步骤基本完成了,以下的步骤,更确切地说是对 Cube 的优化,以提升构建速度或查询性能。
Aggregation Groups 直译过来是聚合组。就是指定 Cube 中哪些维度会同时出现,若 Cube 中有两个维度没有同时出现在任何一个聚合组中,Cube 构建时就不会生成同时包含这两个维度的 Cuboid,聚合组是一个强大的剪枝工具。主要包含以下几项:
Max Dimension Combination:查询时会涉及到的最大维度个数。设置后,多于该维度个数的 Cuboid 将不会被构建。Includes:聚合组里包含哪些维度。Mandatory Dimensions:在该聚合组中,有哪些维度是查询时必选的。Hierarchy Dimensions:哪些维度之间有层级关系,如 国家 -> 省 -> 市。Joint Dimensions:哪些维度之间几乎是一对一的。 注意:若某维度被设置为 Mandatory Dimensions,该维度将不允许出现在 Hierarchy Dimensions 或 Joint Dimensions 中。rowkey 的优化分为以下几个方面:
Hbase 中 rowkey 的维度顺序。《Apache Kylin 权威指南》中给出了一个维度评分的公式,维度评分越高,越应该放在前排: 维度打分 = 维度出现在过滤条件中的概率 * 用该维度过滤时可以过滤掉的记录数维度编码。维度编码有以下几种: a) boolean:布尔型。 b) dict:字典型,一般把字符串映射为整型,rowkey 中存储映射后的整型数值,字典在运算时会加载到内存中,若维度基数过高,可能会导致内存溢出。此时应考虑换成其他数据编码。 c) fixed_length:定长,若维度值长度不等,编码时会对其进行截断或补位,适用于电话号码等定长维度。 d) fixed_length_hex:16进制定长编码,用途尚不明确。 e) integer:整型编码。
是否按某维度分片。一般来说,数据分片策略可近似地认为是随机的,若设置时按某维度进行分片,可能会提高查询效率,但如果这个分片的维度基数过高,查询时可能会造成 Hbase 过大的压力,甚至会影响到 Kylin 的可用性。此优化项需谨慎使用。
官方推荐单台 Hbase 机器的 region 数量在 100 个左右,若超过太多,可能会影响查询性能。必须出现的 Cuboid,支持文件导入。 还有一种方式可以指定要构建的 Cuboid,就是把这个 Cuboid 中出现的所有维度放到一个聚合组里,再把这些维度全部设置为 Mandatory Dimensions。
目前有两种 Cubing Engine 可供选择:MapReduce 和 Spark,前者速度慢,但稳定,后者速度快,但有内存占用过高,导致系统不稳定的风险。 一般来说,若有 COUNT_DISTINCT 指标,我们会优先考虑稳定性,选择 MapReduce,否则我们优先考虑速度,选择 Spark。
一般来说,COUNT_DISTINCT 指标会单独放到一个列簇里面,其他指标会放到另一个列簇里面。若同时有多个 COUNT_DISTINCT 指标,需要根据每个指标列占用空间的大小,评估是否要把不同的 COUNT_DISTINCT 指标放到不同的列簇当中。
其他自定义配置项,此处省略。
一个足够优化的 Cube,需要包含常查询的 Cuboid,删除不常查询的 Cuboid,即在满足查询性能的前提下,占用的空间足够小,只物化高频查询的 Cuboid。基于这一点,我们有一个指标,称为膨胀率。
膨胀率 (Expansion Rate) = Cube数据大小 / 源数据大小膨胀率一般在0 ~ 1000%,若超过这个阈值,这个 Cube 很可能需要优化。例如下面这个 Cube: 膨胀率已超过1000%,所以我们要对其进行优化。
在 Cube 已经构建好以后,又如何进行优化呢? 这时我们就要用到 Cube Planner 这个工具啦,Cube Planner 是内嵌在 Kylin 中的一个剪枝工具,可以统计出每个 Cuboid 的命中次数,进而把各个 Cuboid 做冷热分级,Cube Planner 的统计结果会可视化成下面这张旭日图: 上图中,左边是优化前的 Cube,我们会发现,这个 Cube 之所以膨胀率会超过1000%,是因为它物化了很多从来没被使用过的 Cuboid,右边是推荐的 Cuboid 组合,我们发现 Cube Planner 在优化后会把没被命中过的 Cuboid 删除,优化 Cube 在空间上的占用,还会新增一些 Cuboid,在时间上优化 Cube 的查询性能。 鼠标悬浮在每个 Cuboid 上时,Kylin 还会显示出该 Cuboid 的相关信息:
Name:各维度是否出现,按 rowkey 排序。ID:cuboid ID。Query Count:命中数,也包括子 Cuboid 没被物化时,由此 Cuboid 计算得出的查询命中数。Exactly Match Count:精确命中数。Row Count:行数。Rollup Rate:子cuboid行数 / 父cuboid行数。