前面介绍了如何构建Cube并利用其完成在线多维分析的查询。每次Cube的构建都会从Hive中批量读取数据,而对于大多数业务场景来说,Hive中的数据处于不断增长的状态。为了支持Cube中的数据能够不断地得到更新,且无需重复地为已经处理过的历史数据构建Cube,因此对于Cube引入了增量构建的功能。
我们将Cube划分为多个Segment,每个Segment用起始时间和结束时间来标志。Segment代表一段时间内源数据的预计算结果。在大部分情况下(例外情况见第4章“流式构建”),一个Segment的起始时间等于它之前那个Segment的结束时间,同理,它的结束时间等于它后面那个Segment的起始时间。同一个Cube下不同的Segment除了背后的源数据不同之外,其他如结构定义、构建过程、优化方法、存储方式等都完全相同。
本节将首先介绍如何设计并创建能够增量构建的Cube,然后介绍实际测试或生产环境中触发增量构建的方法,最后将会介绍如何处理由于增量构建而导致的Segment碎片,以保持Kylin的查询性能。
1.为什么要增量构建
全量构建可以看作增量构建的一种特例:在全量构建中,Cube中只存在唯一的一个Segment,该Segment没有分割时间的概念,因此也就没有起始时间和结束时间。全量构建和增量构建各有其适用的场景,用户可以根据自己的业务场景灵活地进行切换。全量构建和增量构建的详细对比如表3-1所示。
对于全量构建来说,每当需要更新Cube数据的时候,它不会区分历史数据和新加入的数据,也就是说,在构建的时候会导入并处理所有的原始数据。而增量构建只会导入新Segment指定的时间区间内的原始数据,并只对这部分原始数据进行预计算。为了验证这个区别,可以到Kylin的Monitor页面观察构建的第二步——创建Hive中间表(Create Intermediate Flat Hive Table),单击纸张形的LOG按钮即可观察该步骤的参数:
INSERT OVERWRITE TABLE
kylin_intermediate_test_kylin_cube_without_slr_left_join_desc_20120601000000_20130101000000 SELECT
TEST_KYLIN_FACT.CAL_DT
,TEST_KYLIN_FACT.LEAF_CATEG_ID
,TEST_KYLIN_FACT.LSTG_SITE_ID
,TEST_CATEGORY_GROUPINGS.META_CATEG_NAME
,TEST_CATEGORY_GROUPINGS.CATEG_LVL2_NAME
,TEST_CATEGORY_GROUPINGS.CATEG_LVL3_NAME
,TEST_KYLIN_FACT.LSTG_FORMAT_NAME
,TEST_KYLIN_FACT.SLR_SEGMENT_CD
,TEST_KYLIN_FACT.PRICE
,TEST_KYLIN_FACT.ITEM_COUNT
,TEST_KYLIN_FACT.SELLER_ID
,TEST_SITES.SITE_NAME
FROM DEFAULT.TEST_KYLIN_FACT as TEST_KYLIN_FACT
LEFT JOIN EDW.TEST_CAL_DT as TEST_CAL_DT
ON TEST_KYLIN_FACT.CAL_DT = TEST_CAL_DT.CAL_DT
LEFT JOIN DEFAULT.TEST_CATEGORY_GROUPINGS as TEST_CATEGORY_GROUPINGS
ON TEST_KYLIN_FACT.LEAF_CATEG_ID = TEST_CATEGORY_GROUPINGS.LEAF_CATEG_ID AND TEST_KYLIN_FACT.LSTG_SITE_ID = TEST_CATEGORY_GROUPINGS.SITE_ID
LEFT JOIN EDW.TEST_SITES as TEST_SITES
ON TEST_KYLIN_FACT.LSTG_SITE_ID = TEST_SITES.SITE_ID
LEFT JOIN EDW.TEST_SELLER_TYPE_DIM as TEST_SELLER_TYPE_DIM
ON TEST_KYLIN_FACT.SLR_SEGMENT_CD = TEST_SELLER_TYPE_DIM.SELLER_TYPE_CD
WHERE (TEST_KYLIN_FACT.CAL_DT >= '2012-06-01' AND TEST_KYLIN_FACT.CAL_DT < '2013-01-01')
distribute by rand();
该构建任务对应于名为test_kylin_cube_without_slr_left_join_empty的Cube构建,其Seg-ment所包含的时间段为从2012-06-01(包含)到2013-01-01(不包含),可以看到在导入数据的Hive命令中带入了包含这两个日期的过滤条件,以此保证后续构建的输入仅包含2012-06-01到2013-01-01这段时间内的数据。这样的过滤能够减少增量构建在后续的预计算中所需要处理的数据规模,有利于减少集群的计算量,加速Segment构建的时间。
其次,增量构建的Cube和全量构建的Cube在查询时也有不同。对于增量构建的Cube,由于不同时间的数据分布在不同的Segment之中,因此为了获得完整的数据,查询引擎需要向存储引擎请求读取各个Segment的数据。当然,查询引擎会根据查询中的条件自动跳过不感兴趣的Segment。对于全量构建的Cube,查询引擎只需要向存储引擎访问单个Segment所对应的数据,从存储层返回的数据无需进行Segment之间的聚合,但是这也并非意味着查询全量构建的Cube不需要查询引擎做任何额外的聚合,为了加强性能,单个Segment的数据也有可能被分片存储到引擎的多个分区上(参考第6章),从而导致查询引擎可能仍然需要对单个Segment不同分区的数据做进一步的聚合。当然,整体来说,增量构建的Cube上的查询会比全量构建的做更多的运行时聚合,而这些运行时聚合都发生在单点的查询引擎之上,因此通常来说增量构建的Cube上的查询会比全量构建的Cube上的查询要慢一些。
可以看到,日积月累,增量构建的Cube中的Segment越来越多,根据上一段的分析可以猜测到该Cube的查询性能也会越来越慢,因为需要在单点的查询引擎中完成越来越多的运行时聚合。为了保持查询性能,Cube的管理员需要定期地将某些Segment合并在一起,或者让Cube根据Segment保留策略自动地淘汰那些不会再被查询到的陈旧Segment。关于这部分的详细内容会在3.4.1节中展开详细讨论。
最后,我们可以得到这样的结论:对于小数据量的Cube,或者经常需要全表更新的Cube,使用全量构建需要更少的运维精力,以少量的重复计算降低生产环境中的维护复杂度。而对于大数据量的Cube,例如,对于一个包含两年历史数据的Cube,如果需要每天更新,那么每天为了新数据而去重复计算过去两年的数据就会变得非常浪费,在这种情况下需要考虑使用增量构建。
2.设计增量构建Cube
2.1 设计增量Cube的前提
并非所有的Cube都适用于增量构建,Cube的定义必须包含一个时间维度,用来分割不同的Segment,我们将这样的维度称为分割时间列(Partition Date Column)。尽管由于历史原因该命名中存在“date”的字样,但是分割时间列既可以是Hive中的Date类型、也可以是Timestamp类型或String类型。无论是哪种类型,Kylin都要求用户显式地指定分割时间列的数据格式,例如精确到年月日的Date类型(或者String类型)的数据格式可能是yyyyMMdd或yyyy-MM-dd,如果是精确到时分秒的Timestamp类型(或者String类型),那么数据格式可能是YYYY-MM-DD HH:MM:SS。
在一些场景中,时间由长整数Unix Time来表示,由于对该类型的支持存在争议(详情可参见https://issues.apache.org/jira/browse/KYLIN-1698),因此在目前的版本中并不支持使用长整数类型作为分割时间列。作为一种变通的方法,可以在ETL过程中克服这个问题。具体来说,就是在Hive中为包含长整数时间列的表创建一个视图,将长整数时间列转化为符合Kylin规范的任意类型,在后续的Cube设计中,应使用该视图而不是原始的表。
满足了设计增量Cube的前提之后,在进行增量构建时,将增量部分的起始时间和结束时间作为增量构建请求的一部分提交给Kylin的任务引擎,任务引擎会根据起始时间和结束时间从Hive中抽取相应时间的数据,并对这部分数据做预计算处理,然后将预计算的结果封装成为一个新的Segment,并将相应的信息保存到元数据和存储引擎中。一般来说,增量部分的起始时间等于Cube中最后一个Segment的结束时间。
2.2 增量Cube的创建
创建增量Cube的过程和创建普通Cube的过程基本类似,只是增量Cube会有一些额外的配置要求。
2.2.1 Model层面的设置
每个Cube背后都关联着一个Model,Cube之于Model就好像Java中的Object之于Class。如同3.2.1节中所描述的,增量构建的Cube需要指定分割时间列。同一个Model下不同分割时间列的定义应该是相同的,因此我们将分割时间列的定义放到了Model之中。Model的创建和修改在第2章中已经介绍过,这里将跳过重复的部分,直接进入Model Designer的最后一步Settings来添加分割时间列,如图3-1所示。
目前分割时间列必须是事实表上的列,且它的格式必须满足3.2.1节中所描述的要求。一般来说如果年月日已经足够帮助分割不同的Segment,那么在大部分情况下日期列是分割时间列的首选。当用户需要更细的分割粒度时,例如用户需要每6小时增量构建一个新的Segment,那么对于这种情况,则需要挑选包含年月日时分秒的列作为分割时间列。
在一些用户场景中,年月日和时分秒并不体现在同一个列上,例如在用户的事实表上有两个列,分别是“日期”和“时间”,分别保存记录发生的日期(年月日)和时间(时分秒),对于这样的场景,允许用户指定一个额外的分割时间列来指定除了年月日之外的时分秒信息。为了区分,我们将之前的分割时间列称为常规分割时间列,将这个额外的列称为补充分割时间列。在勾选了“Has a separate "time of the day" column?”选项之后(如图3-2所示),用户可以选择一个符合时分秒时间格式的列作为补充的分割时间列。由于日期的信息已经体现在了常规的分割时间列之上,因此补充的分割时间列中不应该再具有日期的信息。反过来说,如果这个列中既包含年月日信息,又包含时分秒信息,那么用户应该将它指定为格式是YYYY-MM-DD HH:MM:SS的常规分割时间列,而不需要勾选“Has a separate "time of the day"column?”。在大部分场景下用户可以跳过补充分割时间列。
2.2.2 Cube层面的设置
Cube的创建和修改在前面已经做过介绍,这里将跳过重复的部分,直接进入Cube Designer的“Refresh Settings”。这里的设置目前包含“Auto Merge Thresholds”、“Retention Threshold”和“Partition Start Date”。“Partition Start Date”是指Cube默认的第一个Segment的起始时间。同一个Model下不同的Cube可以指定不同的起始时间,因此该设置项出现在Cube Designer之中。“Auto Merge Thresholds”用于指定Segment自动合并的阈值,而“Retention Threshold”则用于指定将过期的Segment自动抛弃。