这是《漫谈分布式系统》系列的第 25 篇,预计会写 30 多篇。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。
前面几篇文章,我们讲到了因为 MR 太慢,所以出现了 Spark,大幅提升了性能。但 Spark 还不够快,传统关系数据库发展起来的 MPP 架构,比较好的满足了我们的高性能查询要求,在和 HDFS 结合并提出类似 virtual segment 这样的概念后,也一定程度上解决了扩展性的问题。
但是,在一些复杂的场景下,无论是 Batch 还是 MPP 可能都不够快。比如查询条件复杂、join 表特别多、计算量特别大的情况。
MOLAP(Multi-Dimensional OLAP) 就是这样的一种场景。作为 OLAP 的一种,MOLAP 通常涉及很多很复杂的维度,不同维度间可能任意组合,导致计算和数据爆炸。
对于这种场景,也可以在 MPP 架构下去优化,只是难度和成本会比较高。而 Apache Kylin 另辟蹊径,给出了不同的解决方案。
Apache Kylin 的想法其实并没有多么开创性,说白了就是「空间换时间」,很多领域都通过这个思想在解决问题,典型的比如 HashTable。但将这个思想运用到 MOLAP 这个领域却是独到的实现。
通俗点讲,就是提前把所有统计结果算出来,然后放到在线存储引擎上。查询请求过来后,直接查询对应的结果,而不用现场做复杂的计算。
整体的架构也并不复杂:
简化下,主要分为三部分:
Query Server,即 Kylin 的核心服务,包括 SQL 的解析和优化,元数据的管理,Cube 的构建,统计结果的获取等,
Build Cluster,通常是独立的计算集群,可以复用已有的 MapReduce、Spark 集群等,源数据通常也保存在 Hive 集群上,
Storage Cluster,保存计算完的统计结果,默认是 HBase。
通常可能会把构建任务做成周期运行,所以外围还需要一个任务调度系统,Oozie、Azkaban、Airflow 之类的,不过没有体现在这个架构图里。
上面已经提到了 Kylin 或者说 MOLAP 里的核心概念 -- Cube。所谓 Cube,顾名思义,立方体,是一个三维结构,如果维度变多,就成了多维立方体。
每个维度就对应 OLAP 里的一个维度,不同维度给与不同取值,就有了不同的组合,每一个组合就叫 Cuboid,所有 Cuboid 合起来就是一个 Cube。
MOLAP 之所以计算量大,就是因为 Cuboid 组合太多。对于一个 20 个维度 cube 而言,cuboid 数是 2^20,这是个非常大的数字,再考虑到每个维度的基数,计算量就很恐怖了。
所以即使 Kylin 采用了预计算的方法减小查询时的延迟,但预计算并不会减小计算量。
维度剪枝,是减小计算量最直观有效的办法,也是 Kylin 要着重优化的点。
很显然,剪枝只能从业务角度去做,否则会造成误伤。Kylin 从过往业务经验中抽象出了以下几种常见方法,来降低 Cuboid 数:
Aggregate Group,聚合组,选择一组维度,这些维度间有一定的业务规律,使得可以指定特定的规则,来减少 cubiod 数量,
Derived,可以把维表的非主键字段设置为衍生维度,这样不会将这些字段加入 coboid,而用将事实表外键即维表主键代替,以此来减少 cubiod 数量,
其中,聚合组支持的规则有:
mandatory,指定当前维度为必须,则所有不包含该维度的 cobiod 都不需要计算,
hierarchy,如维度 A、B、C 有继承关系,则只保留 A、AB、ABC 三个 cuboid,其他组合均舍弃,典型的比如省市区,
joint,如果 A 和 B 是联合关系,那二者要么同时出现,要么都不出现,所有只有其中之一的 cuboid 都舍弃。
而衍生维度,通过下面这个例子会更容易理解。
如上图,不做任何处理时,维度有以下组合:
XAB, XA, XB, AB, X, A, B(省略 Dim 前缀)
但其实 A 和 B 都可以由 X 确定,如果我们把 A 设置为 derived,则维度组合变成:
XB, X, B
组合数从 6 个减少为 3 个。存储和计算开销也对应减少。
而当执行查询时,仍然要支持对 derived 的维度 A,所以需要做转换。先将 X 全查出来,然后按维表映射关系替换为 A,再对 A 做聚合。
由于预计算没有包含 A,所以这个转换和聚合操作是在查询时现场做的,对响应时间会有一些影响,但相比较预计算资源的节省,通常是可以接受的(当然严谨点说,需要根据业务场景来决定是否设置)。
除了 cuboid 剪枝外,Kylin 还提供了其他减小计算量的办法。如有些不需要精确去重的场景,可以使用基于 HyperLogLog 的 count distinct 来做模糊去重。当然如果必须精确去重,也可以用基于 bitmap 的 count distinct。
另一个需要优化的点是计算结果的存储。Kylin 默认把结果保存在 HBase 中。考虑到数据结构和查询方式,HBase 确实是个不错的选择。
但 Kylin 把每个分区的数据定义为 segment,而每个 segment 都对应一张 HBase 表。这就使得 HBase 的压力可能变得很大。
segment 数量可能会急速膨胀,导致 HBase 表数量迅速增长,元数据管理压力陡增,对集群稳定性和性能造成非常大的负担。
对于这个办法,目前有两个思路解决。
其一,是做 segment 的合并,来减少表的数量。
合并之后自然问题能得到极大缓解,但一旦需要重刷部分历史数据,只能整个 segment 全部重刷。就可能出现发现有一天数据异常,但却要重刷一整年的数据的情况。
显然这又是一个需要 trade-off 的场景。可以考虑通过时间窗口来调节,滞后一段时间留待观察,来尽量避免大规模重算。
另一个思路,更彻底,是替换掉 HBase。
社区版本早期考虑过这个思路,业界一些公司也有自己的实践,比如 Kylin on Druid。
但 Kylin 的商业版本 Kylingence Enterprise 最后采用了 Spark 常驻 session + Parquet 的方案,开源版本也在积极改造跟进中,势必会成为主流方案。
以往普遍认为数据库才适合存结果数据,但实际上在 Spark 和 Parquet 大量优化的支持下,至少对 MOLAP 这个场景来说,也能获得足够用的性能。其实这也并不奇怪,数据库本身也是在自定义文件格式的基础之上,结合大量优化达到的高性能。
做好了维度剪枝和存储引擎替换这两个大问题,Kylin 通过预计算这个思路,另辟蹊径的在 OLAP 领域挣得了一席之地。
对于我们自己在做架构设计时,我想,这一点也是最重要的。很多时候,所谓的创新,并不一定要开天辟地,一点思路上的转变,就可能有出乎意料的收获。
最近差不多 10 篇文章,围绕着批处理这个话题,我们一路从 MR -> Spark -> MPP -> Kylin 走过来,解决了一个又一个问题。但并不代表后面提到的框架就比前面的更优秀,就能替代前辈们。
大多数时候,并没有一个框架能解决所有问题(No silver bullets),哪怕很多框架有这个野心。更多的时候,我们都需要在特定的应用场景下,选择最合适的框架,而在另外的场景,可能其他的框架才更合适。事实也确实如此,在规模足够大的公司,这些框架经常都是并存的。
原创不易
关注/分享/赞赏
给我坚持的动力