产品技术|用炒菜的思路搞定你的复杂分析

【产品技术|用炒菜的思路搞定你的复杂分析】今天的分享来自于 Kyligence 解决方案团队冯礼。作为一个老售前,他被客户问的最多的一个问题就是: “我们是数据提供方,无法在开始就了解所有的业务分析需求,业务方又希望无论分析多复杂都能得到迅速响应,Kylin 可以解决这个问题吗?”作者将从原理出发,通过几个厨房里的类比来解释一下 Apache Kylin 及其商业版 Kyligence Enterprise 的适用场景和优化思路。
从烹饪看计算
如果把数据分析比喻成做菜,菜品比较简单的小饭店加工过程一般是这样的:从原始食材开始加工,经过洗菜、切菜、入味、炒菜等步骤送到食客面前。但是如果你想开的是一家可以提供佛跳墙这类高端菜品的饭店,需要的食材多,做起来又费事,小饭店的做法显然捉襟见肘。有了Kyligence,这个问题就将迎刃而解。做菜的流程如下:

  • step1: 根据用餐场景设计菜谱(模型和Cube设计)
  • step2: 根据菜谱对食材进行预加工,得到半成品备菜(加载数据进行Cube构建)
  • step3: 根据客人点的菜找到相应的备菜(用户发起查询,命中合适的Cube预计算结果)
  • step4: 下锅或烹或炒(基于预计算结果进行在线计算)
  • step5: 快速完成出锅 (返回查询结果)

一言以蔽之,想做满汉全席复杂数据加工的请联系 Kyligence!

从上面这个例子我们对预计算技术有了个感性认识。这种技术通过在设计时确定要聚合/过滤的列以及要计算的指标,预先计算聚合结果,在执行SQL时直接对聚合结果(Cube的Cuboid)进行查询,所以能达到亚秒级的查询响应速度和高并发的查询吞吐率。
下面是Cube中某个Cuboid的逻辑结构,原表是一张交易流水表,通过预计算进行聚合后, Cuboid里存储的是三个维度:交易日期(PART_DT),交易地点(LOCATION)和品名(ITEM)的group by聚合结果,记录了汇总后的交易总额(Sum(sales))。Cuboid编号为111,其中每个1对应一个维度。

从执行层面看,下面这张图说明了普通数据库和Kylin的执行计划的不同,Kylin已经把最花时间的两个步骤:联接(Join)和聚合(Aggregate)事先算好,存储在Cube中,等到查询时,看菜下饭,找到最适合回答查询的维度组合(即Cuboid),然后只要进行小代价的几个步骤:过滤(Filter),投影(Project)和可能需要的后聚合(Post-Aggregate),就在眨眼间大功告成了。

预计算这么管用,那要实现这套流程哪一步最关键?当然是制作菜谱(设计Cube)了,一本设计得好的菜谱,可以让你从容应对各路食客食不厌精或刁钻古怪的饕餮需求,还不用花太多时间和人手在预加工上,可谓事半功倍。
一本菜谱里,可以有多种食材(即要分析的维度),他们的各种排列组合就形成了一道道菜色的备菜(即Cuboid),好比[西红柿+鸡蛋+葱花]是番茄炒蛋的备菜,[西红柿+牛肉+土豆+洋葱]是罗宋汤的备菜。显然,开始给的食材越多,能做出的菜色就越多,这本菜谱也就越复杂。如果不加限制,那食材(维度)的个数n和菜色(Cuboid)的数量是指数级的对应关系, n个维度对应2的n次方个Cuboid。(注:此处可参考金庸老先生射雕里的名菜”玉笛谁家听落梅” ,不过金老先生只算了食材为一种和两种组合的情况)。
当n比较大时,事先加工的代价会非常大。所以我们在设计时,会考虑两个因素来减少菜谱上实际要备菜的菜色(Cuboid剪枝)。
  • 一是考虑客人常点的菜色(事先知道查询模式)
  • 二是发挥炒菜师傅的主观能动性(多使用在线计算)
还是举烧菜的例子,假设菜馆里已经准备了[西红柿+牛肉+土豆+洋葱]这么一份半成品备菜,客人要点罗宋汤那是正好合适,可以直接做,而客人如果点了个[土豆炖牛肉],也可以基于这份备菜来制作,无非让炒菜师傅费点事,从半成品里面把西红柿和洋葱挑出去,然后就可以开始炖土豆牛肉。这里的关键是挑出西红柿和洋葱的这个过程并不十分费事(在线计算代价小),因为西红柿和洋葱的量不大(维度基数小)。
而如果是要从五彩虾仁的备菜[玉米粒+胡萝卜丁+虾仁]里把所有玉米粒和胡萝卜挑出去来做清炒虾仁,那炒菜师傅就要跳起来了。因为玉米粒和胡萝卜丁数量都很多(维度基数大),挑出去这个过程很费事(在线计算代价大)。可见,我们在设计Cube时,如果要利用后聚合,维度的基数是一个关键因素。
回到Kylin的场景,举个实例来说明后聚合运算。如果我们想按日期(PART_DATE)和地点(LOCAITON)汇总计算销售额,SQL类似
Select PART_DATE, LOCAITON,Sum(sales) from table group by PART_DATE, LOCAITON;
而Cube里最接近的Cuboid只有[PART_DATE,LOCATION,ITEM],当我们把查询提交给Kylin后,发生的过程如下图:
首先进行Project,去除不需要的ITEM列,只保留PART_DATE和LOCATION两列,然后对这两列进行后聚合运算,合并Sum(Sales),得到最终结果。由于这个例子里的三列:日期、地点、品名的基数都不高,所以我们是从5行里后聚合得到2行。反过来,如果品名有1百万种,那下面这个例子就需要从几百万行里进行后聚合,在线计算的耗时会很长。

Cube设计举例
说完原理,我们来聊一下实践。在多维分析这个领域里,大体有两种风格的食客:
  • 其一曰固定报表,可类比作去食堂吃饭,能点什么菜都固定,而且菜色不会很多,菜谱就相对好设计;
  • 其二曰灵活查询,可类比为海鲜集市里的代客加工,一开始你只看到有哪些食材,只要是这些食材能做出的菜客人都可能点,而且可以随意排列组合,类似本文开头客户会问的那个问题,那菜谱就会比较难设计。
先从简单的开始,对于固定报表,我们在设计Cube时,可以为每一个固定场景设计一个聚合组1,每个聚合组里包含该场景里所有进行聚合和过滤的维度,并把这些维度都设置为必需维度1,这样每个聚合组只包含一个Cuboid,总的Cuboid个数可以控制的非常少;短处是查询模式需要固定,稍有变化就意味着需要从一个大的Cuboid进行后聚合,时间代价会比较大,想象一下从一大堆食材里挑出你需要的一些。如果要保留一定的灵活性,可以把固定的过滤条件设置为必需维度,再根据业务的使用场景来设置联合,层级等维度。(注1:关于聚合组、必需维度、联合维度、层级维度等概念请参考Kyligence Enterprise的产品手册)。
下图是一个固定报表的Cube设计,聚合组-1和聚合组-2都对应一张固定报表:

灵活查询的场景往往出现在自助式的前端展现上,业务人员可以随意地拖拽维度进行分析,无法事先知道查询模式。这种情况下,如果总的维度个数大于10,我们必须进行剪枝优化,目标是把Cuboid的个数控制在1024以下(在Cube设计的界面可以看到Cuboid个数),1024是个经验数据,意味着膨胀率在一个可接受的级别,餐馆老板不会觉得备菜的成本太高。由于我们并不能事先得知用户的查询模式,能参考的信息一是维度的基数,二是维度之间是否有层级关系(一般看数据表的描述可以得知层级关系,用于设置层级维度)。
维度基数可以运行KyligenceEnterprise的数据源采样功能获得,如下图:

得到所有维度的基数后,对于百万以上基数的维度,我们需要进行一些特殊处理,否则它会和其他的维度进行各种组合,从而产生一大堆包含它的Cuboid,所有这些 Cuboid在行数和体积上都会非常庞大,这会是一场存储的灾难,就好像我们在每道备菜里都放上一大堆玉米粒。这时我们需要去找业务人员,确定高基维度只与部分维度同时被查询,然后可以通过聚合组对这个高基数维度做一定的“隔离”。把这个高基维度放入一个单独的聚合组,将它设置为必需维度,再把所有可能会与这个高基维度一起被查询的维度也放进同一个聚合组。这样,这个高基维度就被“隔离”在一个聚合组中,所有不会与它被一起查询到的维度都不和它出现在同一个Cuboid里。 通过这样做,你确保了只有非玉米不可的备菜里才会放进玉米粒,其他菜色都安全了。
对于基数特别低的维度(10以下),我们可以人为地把他们组合在若干联合维度组里(即使他们实际并不一定同时在查询中出现)。就比如鸡蛋和姜片,他们都比较容易从备菜中挑出去,虽然不一定同时使用,但也可以来这样组个CP,把它们的组合当成一种食材来看待。还要注意的一点是控制每组中的维度基数的乘积不要超过1000,这也是个经验数据,太多会导致挑出食材的时间过长,你的炒菜师傅会暴走。在对这些维度进行查询时,会进行后聚合,由于总基数低,相当于对一个小的记录数进行后聚合,使得整个查询的时延不会很长。
此外,还有一个有力的剪枝工具是基于最大维度组合数的Cuboid剪枝(MDC),这个工具的设计思想是在一张报表内,业务分析人员能同时查看的维度总数(包含用于聚合和过滤)是有限的,所以可以人为设置一个上限。毕竟满汉全席比较小众,更多还是少数几种食材就能做的常规菜色。在预计算时,超过这个上限的Cuboid都会被舍去。如下图,设置了最大维度组合数为4以后,Cuboid数量从256减少为163。

当你完成了维度的设计,把原本天文数字的Cuboid数量减到可控后,是不是很有成就感?但别急着点保存,还有很重要的一步:Rowkey的设计。
首先,需要选择每个维度的编码,这决定着维度在磁盘上实际存储的形式。在最新版本的KyligenceEnterprise里,当你第一次打开Rowkey页面时,会自动根据维度的数据类型和采样的结果选择编码,一般按默认的推荐值就行,如下图:

要特别注意的是:避免对百万以上基数的维度进行dict(字典)编码,因为那意味着会产生一个超大的字典文件,在未来会给你带来许多麻烦。

完成编码设置后,还需要拖拽来调整Rowkey顺序,这里需要遵守以下的几个原则:
  • 可能在查询中被用作过滤条件的维度,应当放在其它维度的前面。
    a)对于多个可能用于过滤条件的维度,基数高的(意味着用它进行过滤时,较多的行被过滤,返回的结果集较小)更适合放在Roweky的前列;
  • b)总体而言,可以用下面这个公式给维度打分,得分越高的越应该放在前排:
    排序评分=维度出现在过滤条件中的概率*该维度进行过滤时舍去的记录数

  • 经常使用到的维度,放在不经常使用的维度的前面,这样在需要进行后聚合的场景中效率会更高。

  • 对于不会出现在过滤条件中的维度,按照其基数的高低排列,优先将高基数的维度放在Rowkey的前面。这是为了优化构建时的效率。
好了,到这里整个Cube设计中最难的部分已经完成。如果你已经是Kylin的使用者,曾经纠结过Cube设计怎么满足复杂的业务需求,纠结过空间和时间的平衡取舍,不妨按照本文的步骤去尝试一下,并告诉我们你的反馈。如果你还没有接触过Kylin,觉得上面这套方法能帮到你,那请点击‘阅读原文’联系我们。
关于作者
冯礼, Kyligence解决方案部门主任架构师/服务团队Leader,多年大数据行业老兵,目标是把专业热诚的服务带给每个客户。PS.烹饪的水平只能到糖醋排骨这个层次。

    推荐阅读