北京顺丰同城科技|末端物流场景下的 Kylin 优化

北京顺丰同城科技
2022年 2月 09日
01 背景

随着顺丰末端物流(末端物流主要分为对小哥、柜机、区域等的资源的管理和分批;对路径、排班、改派等信息的实时调度和规划)业务的发展,越来越多的末端物流数据接入了顺丰大数据平台。末端物流业务作为顺丰主要的物流科技载体,不仅涉及的业务场景较广,而且对后端服务的要求更高:亿级数据量下数据统计灵活维度组合下的数据查询查询服务响应要求亚秒级要求服务 24 小时响应等。在这样的业务背景下,传统的 TiDB 和 Impala 查询不能满足业务需求,使用稳定高效的 OLAP 引擎是最佳的选择。

Apache Kylin 是 Hadoop 大数据平台上的一个开源的联机分析处理(OLAP)引擎。它采用多维立方体预计算技术,利用空间换取了时间,将大数据的 SQL 查询速度从之前的分钟乃至数小时级别提升到亚秒级别,在复杂的查询场景中表现优秀。随着末端物流业务的发展,末端物流业务指标数量日益增长。公司大数据平台接入 10 个数据主题,包括 200+ 个指标,5 个数据报表产出。数据仓库维护 400+ 的 Hive 表,单表日统计数据量约 200G,涉及日、周、月、年的时间维度统计,支持 AOI、AOI 区域、GUID、网点、小哥等多个维度组合的查询,涵盖区域直管、AOI 区域数据库、热力图、AOI 区域变更、路径规划、外部资源、产品板块等功能模块。


目前,经过我们数据团队一年多的建设,Kylin 可以做到:

  • 线上业务查询响应保证在 500ms 以内;
  • Cube 模型构建时间控制在 1.5h 内,减少构建占有集群资源时间;
  • 存储膨胀率低于 500%,节省约 3 倍的磁盘资源。
02 技术难点及目标

末端物流业务场景一般有以下特点:

  1. 数据量大,单表单日数据量级在百万级;
  2. 查询维度组合多,例如 AOI、AOI 区域、GUID、网点、分部、大区、小哥等多维度随机组合;
  3. 查询时间跨度大,查询时间范围从1天到3个月不等,用户可以自由选择时间进行数据查询;
  4. 服务响应要求较高,需要在亚秒级响应请求。
图 1-1 查询量
图 1-2 并发量

考虑到上述难点,顺丰末端物流业务场景下的数据侧目标为:

  1. 满足业务场景的情况下更高效响应用户请求;
  2. 用更短的时间和更少的资源构建出 Cube;
  3. 使用更小的磁盘存储更多的数据。
03 Kylin 组件介绍

Kylin 的优势

1. 基于标准 SQL 的接口查询

Kylin 选择使用标准 SQL 接口作为对外服务的主要接口,对于绝大多数的数据开发者来说,SQL 是最熟悉的工具,并且简单易用。

2. 支持超大数据集的查询

Kylin 在末端物流的实际场景中支持上亿条数据的亚秒级查询,在其他公司的实践中更是支持千亿条记录秒级查询。由于 Kylin Cube 模型的引入,理论上 Kylin 可以支持的数据集大小是没有上限的,其仅受限于存储系统和分布式计算系统的承载能力,并且查询速度不会随着数据集的增加而减慢。

3. 亚秒级响应速度

Kylin 使用的预计算的思想,将很多复杂的计算比如 join、group by、count 等操作在预计算的过程中已经完成了,这就降低了查询时需要的计算量,也就相应的提升了查询的响应速度。通过对具体数据场景下 Kylin 的性能调优、模型优化,就能达到 Kylin 的亚秒级数据响应。

4. 可伸缩性和高吞吐率Kylin 有着良好的可伸缩性和很高的吞吐率,图 3-1 为《Apache Kylin 权威指南》中对 Kylin 的性能测试:

  • 左图为 Kylin 和 Mondrian/Oracle 的查询速度对比,可以看出 Kylin 的查询速度比 Mondrian/Oracle 快 147 倍、314 倍和 59 倍;
  • 右图表现了 Kylin 实例和吞吐率之间的关系,一个 Kylin 实例中可以处理近 70 个查询,并且随着服务器的增加,吞吐率也呈线性增加。
图 3-1

Kylin 工作原理

Kylin 本质就是对数据模型做 Cube 计算,利用计算的结果提高查询的效率。Kylin 会根据用户定义的数据模型、数据维度和度量,对数据进行预计算,并将计算所有的 Cuboid 进行保存。执行查询时通过读取 Cuboid,对数据进行加工产出。Kylin 的查询过程不会扫描原始的记录,而是利用预计算的结果执行查询。在超大数据集上,这种查询方式比非预计算的技术在查询速度上会高出多个数量级。

Kylin 架构

Kylin 的系统架构图如图 3-2 所示。其分为上半区的在线查询部分和下半区离线构建部分。

图 3-2

离线构建部分中 Kylin 可以从 Hadoop、Hive 等数据源中抽取符合星型模型和雪花模型的数据表中的数据构建 Cube,并将构建的 Cube 保存在 HBase 中(HBase 是 Kylin 3 默认的存储引擎)。

实时部分由外部发起,通过 REST API 或 JDBC 的形式,将 SQL 提交到 REST Server 层中,再转交到查询引擎中进行处理。查询引擎通过解析 SQL,生成基于关系表的逻辑执行计划,然后转译为基于 Cube 的物理执行计划,最终查询存储在 HBase 中预计算好的 Cube 结果。

整个的查询过程对开发者和使用者来说是很友好的,由于使用了 SQL 语法,屏蔽了直接操作 Cube,降低了使用成本。

Kylin 关键词介绍

  • 维度(Dimension):维度是观察数据的角度。图3-3是顺丰末端物流场景 AOI 维度宽表的部分记录,图中是按照 AOI 编码和网点统计当日收件量。AOI 编码和网点编码就是维度。
图 3-3
  • 度量(Measure):度量是被聚合的统计值。如图3-3中的例子,收件量就是度量。
  • 超高基列(Ultra High Cardinality, UHC):通常是指行数多余100万的维度,如图3-3中的例子,末端物流业务aoi维度宽表中aoi编码每日数量约在400万左右,该列就是一个超高基列。
  • 数据立方体(Cube):一组用于分析数据的相关度量值和维度,是所有 Cuboid 的集合,作为存储和分析的基本单位。
  • Cuboid:某一维度组合下,度量聚合后的结果集合。
  • Cube Segment:针对数据源中的某一片度计算出来的 Cube 数据。

Cube 计算原理(by-layer)

Kylin 会对每一种维度的组合进行聚合预计算,如图3-4所示。每一种维度排列组合的预计算结果就是一个 Cuboid。

图 3-4

图 3-4 中 A、B、C、D 是 4 种计算维度,总共有24=16 种组合维度,最终会生成 16 个 Cuboid。在逐层算法中,按照维度数逐层减少来计算,每个层级的计算(除了第一层,由原始数据聚合而来),是基于上一层级的计算结果来计算的。例如:group by [A,B] 的结果,可以基于 group by [A,B,C] 的结果,通过去掉 C 后聚合得来的,这样可以减少重复计算,当 0 维 Cuboid 计算出来的时候,整个 Cube 的计算也就完成了。

04 优化方案及末端物流场景下的应用

Kylin 的核心思想是将用户定义好的数据模型和查询样式进行预计算,在查询时使用预预计算结果进行结果返回。但是预计算是需要成本的,如果我们可以根据具体场景跳过预计算多余的步骤,减少预计算的工作量,最终减少存储 Cube 的数据大小,构建出体积更小、查询速度更快的 Cube,因此 Kylin Cube 的优化是必要的。

Kylin 优化方式

我们以末端物流场景下的 AOI 维度结果宽表为例进行说明,AOI 维度结果宽表包含 AOI 编码、网点、大区、时间等12个维度字段,收件量、派件量等43个度量字段。根据该表构建无优化的 Kylin Cube planner 如下:

图 4-1

1 Cuboid 剪枝优化

由于 Kylin 使用 By-layer(第3节中介绍)的方式进行维度的构建,当构建的维度较多时,生成 Cuboid 的数量将非常庞大,因此我们最常见的 Kylin 优化方式就是 Cuboid 的剪枝优化。

1. 聚合组(Aggregation Group)

聚合组是 Kylin 优化最常用的一种优化方式,其主要是将一个 Cube 的所有维度根据业务需求划分成一个/若干组,同一组内的维度更可能同时被同一查询用到,不同组内的维度几乎或极少会被同一查询用到。每个分组的维度集合是 Cube 的所有维度的一个子集,分组之间可能有相同的维度,也可能完全没有相同的维度。例如末端物流  AOI 维度大宽表,统计收件总量指标,按照 AOI 和网点维度每日统计时,会产生以下聚合组:

聚合组1:[aoi_id ,inc_day]

聚合组2:[dept_code,inc_day]

如果未主动设置聚合组,得到的 Cuboid 的结构如下:

图 4-2

主动设置聚合组后,得到的 Cuboid 结构如下:

图 4-3

不难看出,通过聚合组的优化,少去了 aoi_id 和 dept_code 的组合维度,减少了不必要的 Cuboid 的构建。

基于该种优化方式,我们将查询组合进行了聚合,按照聚合组的方式构建 Cube。将 aoi_id,dept_code 等查询组合方式进行了聚合,最终聚合成 aoi_id 和 dept_code 两种聚合组进行构建。

2. 层级维度

如果维度之间有层级关系,如末端物流 AOI 宽表中大区、地区、网点维度,我们可以设置层级维度实现 Cuboid 的优化。

一般查询会的组合一般是分部、地区、大区、网点,其具有层级关系,且不会存在有[分部、网点]等这类组合。因此我们可以设置[分部、地区、大区、网点]作为层级维度,Cuboid 构建时只会保留如[分部]、[分部、地区]、[分部、地区、大区]、[分部、地区、大区、网点]这样的组合方式,将 Cuboid 的个数从16个减少到4个。

层级维度的适用场景主要是一对多的层级关系,例如地域层级、机构层级、渠道层级、产品层级等等。

3. 联合维度

联合维度一般用在同时查询几个维度的场景,它往往能将 Cuboid 的总数降低几个数量级。如末端物流 AOI 宽表中,用户通常会联合查询 dept_code、hq_code、 area_code、 division_code 这些维度,因此我们可以将这些维度进行组合并定义为联合维度,Kylin 构建是就仅仅只会构建 Cuboid[dept_code,hq_code,area_code,division_code],而 Cuboid[dept_code,hq_code][dept_code,area_code]…..[division_code]等就不会被生成,减少了非必要的 Cuboid 构建。

联合维度的适用场景:

  1. 维度经常访问同时在查询 where 或 group by 条件中同时出现,甚至本来就是一一对应关系的,如网点 code 和网点名称,它们就可以组成一个联合维度;
  2. 将若干个低基数的维度或很少使用的若干维度合并成一个联合维度,可以大大减少 Cuboid 的数量,利用在线计算能力,虽然查询时会消耗一定的查询时间,但相比能减少存储空间和构建时间而言,是值得的;
  3. 必要时可以将两个有强关系的高基维度组成一个联合维度,如合同日期和入账日期。

4. 使用衍生维度

Kylin 可以将一个维度表上的维度设置为衍生维度,则这个维度不会参与预计算,而是使用维度表的主键来代替它。Kylin 会在底层记录维度表主键与维度表其他维度之间的映射关系,以便所在查询时能够动态地将维度表的主键“翻译”成这些非主键维度,并进行实时聚合。

5. 必要维度

如果某个维度是查询时 where 或 group by 中的必要条件,那这个维度可以被设置为必要维度。这样设置会在构建 Cuboid 时所有 Cuboid 都会带有这个维度,Cuboid 的数据将会减少一半。

6. 并发粒度优化

Kylin 底层是使用 segment 的方式来存储 Cuboid 的,当一个 segment 的存储大小不能存储一个 Cuboid 时,会切分该 Cuboid 到多个分区中,来实现 Cuboid 的读取并行,从而提高 Cube 的查询效率。我们使用 HBase 作为 Kylin 的存储引擎,因此可以通过设置参数“kylin.hbase.region.cut”定义使用分区的个数;通过设置“kylin.hbase.region.count.min”和“kylin.hbase.region.count.max”来设置最少和最多被划分为多少个分区。

2 Rowkey 优化


Apache Kylin 使用 HBase 做为 Cube 的存储引擎,Kylin 将多个维度以用户设置的次序组成 Rowkey,排在 Rowkey 靠前部分的维度,将比排在靠后部分的维度更易于做筛选,因此查询效率更高。除了各维度在 Rowkey 上的次序外,维度的编码方法对于空间占用及查询性能也有着显著的影响。因此可以通过优化 Rowkey 的方式来优化 Kylin Cube 的查询效率。

1. Rowkey 顺序调整

Kylin 会把所有维度按照用户编辑的顺序粘合成一个完整的 Rowkey,并且按照这个 Rowkey 升序排列 Coboid 中的所有行,因此我们可以按照以下规则调整 Rowkey 的顺序实现 Rowkey 的优化。

  • 用做过滤条件的维度应该放在前面;
  • 将经常出现在查询中的维度放在不经常出现的维度前面;
  • 对于不会出现在过滤条件中的维度,将高基维度放在低基维度前面。

2. 维度编码调整

Kylin 为了统一维度中的数值,会通过编码的格式对维度进行优化。维度的编码不仅可以压缩信息的存储空间,并且可以提高扫描的效率,减少解析开销。因此我们可以通过数据的特征调整 Rowkey 的编码格式,从而减少数据的存储空间,提高 Rowkey 的查询效率。

3. 按照维度分片

Kylin 会对 Cuboid 中的数据进行分片存储,默认情况下是对所有列进行哈希计算后随机分配的。但是当查询 group by 的结果中命中了某个 Cuboid,查询结果中多条数据由于被哈希到不同机器上存储,这样就需要将数据查询到存储引擎中,再进行逻辑聚合。Kylin 支持按照维度进行分片,将某个高基维度设置为分片维度后,Kylin 会根据该值分发数据,拥有相同值的行会被分发到同一个文件,这样做不仅重新分布了数据,并且在查询时相同维度的数据查询效率也会有一定的提升。

优化结果

综合以上优化方式,我们采用了聚合组+联合维度+层级维度的方式对 Cuboid 进行了优化,并且优化了 Rowkey 的顺序和分片使得kylin的存储更加合理,提升了 Kylin 的查询效率。

1.  Cuboid 数量减少

图 4-4

如图 4-4 所示,Cuboid 数量减少了 90.6%。

2.  构建效率提升

表 4-1

通过表 4-1 对比可明显看出,优化前的构建时间约在 500min 左右,优化后构建时间约 150min 左右,Cube 构建时间优化提升 70%。

3.  查询时间缩短

表 4-2

通过表4-2对比可以看出,经过 Rowkey 优化后,优化前2021年03月15日大于 5s 的查询量约为 5000 左右,占总查询量的 0.25%;优化后2021年03月20日大于 5s 的查询量约为 300 左右,占总查询量的 0.015%。在不考虑并发压力的情况下,查询量大于 5s 的查询减少 94%。

05 未来展望

由于现有 Kylin 的构建还是基于离线的 MapReduce 进行构建,无法满足分钟级的数据实时更新。Kylin 提出了流式构建的解决方案。未来可以通过搭建 Kafka 数据源到Kylin 的流式构建架构,达成 Kylin 的分钟级数据更新。

切换 MapReduce 构建方式到 Spark 的方式,摒弃 MapReduce 引擎,消除在多轮任务计算时多次 MR 消耗网络传输和磁盘 I/O 的问题,使用 Spark 引擎依靠 RDD 的方式使用内存进行计算,增加中间数据的复用性,减少 Kylin 构建时间。