关于数据仓库,早期分享过不少基础类文章,偶然间看到知乎上这篇关于OLAP的深度解读,从技术发展,产品选型,执行优化等方面做了详细的剖析,分享来给大家看看!
1、按数据量划分
对一件事物或一个东西基于不同角度,可以进行多种分类方式。对数仓产品也一样。比如我们可以基于数据量来选择不同类型的数量,如下图所示:
本系列文章主要关注的是数据量处于百万到百亿级别的偏实时的分析型数仓,Cloudera的Impala、Facebook的Presto和Pivotal的GreenPlum均属于这类系统;
如果超过百亿级别数据量,那么一般选择离线数仓,如使用Hive或Spark等(SparkSQL3.0看起来性能提升很明显);对于数据量很小的情况,虽然是分析类应用,也可以直接选择普通的关系型数据库,比如MySQL等,“杀鸡焉用牛刀”。
2、按建模类型划分
下面我们主要关注数据量中等的分析型数仓,聚焦OLAP系统。根据维基百科对OLAP的介绍,一般来说OLAP根据建模方式可分为MOLAP、ROLAP和HOLAP 3种类型,下面分别进行介绍并分析优缺点。
(1)MOLAP
这应该算是最传统的数仓了。大多数MOLAP产品均对原始数据进行预计算得到用户可能需要的所有结果,将其存储到优化过的多维数组存储中。
由于所有可能结果均已计算出来并持久化存储,查询时无需进行复杂计算,且以数组形式可以进行高效的免索引数据访问,因此用户发起的查询均能够稳定地快速响应。这些结果集是高度结构化的,可以进行压缩/编码来减少存储占用空间。
但高性能并不是没有代价的。首先,MOLAP需要进行预计算,这会花去很多时间。如果每次写入增量数据后均要进行全量预计算,显然是低效率的,因此支持仅对增量数据进行迭代计算非常重要。其次,如果业务发生需求变更,需要进行预定模型之外新的查询操作,现有的MOLAP实例就无能为力了,只能重新进行建模和预计算。
因此,MOLAP适合业务需求比较固定,数据量较大的场景。在开源软件中,由eBay开发并贡献给Apache基金会的Kylin即属于这类OLAP引擎,支持在百亿规模的数据集上进行亚秒级查询。
其架构图较直观地反映了基于cube的预计算模型(build),如下所示:
(2)ROLAP
与MOLAP相反,ROLAP无需预计算,直接在构成多维数据模型的事实表和维度表上进行计算。R即表示关系型(Relational)。显然,这种方式相比MOLAP更具可扩展性,增量数据导入后,无需进行重新计算,用户有新的查询需求时只需写好正确的SQL语句既能完成获取所需的结果。
但ROLAP的不足也很明显,尤其是在数据体量巨大的场景下,用户提交SQL后,获取查询结果所需的时间无法准确预知,可能秒回,也可能需要花费数十分钟甚至数小时。本质上,ROLAP是把MOLAP预计算所需的时间分摊到了用户的每次查询上,肯定会影响用户的查询体验。
当然ROLAP的性能是否能够接受,取决于用户查询的SQL类型,数据规模以及用户对性能的预期。对于相对简单的SQL,比如TPCH中的Query响应时间较快。但如果是复杂SQL,比如TPC-DS中的数据分析和挖掘类的Query,可能需要数分钟。
相比MOLAP,ROLAP的使用门槛更低,在完成星型或雪花型模型的构建,创建对应schema的事实表和维度表并导入数据后,用户只需会写出符合需求的SQL,就可以得到想要的结果。相比创建“数据立方体”,显然更加方便。
有分析表明,虽然ROLAP的性能不如MOLAP,但由于其灵活性、扩展性,ROLAP的使用者是MOLAP的数倍。
目前生产环境使用较多的开源ROLAP主要可以分为两大类,一个是宽表模型,另一个是多表组合模型(就是前述的星型或雪花型)。
1、宽表模型
宽表模型能够提供比多表组合模型更好的查询性能,不足的是支持的SQL操作类型比较有限,比如对Join等复杂操作支持较弱或不支持。
目前该类OLAP系统包括Druid和ClickHouse等,两者各有优势,Druid支持更大的数据规模,具备一定的预聚合能力;ClickHouse部署架构简单,易用,保存明细数据,依托其向量化查询、减值等优化能力,具备强劲的查询性能。两者均具备较高的数据实时性,在互联网企业均有广泛使用。
除了上面介绍的Druid和ClickHouse外,ElasticSearch和Solar也可以归为宽表模型。但其系统设计架构有较大不同,这两个一般称为搜索引擎,通过倒排索引,应用Scatter-Gather计算模型提高查询性能。对于搜索类的查询效果较好,但当数据量较大或进行扫描聚合类查询时,查询性能会有较大影响。
2、多表组合模型
采用星型或雪花型建模是最通用的一种ROLAP系统,常见的包括GreenPlum、Presto和Impala等,他们均基于MPP架构,采用该模型和架构的系统具有支持的数据量大、扩展性较好、灵活易用和支持的SQL类型多样等优点。
相比其他类型ROLAP和MOLAP,该类系统性能不具有优势,实时性较一般。通用系统往往比专用系统更难实现和进行优化,这是因为通用系统需要考虑的场景更多,支持的查询类型更丰富。而专用系统只需要针对所服务的某个特定场景进行优化即可,相对复杂度会有所降低。
对于ROLAP系统,尤其是星型或雪花型的系统,如果能够尽可能得缩短响应时间非常重要,这将是该系统的核心竞争力。
目前生产环境使用的ROLAP系统,均实现了大部分的该领域性能优化技术,包括采用MPP架构、支持基于代价的查询优化(CBO)、向量化执行引擎、动态代码生成机制、存储空间和访问效率优化、其他cpu和内存相关的计算层优化等。
首先来聊聊系统架构,这是设计OLAP系统的第一次分野,目前生产环境中系统采用的架构包括基于传统的MapReduce架构加上SQL层组装的系统;主流的基于MPP的系统;其他非MPP系统等。
1、MR架构及其局限
在Hadoop生态下,最早在Hive上提供了基于MapReduce框架的SQL查询服务。
但基于MR框架局限性明显,比如:
第一个问题导致无法进行跨MR操作间的优化,第二个问题导致MR间数据交互需要大量的IO操作。两个问题均对执行效率产生很大影响,性能较差。
2、MPP优缺点分析
MPP是massively parallel processing的简称,即大规模并行计算框架。相比MR等架构,MPP查询速度快,通常在秒级甚至毫秒级以内就可以返回查询结果,这也是为何很多强调低延迟的系统,比如OLAP系统大多采用MPP架构的原因。
下面以Impala为例,简单介绍下MPP系统架构。
3、MPP架构之所以性能比MR好,原因包括:
这样可以充分利用CPU资源,减少IO资源消耗。但事情往往是两面的,MPP并不完美,主要问题包括:
基于上述分析,MPP比较适合执行时间不会太久的业务场景,比如数小时。因为时间越久,故障概率越大。
4、其他非MPP架构
基于MR系统局限性考虑,除了采用MPP架构外,Hive和Spark均使用不同方式进行了优化,包括Hive的Tez,SparkSQL基于DAG(Directed Acyclic Graph)等。
不同架构有不同优缺点,重要的是找到其适用的场景,并进行靠谱的优化,充分发挥其优势。
有了适合的系统架构并不一定能够带来正向收益,“好马配好鞍”,执行计划的好坏对最终系统的性能也有着决定性作用。执行计划及其优化,就笔者的理解来说,其来源于关系型数据库领域。这又是一门大学问,这里仅简单介绍。
分布式架构使得执行计划能够进行跨节点的并行优化,通过任务粒度拆分、串行变并行等方式大大缩短执行时间。除此之外,还有2个更重要的优化方式,就是传统的基于规则优化以及更高级的基于代价优化。
1、基于规则优化
通俗来说,基于规则的优化指的是不需要额外的信息,通过用户下发的SQL语句进行的优化,主要通过改下SQL,比如SQL子句的前后执行顺序等。比较常见的优化包括谓语下推、字段过滤下推、常量折叠、索引选择、Join优化等等。
2、基于代价优化
基于规则的优化器简单,易于实现,通过内置的一组规则来决定如何执行查询计划。与之相对的是基于代价优化(cost based optimization,CBO)。
CBO的实现依赖于详细可靠的统计信息,比如每个列的最大值、最小值、平均值、区分度、记录数、列总和,表大小分区信息,以及列的直方图等元数据信息。
CBO的一大用途是在Join场景,决定Join的执行方式和Join的顺序。这里所说的Join我们主要是讨论Hash Join。
Join执行方式
根据参与Join的驱动表(Build Table)和被驱动表(Probe Table)的大小,Hash Join一般可分为broadcast和partition两种。
广播方式适用于大表与小表进行Join,在并行Join时,将小表广播到大表分区数据所在的各个执行节点,分别与大表分区数据进行Join,最后返回Join结果并汇总。
而分区方式是最为一般的模式,适用于大表间Join或表大小未知场景。分别将两表进行分区,每个分区分别进行Join。
显然,判断大小表的关键就看是否能够通过某种方式获取表的记录数,如果存储层保存了记录数,那么可从元数据中直接获取。
查询执行引擎 (query execution engine) 是数据库中的一个核心组件,用于将查询计划转换为物理计划,并对其求值返回结果。
查询执行引擎对系统性能影响很大,在一项针对Impala和Hive的对比时发现,Hive在某些简单查询上(TPC-H Query 1)也比Impala慢主要是因为Hive运行时完全处于CPU bound的状态中,磁盘IO只有20%,而Impala的IO至少在85%。
什么原因导致这么大的差别呢?
1、向量化执行引擎
向量化执行以列存为前提,主要思想是每次从磁盘上读取一批列,这些列以数组形式组织。每次next都通过for循环处理列数组。这么做可以大幅减少next的调用次数。相应的CPU的利用率得到了提高,另外数据被组织在一起。
可以进一步利用CPU硬件的特性,如SIMD,将所有数据加载到CPU的缓存当中去,提高缓存命中率,提升效率。在列存储与向量化执行引擎的双重优化下,查询执行的速度会有一个非常巨大的飞跃。
2、动态代码生成
向量化执行减少CPU等待时间,提高CPU Cache命中率,通过减少next调用次数来缓解虚函数调用效率问题。而动态代码生成,则是进一步解决了虚函数调用问题。
动态代码生成技术不使用解释性的统一代码,而是直接生成对应的执行语言的代码并直接用primitive type。对于判断数据类型造成的分支判断,动态代码的效果可以消除这些类型判断,使用硬件指令来进一步提高循环处理效率。
具体实现来说,JVM系如Spark SQL,Presto可以用反射,C++系的Impala则使用了llvm生成中间码。相对来说,C++的效率更高。
向量化和动态代码生成技术往往是一起工作达到更好的效果。
存储和IO模块的优化方法很多,这里我们还是在Hadoop生态下来考虑,当然,很多优化方法不是Hadoop特有的,而是通用的。OLAP场景下,数据存储最基础而有效的优化是该行存储为列存储,下面讨论的优化措施均基于列存。
1、数据精细化存储
所谓数据精细化存储,是通过尽可能多得提供元数据信息来减少不必要的数据扫描和计算,常用的方法包括但不限于如下几种:
2、数据本地化访问
数据本地化读写是常见的优化方法,在Hadoop下也提供了相应的方式。
一般来说,读HDFS上的数据首先需要经过NameNode获取数据存放的DataNode信息,再去DataNode节点读取所需数据。
对于Impala等OLAP系统,可以通过HDFS本地访问模式进行优化,直接读取磁盘上的HDFS文件数据。HDFS这个特性称为"Short Circuit Local Reads",其相关的配置项(在hdfs-site.xml中)如下:
3、运行时数据过滤
这是少部分OLAP系统才具有的高级功能。
sparksql图1(官方这个图有误,右边应该是Scan Date)
上面2幅图是SparkSQL 3.0的动态分区裁剪示意图。将右表的扫描结果(hashtable of table Date after filter)广播给左表的Join节点,在进行左表扫描时即使用右表的hashtable进行条件数据过滤。
还有个极为重要的技术是集群资源管理和调度。Hadoop使用YARN进行资源调度,虽然带来了很大便利,但对性能要求较高的OLAP系统却有些不适合。
如启动AppMaster和申请container会占用不少时间,尤其是前者,而且container的供应如果时断时续,会极大的影响时效性。
目前的优化方法主要包括让AppMaster启动后长期驻守,container复用等方式。让资源在需要用时已经就位,查询无需等待即可马上开始。
本文主要是想阐述下自己对数仓和OLAP系统的理解,由于水平有限,难免有所错误,非常欢迎大家看后能够指出。