本文写的有点小认真,也是之前做的项目的总结~
1.研究目标
1.1 介绍
1.1.1 Spark SQL
SparkSQL的前身是Shark,给熟悉RDBMS但又不理解MapReduce的技术人员提供快速上手的工具。随着Spark的发展, Shark对于Hive的太多依赖(如采用Hive的语法解析器、查询优化器等等),制约了Spark的One Stack Rule Them All的既定方针,制约了Spark各个组件的相互集成,所以提出了SparkSQL项目。SparkSQL抛弃原有Shark的代码,汲取了Shark的一些优点,如内存列存储(In-Memory Columnar Storage)、Hive兼容性等,重新开发了SparkSQL代码;由于摆脱了对Hive的依赖性,SparkSQL无论在数据兼容、性能优化、组件扩展方面都得到了极大的方便。
- 数据兼容方面
- 不但兼容Hive,还可以从RDD、parquet文件、JSON文件中获取数据,未来版本甚至支持获取RDBMS数据以及cassandra等NOSQL数据;
- 性能优化方面
- 除了采取In-Memory Columnar Storage、byte-code generation等优化技术外、将会引进Cost Model对查询进行动态评估、获取最佳物理计划等等;
- 组件扩展方面
- 无论是SQL的语法解析器、分析器还是优化器都可以重新定义,进行扩展。
1.1.2 查询下推
查询下推可以从两个两个方面来理解:
- 逻辑优化角度
- 从逻辑优化的角度来看,查询下推属于是将逻辑查询树中的一些节点下推到叶子结点或接近叶子结点,即更接近数据源的地方,从而使得上层节点的操作所涉及到的数据量大大减少,提高数据处理效率,如图1.2所示为一个简单的条件下推;
- 物理计划执行角度
- 从物理计划执行的角度来看,查询下推是将查询的条件下推到数据源,让数据源直接过滤掉与查询结果无关的数据,从而降低数据IO,提高数据的传输和处理效率;如图1.2所示;
1.2 现状
1.2.1 逻辑优化
从逻辑优化的角度,目前的各种数据库查询引擎都能能够支持逻辑优化中的查询下推,将查询条件等尽量下推到叶子结点;
1.2.2 物理执行
1)可进行的下推
从物理执行获取数据的角度来分析,主要能进行下推的操作氛围如下:
投影下推:投影下推即列裁剪,通过将需要的列下推到数据源,这样可以使数据源只返回所需要的列数据给查询引擎,这样可以大大减小数据的传输IO,尤其是对于列存数据源,可以极大的提高查询速率;
条件下推:条件下推即将过滤条件下推到数据源,数据源先进行预过滤,将复合条件的数据返回给查询引擎;
聚合下推:聚合下推是将聚合函数Count,Max,Min,Sum,avg进习惯下推给数据源,利用数据源自己本身的聚合功能进行聚合求值,将求值结果返回给查询引擎,可以极大的减小数据传输开销,同时可以大大节省内存的使用;
OrderBy,Limit下推:这两个下推的意义与上述类似,对于这两个谓词下推到数据源,使数据源在进行数据返回的时候,返回一系列有序的元组或者是所要求的前几条元组,这样便可以节省查询引擎进行复杂的排序开销以及获取不必要的数据量;
2)已实现的下推
从物理执行的角度看,这个角度一般涉及到的是当前的一些大数据处理引擎,如Hadoop,SparkSQL,Impala,Presto等;
这些引擎对于查询下推的支持程度如下:
1.3 意义
查询下推不仅可以降低数据的IO,而且能够节省内存,减少CPU的计算量等,特别是对于分布式的数据处理情境中,查询下推能够对查询性能有一个显著的提升;
2.研究内容
2.1 聚合下推
主要研究SparkSQL中聚合下推的实现方法,通过将聚合函数下推到数据源(MySQL等数据库)执行,从而可以直接从数据源获取到结果,从而大大提高查询引擎的查询效率;
在数据查询时,常用的聚合函数包括:Count(*)、Count(col)、Sum(col)、avg(col)、Max(col)以及Min(col);
2.2 具体实现
图2.1是一个Spark SQL进行SQL语句从解析到查询的完整流程,想要进行聚合谓词的下推,就是在Optimized Logical Plan转为Physical Plan的过程中,修改转换策略,使得生成的物理计划能够将聚合谓词和数据扫描(获取)合并;
例如:示例sql语句:select count(*) from info where fuin > 100174;
聚合下推前的Physical Plan
*(2) HashAggregate(keys=[], functions=[count(1)], output=[count(1)#25L])
+- Exchange SinglePartition
+- *(1) HashAggregate(keys=[], functions=[partial_count(1)], output=[count#28L])
+- *(1) Project
+- *(1) Scan JDBCRelation(info) [numPartitions=1] [] PushedFilters:[*IsNotNull(FUIN), *GreaterThan(FUIN,100174)], ReadSchema: struct<>
聚合下推后的Physical Plan
*(2) HashAggregate(keys=[], functions=[count(1)], output=[count(1)#25L])
+- Exchange SinglePartition
+- *(1) Scan JDBCRelation(info) [numPartitions=1] [count#28L] PushedFilters: [], ReadSchema: struct<count:bigint>
实现方案:
在Spark SQL中,优化后的逻辑计划转化为物理计划的时候,会在Spark Planner中应用一些列的策略,如下:
class SparkPlanner(
val sparkContext: SparkContext,
val conf: SQLConf,
val experimentalMethods: ExperimentalMethods)
extends SparkStrategies {
def numPartitions: Int = conf.numShufflePartitions
//各种策略的应用
override def strategies: Seq[Strategy] =
experimentalMethods.extraStrategies ++
extraPlanningStrategies ++ (
DataSourceV2Strategy ::
FileSourceStrategy ::
DataSourceStrategy(conf) ::
SpecialLimits ::
Aggregation ::
JoinSelection ::
InMemoryScans ::
BasicOperators :: Nil)
通过应用各种策略,从而一步步将逻辑计划转换成物理计划;
因此,如果要想进行聚合下推,就可以在进行聚合的策略中进行修改以改变最后生成的物理执行计划;
具体的修改方案的架构如下:
· 在Aggregate策略进行应用的时候,通过条件判断来开启一个新的分支:如果我们在进行查询的时候,设置了开启查询下推的功能,则在这里进行判断后则会进入一个新的分支AggPushDownUtils,该分支的作用是进行数据扫描和初步聚合的结合,即将聚合函数下推到数据源,直接返回聚合结果;
· AggPushDownUtils在PhysicalOperation进行了聚合和数据读取两方面的物理计划
· 当最后执行到数据读取的时候,则会调用JDBCRelation和JDBCRDD_AGG两个类,JDBCRDD_AGG是为聚合下推重写的一个数据读取的类;
2.3 测试结果
通过性能数据测试我们可以发现:
性能:
当数据量在100w左右的时候,下推的性能没有太大的提升,但是随着数据量的增大,下推的效果逐步明显,在数据量1000w的时候,已经开始出现了较为明显的性能提升,如Sum函数的下推已经达到了性能提升一倍的效果;随着数据量的继续增大,各个聚合函数的性能提升都达到了1倍左右,而且在1billion的数据量的时候,all的方式性能提升又2倍多;继续增大数据量,可以发现性能继续提升,PushDown的性能提升了3~4倍。
内存:
同时,采用下推的方式,还可以大大节省内存,从实验来看,PushDown的方式不会因为数据量的增大而增加内存的需求量,即使亿级的数据量,依然只需要1G的内存即可,但是反观Normal的执行方式,随着数据量的增大会出现内存溢出,需要不断加大内存;而且到了2billion的数据量的时候,Normal方式对内存的需求量进一步提高。
2.4 性能影响
由上图可以看出,查询下推之后的性能性能影响因素主要由数据源决定,聚合下推的关键是将聚合函数下推到数据源,由数据源执行完聚合计算后返回聚合结果。 因此数据源的聚合性能直接影响了聚合查询的效率。