Spark3.0新特性分析

特性概览

官方release note:

https://spark.apache.org/releases/spark-release-3-0-0.html

https://spark.apache.org/docs/3.0.0/core-migration-guide.html

http://spark.apache.org/releases/spark-release-3-0-2.html

1.Adaptive Query Execution - [SPARK-31412]
2.Dynamic Partition Pruning - [SPARK-11150]
3.Accelerator-aware Scheduler - [SPARK-24615]
4.Hadoop 3 support - [SPARK-23534]
5.Java 11 support - [SPARK-24417]
6.Better ANSI SQL compatibility
7.Redesigned pandas UDF API with type hints - [SPARK-28264]

特性详细分析

1. 自适应查询执行

背景

spark社区作为最活跃的开源社区之一,这些年来一直致力于改进spark的执行计划优化器和执行计划生成器,以生成更高质量和更好性能的查询执行计划。其中,较大的改进之一是基于成本的优化,即CBO(cost-based optimization)框架,该框架收集并利用各种数据统计( statistics )信息(如行数,不同值的数量,NULL 值,最大/最小值等)来帮助 Spark 选择更好的执行计划。基于成本的优化很好的例子就是选择正确的 join 类型(如broadcast hash join和sort merge join),在 hash join 的时候选择正确的连接顺序,或在多个 join 中调整 join 顺序。然而,过时的和不完整的统计信息和不完善的基数估计可能导致生成次优查询计划。

基于这样的背景,开源社区提出了自适应执行的概念,即在任务执行过程中根据更真实可靠的运行时数据统计信息来动态的调整和优化执行计划,以生成最优的执行计划,从而提高任务的执行效率。在 Apache Spark 3.0 版本中新增了自适应查询执行(Adaptive Query Execution)的新功能。它希望根据查询执行过程中收集的运行时统计信息,通过重新优化和调整查询计划来解决这些问题 。

自适应执行框架

自适应查询执行最重要的问题之一是何时进行重新优化。Spark 算子通常是以 流水线(pipeline) 形式进行,并以并行的分布式的方式执行,这使得找到一个重新优化执行计划的时机成为关键点。然而,shuffle 或 broadcast exchange 刚好就提供了这样一个绝好的时机。在spark中我们称它们为物化点,并使用术语“stage”来表示查询中由这些物化点限定的子部分。每个查询阶段都会物化它的中间结果,只有当运行物化点的所有并行进程都完成时,才能继续执行下一个阶段。这为重新优化提供了一个绝佳的机会,因为此时所有分区上的数据统计信息都是可用的,并且后续操作还没有开始。

当查询开始时,自适应查询执行框架首先启动所有叶子阶段(leaf stages),这些阶段不依赖于任何其他阶段。一旦其中一个或多个阶段完成物化,框架便会在物理查询计划中将它们标记为完成,并相应地更新逻辑查询计划,同时从完成的阶段检索运行时统计信息。基于这些新的统计信息,框架将运行优化程序、物理计划程序以及物理优化规则,其中包括常规物理规则(regular physical rules)和自适应执行特定的规则,如合并分区(coalescing partitions)、join 数据倾斜处理(skew join handling)等。现在我们有了一个重新优化的查询计划,其中包含一些已完成的阶段,自适应执行框架将搜索并执行子阶段已全部物化的新查询阶段,并重复上面的 execute-reoptimize-execute 过程,直到完成整个查询。

Spark 3.0的AQE框架有以下三个特性:

  • 动态合并shuffle分区( Dynamically coalescing shuffle partitions )

  • 动态调整join策略( Dynamically switching join strategies )

  • 动态优化数据倾斜的join( Dynamically optimizing skew joins )

动态合并shuffle分区

在spark作业中,当查询任务处理的数据量很大的情况下,shuffle无疑对查询性能的影响非常大,因为shuffle需要在网络中传输数据,数据需要按照下游操作符(算子)重新分发数据到不同的节点。在spark 3.0之前的版本中通常shuffle的分区数是固定的,也就是在配置中设置了固定的分区数或者直接继承了上一阶段的分区数。如果分区数太少,那么每个分区处理的数据量可能非常大,处理这些大分区的任务可能需要将数据溢写到磁盘(例如,涉及排序或聚合),从而减慢查询速度。如果分区数太多,那么每个分区处理的数据量可能非常小,下游来读取shuffle数据时将有大量的网络数据拉取,这也会由于低效的 I/O 模式而减慢查询速度;同时,大量的任务也会给 spark 任务调度带来更大的负担。所以shuffle的分区数是一个关键的属性, 分区数的较佳数量取决于数据量的大小,但是数据大小可能在不同的阶段、不同的查询之间有很大的差异,这使得这个数字很难调优。

在spark 3.0中,AQE框架通过运行时动态合并分区数解决了这个问题。 我们可以在任务开始时设置相对较多的shuffle分区数,然后AQE框架在运行时通过获取到最新的shuffle文件统计信息将相邻的小分区合并为较大的分区,这样就能保证在任务的各个阶段都有较佳的分区数,从而保证每一个阶段都有较好的执行效率。

spark shuffle原理:

如上图所示,该 Shuffle 中stage0总共有 2 个 Task(即Mapper),在stage1有 5 个 Task(即Reducer)。每个 Mapper 会按相同的规则(由 Partitioner 定义)将自己的数据分为五份。每个 Reducer 从这两个 Mapper 中拉取属于自己的那一份数据。

固定分区shuffle原理:

如上图所示,固定分区的情况下,每一次shuffle时下一个阶段的Reducer数量都是固定的,不会随着数据量的大小动态的调整Reducer数量。首先,在整个任务执行过程中,越后面的阶段数据量往往会越小,因为在各个阶段可能都会对数据进行过滤筛选或者合并,数据量逐个阶段减少,但是Reducer数量始终保持不变,这显然是不合理。其次,如果输入数据很大的情况下,且shuffle时Reducer数量设置的过小,将会导致单个Reducer中数据量太大,数据处理效率低下。所以固定分区的情况下,shuffle很难有较好的性能,即使是通过调优也很难保证每一个任务的分区数都是较优的。

动态分区shuffle原理:

如上图所示,在动态调整shuffle分区的情况下,Reducer的数量会随数据量的变化做出动态调整,总体上能保证每一个阶段都有一个较优的分区数量,从而使得每一个阶段都有较好的性能,这对于一个stage较多且数据量很大的任务来说尤为重要,可以大幅的提升整个任务的执行效率。

动态调整join策略

spark支持多种join策略,这些策略和许多分布计算框架差不多。在一个查询任务中,join策略的选择往往会直接影响查询的效率(注意:这里说的是join策略,不是join类型,不同的join类型可能会得到不同的查询结果,join策略指的是join的方式。spark的join类型包括Inner Join、Left Join、Reight Join、Full Join、Cross Join、Semi Join等),spark常用的三种join策略:

  • Shuffle Hash Join

  • Broadcast Hash Join

  • Sort Merge Join

Shuffle Hash Join

shuffle hash join可以说是spark的join策略里的基础策略。当要join的两张表的数据量都比较大时,可以选择Shuffle Hash Join。可以将join的两张大表按照join的key进行重分区,保证相同的join key都发送到同一个分区中,再对相同join key的分区进行join操作。如下图示:

Hash Join的具体过程如下:

  • 确定Build Table和Probe Table:这个比较重要,Build Table会被构建成以join key为key的hash table,而Probe Table使用join key在这张hash table表中寻找符合条件的行,然后进行join链接。Build表和Probe表是Spark决定的。通常情况下,较小的表会被作为Build Table,较大的表会被作为Probe Table。

  • 构建Hash Table:依次扫描Build Table的每一行数据,对每一条数据根据join key进行hash,hash到对应的bucket中(类似于HashMap的原理),最后会生成一张HashTable,HashTable会缓存在内存中,如果内存放不下会dump到磁盘中。

  • Join匹配:生成HashTable后,再依次扫描Probe Table的数据,使用相同的hash函数(在spark中,实际上就是要使用相同的partitioner)在HashTable中寻找hash(join key)相同的值,如果匹配成功就将两者join在一起。

join过程如下图所示:

Broadcast Hash Join

当两张表join的时候,如果其中一张表很小(一般是维度表),可以使用Broadcast Hash Join。

Broadcast Hash Join的条件有以下两个:

  • 被广播的表需要小于spark.sql.autoBroadcastJoinThreshold配置的大小,默认是10M;

  • 基表不能被广播,比如left outer join时,只能广播右表。

Broadcast Hash Join分为两步:

  • broadcast阶段:将小表广播到所有的executor上,广播的算法有很多,最简单的是先发给driver,driver再统一分发给所有的executor,spark就是使用这种方式,要不就是基于bittorrete的p2p思路;

  • hash join阶段:在每个executor上执行 hash join,小表构建为hash table,大表的分区数据匹配hash table中的数据。

Broadcast Hash Join过程如下图所示:

Sort Merge Join

当两个表都非常大时,SparkSQL采用了一种全新的方案来join两张表的数据,即Sort Merge Join。这种join方式不需要将一张表的数据全部加载后再进行hash join,但需要在join前将数据进行排序。

首先将两张表按照join key进行shuffle重新分区,保证join key值相同的记录会被分发到相同的分区中,重分区后对每个分区内的数据进行排序,排序后再对相应的分区内的记录进行连接。可以看出,无论分区有多大,Sort Merge Join都不用把一侧的数据全部加载到内存中,而是即用即丢;因为两个序列都是有序的,从头开始遍历,碰到key相同的就merge输出,如果不同,左边小就继续取左边,反之取右边。从而大大提高了大数据量下join的稳定性。

整个过程分为三个步骤:

  • shuffle阶段:将两张大表根据join key进行重新分区,两张表数据会分布到整个集群,以便分布式并行处理;

  • sort阶段:对单个分区节点的两表数据,分别进行排序;

  • merge阶段:对排好序的两张分区表数据执行join操作。join操作很简单,分别遍历两个有序序列,碰到相同join key就merge输出,否则继续取更小一边的key向后遍历。

Sort Merge Join过程如下图所示:

经过上面的分析,很明显可以得出这几种join的代价关系:cost(Broadcast Hash Join)< cost(Shuffle Hash Join) < cost(Sort Merge Join),所以数据仓库设计时最好避免大表与大表的join查询,SparkSQL也可以根据内存资源、带宽资源适量将参数spark.sql. autoBroadcastJoinThreshold适当的调大,让更多的join使用Broadcast Hash Join策略。

如何在每一次join时都能选择最优的join策略,这对于SparkSql来说尤为重要,在之前的版本中,join策略在生成执行计划时就已经确定,在运行过程中不会改变join策略,即使可以使用更优的策略,Spark也不会再去调整join策略。在Spark 3.0版本中,AQE框架解决了这个问题。为了获取更好的性能,AQE框架在任务运行的过程中,即在每一个有数据join的物化点(shuffle),会重新根据数据的运行时统计信息优化执行计划,选择最优的join方式,从而来提高join的性能。这对于大数据量join的场景效果尤为显著。

动态优化数据倾斜的join

当数据在集群中的分区之间分布不均时,就会发生数据倾斜。严重的倾斜会明显的降低查询性能,特别是在进行 Join 操作时。目前SparkSQL没有对倾斜的数据进行相关的优化处理。

Spark 3.0的AQE倾斜 Join 优化从shuffle文件统计信息中自动检测到这种数据倾斜。然后,它将倾斜的分区分割成更小的子分区,这些子分区将分别连接到相应的分区进行join操作,以此减少单个分区join的耗时。

数据倾斜join如图所示:

join倾斜优化示意图:

2. 动态分区裁剪

Spark或Hive在查询时,会根据查询条件或分区字段自动过滤底层的数据文件。但是如果过滤条件没有及时的反映到查询上,就会导致数据被冗余加载。比如下面的sql:

select * from A join B on (A.partcol = B.partcol) where A.othercol > 100;

静态裁剪:

正常静态裁剪的情况下,A表的数据会通过where条件先进行过滤,然后扫描加载数据,扫描的数据再和B表全表扫描的数据进行join。可以看出,当B表有很多无用数据的情况下,会加载很多冗余数据,会严重降低后续join操作的效率。

动态裁剪:

从上面可以看出,当使用动态分区裁剪时,Spark能够在扫描数据前先对B表的partcol进行一次过滤,然后再和 A表进行join。在B表有很多无用数据时,效率提升可想而知应该是非常显著的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值