Spark调优(5类调优)

 

目录

1、资源调优

1.1、分配更多的资源

1.2、提高并行度

2、开发调优

2.1、RDD的重用和持久化

2.2、广播变量的使用

2.3、尽量避免使用shuffle类算子

2.4、 使用高性能的算子

2.5、使用Kryo优化序列化性能

2.6、使用fastutil优化数据格式

2.7、调节数据本地化等待时长

3、基于spark内存模型调优

3.1、spark中executor内存划分

3.2、 spark的内存模型

3.3、 任务提交脚本参考

4、shuffle相关参数调优

4.1、spark的shuffle原理

4.2、HashShuffle机制

4.3、Sort shuffle机制

4.4、Spark Shuffle参数调优

5、数据倾斜调优

5.1、据倾斜发生时的现象

5.2、数据倾斜原理

5.3、数据倾斜定位

5.4、数据倾斜原因

5.5、数据倾斜的后果

5.6、数据倾斜调优


1、资源调优

1.1、分配更多的资源

写完了一个复杂的spark作业之后,进行性能调优的时候,首先第一步,就是要来调节最优的资源配置;

(1)分配资源

executor-memory、executor-cores、driver-memory

(2)如何设置这些资源

在实际的生产环境中,提交spark任务时,使用spark-submit shell脚本,在里面调整对应的参数。
 
 提交任务的脚本:
 spark-submit \
 --master spark://node1:7077 \
 --class com.fengge.WordCount \
 --num-executors 3 \    配置executor的数量
 --driver-memory 1g \   配置driver的内存(影响不大)
 --executor-memory 1g \ 配置每一个executor的内存大小
 --executor-cores 3 \   配置每一个executor的cpu个数
 /export/servers/wordcount.jar

(3)参数调节到多大

  • Standalone模式

  先计算出公司spark集群上的所有资源,每台节点的内存大小和cpu核数,
  比如:一共有20台worker节点,每台节点8g内存,10个cpu。
  实际任务在给定资源的时候,可以给20个executor、每个executor的内存8g、每个executor的使用的cpu个数10。

  • Yarn模式

  先计算出yarn集群的所有大小,比如一共500g内存,100个cpu;
  这个时候可以分配的最大资源,比如给定50个executor、每个executor的内存大小10g,每个executor使用的cpu个数为2。

(4)使用原则

在资源比较充足的情况下,尽可能的使用更多的计算资源,尽量去调节到最大的大小。

1.2、提高并行度

spark作业中,各个stage的task的数量,也就代表了spark作业在各个阶段stage的并行度!当分配完所能分配的最大资源了,然后对应资源去调节程序的并行度。

合理设置并行度,可以充分利用集群资源,减少每个task处理数据量,而增加性能加快运行速度。

(1)可以设置task的数量

至少设置成与spark Application 的总cpu core 数量相同
最理想情况,150个core,分配150task,一起运行,差不多同一时间运行完毕;
官方推荐,task数量,设置成spark Application 总cpu core数量的2~3倍。

(2)设置参数spark.defalut.parallelism来设置task数量

默认是没有值的,如果设置了值为10,它会在shuffle的过程才会起作用。

可以通过在构建SparkConf对象的时候设置,例如:
   new SparkConf().set("spark.defalut.parallelism","500")

(3)给RDD重新设置partition的数量

使用rdd.repartition 来重新分区,该方法会生成一个新的rdd,使其分区数变大。一个partition对应一个task。

(4)提高sparksql运行的task数量

通过设置参数 spark.sql.shuffle.partitions, 默认为200;
可以适当增大,来提高并行度。 比如设置为 spark.sql.shuffle.partitions=500

2、开发调优

2.1、RDD的重用和持久化

(1)重用

  • 当第一次使用rdd2做相应的算子操作得到rdd3的时候,就会从rdd1开始计算,先读取HDFS上的文件,然后对rdd1做对应的算子操作得到rdd2,再由rdd2计算之后得到rdd3。同样为了计算得到rdd4,前面的逻辑会被重新计算。
  • 默认情况下多次对一个rdd执行算子操作,去获取不同的rdd,都会对这个rdd及之前的父rdd全部重新计算一次。

  • 可以把多次使用到的rdd,也就是公共rdd进行持久化,避免后续需要,再次重新计算,提升效率。如图,将rdd2持久化:

(2)持久化

调用rdd的cache或者persist方法进行持久化。

  • cache方法默认是把数据持久化到内存中 ,例如:rdd.cache ,其本质还是调用了persist方法
  • persist方法中有丰富的缓存级别,这些缓存级别都定义在StorageLevel这个object中,可以结合实际的应用场景合理的设置缓存级别。

(3)持久化的时可以采用序列化

  • 将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许会导致OOM内存溢出。
  • 当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑使用序列化的方式在纯内存中存储。将RDD的每个partition的数据,序列化成一个字节数组;序列化后,大大减少内存的空间占用。
  • 序列化在获取数据的时候,需要反序列化。但是可以减少占用的空间和便于网络传输。
  • 可以使用双副本机制,进行持久化。

2.2、广播变量的使用

(1)广播变量引入

Spark中分布式执行的代码需要传递到各个executor的task上运行。对于一些只读、固定的数据,每次都需要Driver广播到各个Task上,这样效率低下。广播变量允许将变量只广播给各个executor。该executor上的各个task从所在节点的BlockManager(负责管理某个executor对应的内存和磁盘上的数据)获取变量,而不是从Driver获取变量,从而提升了效率。

(2)广播变量,初始的时候,就在Drvier上有一份副本。通过在Driver把共享数据转换成广播变量

(3)广播变量数量等于executor数量

(4)广播变量只能在Driver端定义,不能在Executor端定义。

(5)在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。

(6)如何使用广播变量

通过sparkContext的broadcast方法把数据转换成广播变量。

获取广播变量中的值可以通过调用其value方法。

2.3、尽量避免使用shuffle类算子

(1)shuffle过程,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。

(2)shuffle涉及到数据要进行大量的网络传输。

(3)使用reduceByKey、join、distinct、repartition等算子操作,这里都会产生shuffle。

(4)避免产生shuffle:Broadcast+map的join操作,不会导致shuffle操作。
(5)使用map-side预聚合的shuffle操作,减少数据的传输量,提升性能。

  • map-side预聚合,在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。
  • 建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。因为reduceByKey和aggregateByKey算子都会使用用户自定义的函数对每个节点本地的相同key进行预聚合

2.4、 使用高性能的算子

(1)使用reduceByKey/aggregateByKey替代groupByKey;

(2)使用mapPartitions替代普通map;mapPartitions一次函数调用处理一个partition所有的数据

(3)使用foreachPartitions替代foreach;一次函数调用处理一个partition的所有数据。

(4)使用filter之后进行coalesce操作;

  • 使用coalesce算子,手动减少RDD的partition数量;

(5)使用repartitionAndSortWithinPartitions替代repartition与sort类操作;

  • 如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。
  • 因为该算子可以一边进行重分区的shuffle操作,一边进行排序。

2.5、使用Kryo优化序列化性能

(1)Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少。

(2)Kryo序列化启用后生效的地方:

  • 算子函数中使用到的外部变量;
  • 持久化RDD时进行序列化,StorageLevel.MEMORY_ONLY_SER;
  • 产生shuffle的地方,也就是宽依赖

(3)开启Kryo序列化机制

// 创建SparkConf对象。
val conf = new SparkConf().setMaster(...).setAppName(...)
// 设置序列化器为KryoSerializer。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

// 注册要序列化的自定义类型
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

2.6、使用fastutil优化数据格式

  • fastutil扩展了Java标准集合框架的类库;
  • fastutil集合类,可以减小内存的占用,更快的存取速度;
  • 使用List (Integer)的替换成IntList即可使用fastutil。

2.7、调节数据本地化等待时长

数据本地化:数据在哪里就在对应的机器上开启计算任务,减少网络传输。

(1)本地化级别

  • PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中;性能最好;
  • NODE_LOCAL:节点本地化,代码和数据在同一个节点中;数据需要在进程间进行传输;性能其次;
  • RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输;性能较差;
  • ANY:无限制,数据和task可能在集群中的任何地方,性能最差。

(2)数据本地化等待时长

spark.locality.wait,默认是3s。

首先采用最佳的方式,等待3s后降级,还是不行,继续降级...,最后还是不行,只能够采用最差的。

(3)调节参数并且测试

修改spark.locality.wait参数,默认是3s,可以增加。

在代码中设置:
new SparkConf().set("spark.locality.wait","10")

3、基于spark内存模型调优

3.1、spark中executor内存划分

Executor的内存主要分为三块:

  1. 第一块是让task执行我们自己编写的代码时使用
  2. 第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用;
  3. 第三块是让RDD缓存时使用。

3.2、 spark的内存模型

(1)静态内存模型

实际上就是把我们的一个executor分成了三部分:

  1. 一部分是Storage内存区域,默认0.6;
  2. 一部分是execution区域,默认0.2;
  3. 还有一部分是其他区域,默认0.2。

用这几个参数去控制:

  • spark.storage.memoryFraction:默认0.6(cache数据大,则应调高)
  • spark.shuffle.memoryFraction:默认0.2  (shuffle多,则应调高)

静态内存模型的缺点:

Storage内存区域和execution区域后,我们的一个任务假设execution内存不够用了,但是它的Storage内存区域是空闲的,两个之间不能互相借用。

(2)统一内存模型

动态内存模型先是预留了300m内存,防止内存溢出。

动态内存模型把整体内存分成了两部分,即统一内存(60%)和其他内存(40%)。

统一内存又划分为两个小部分,Storage内存和execution内存,两者各占50%。

(3)统一内存模型的特点

Storage内存和execution内存可以相互借用。

  • Execution使用的时候发现内存不够了,然后就会把storage的内存里的数据驱逐到磁盘上。

  • 一开始execution的内存使用得不多,但是storage使用的内存多,所以storage就借用了execution的内存,但是后来execution也要需要内存了,这个时候就会把storage的内存里的数据写到磁盘上,腾出内存空间。

3.3、 任务提交脚本参考

./bin/spark-submit \
  --master yarn-cluster \
  --num-executors 100 \
  --executor-memory 6G \
  --executor-cores 4 \
  --driver-memory 1G \
  --conf spark.default.parallelism=1000 \
  --conf spark.storage.memoryFraction=0.5 \
  --conf spark.shuffle.memoryFraction=0.3 \

4、shuffle相关参数调优

4.1、spark的shuffle原理

Shuffle就是对数据进行重组;在整个shuffle过程中,伴随着大量的磁盘和网络I/O。

(1)spark中的shuffle

在DAG调度的过程中,Stage阶段的划分是根据是否有shuffle过程,也就是存在wide Dependency宽依赖的时候,需要进行shuffle,这时候会将作业job划分成多个Stage,每一个stage内部有很多可以并行运行的task。

stage与stage之间的过程就是shuffle阶段,在Spark的中,负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。

(2)shffle的分类

ShuffleManager随着Spark的发展有两种实现的方式,分别为HashShuffleManager和SortShuffleManager。因此spark的Shuffle有Hash Shuffle和Sort Shuffle两种。

4.2、HashShuffle机制

ShuffleManager-HashShuffleManager会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。

在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。

HashShuffleManager的运行机制主要分成两种:

  • 一种是普通运行机制
  • 另一种是合并的运行机制:主要是通过复用buffer来优化Shuffle过程中产生的小文件的数量。

(1)普通机制的Hash shuffle

3个ReduceTask;

4个 ShuffleMapTask;

总共输出了4 x 3个分类文件 = 12个本地小文件。

shuffle Write阶段:在一个stage结束计算之后,将每个task处理的数据按key进行“分区”(即对相同的key执行hash算法)。从而将相同key都写入同一个磁盘文件中,在将数据写入磁盘之前,会先将数据写入内存缓冲中,当内存缓冲填满之后,才会溢写到磁盘文件中。

shuffle Read阶段:stage刚开始时要做的事情;每个shuffle read task都会有一个自己的buffer缓冲,每次都只能拉取与buffer缓冲相同大小的数据,然后通过内存中的一个Map进行聚合等操作。

Hash shuffle普通机制的问题:会产生海量的小文件;可能导致OOM。

(2) 合并机制的Hash shuffle

合并机制就是复用buffer缓冲区,开启合并机制的配置是spark.shuffle.consolidateFiles。该参数默认值为false,将其设置为true即可开启优化机制。

总共输出是 2个Cores(executor) x 3个分类文件(reduceTask) = 6个本地小文件。

启动HashShuffle的合并机制:ConsolidatedShuffle的配置spark.shuffle.consolidateFiles=true

block file=Core*R    (Core为CPU的核数,R为Reduce的数量)

Hash shuffle合并机制的问题:并行任务或者是数据分片过多也会产生很多小文件。

4.3、Sort shuffle机制

SortShuffleManager的运行机制主要分成两种,

  • 一种是普通运行机制;
  • 另一种是bypass运行机制;

(1)Sort shuffle的普通机制

  • 数据会先写入一个数据结构,聚合算子写入Map,一边通过Map局部聚合,一边写入内存。
  • Join算子写入ArrayList直接写入内存中。然后需要判断是否达到阈值(5M),如果达到就会将内存数据结构的数据写入到磁盘,清空内存数据结构。
  • 在溢写磁盘前,先根据key进行排序,排序过后的数据,会分批写入到磁盘文件中。
  •  最后在每个task中,将所有的临时文件合并,这就是merge过程。一个task的所有数据都在这一个文件中,同时单独写一份索引文件。

优点:

 1. 小文件明显变少了,一个task只生成一个file文件
  
  2. file文件整体有序,加上索引文件的辅助,查找变快

(2)bypass模式的sortShuffle

bypass机制运行条件:

  1. shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值
  2. 不是聚合类的shuffle算子(比如reduceByKey)。

在shuffleMapTask数量小于默认值200时,启用bypass模式的sortShuffle(原因是数据量本身比较少,没必要进行sort全排序,因为数据量少本身查询速度就快)

该机制与普通SortShuffleManager运行机制的不同在于:
    第一: 磁盘写机制不同;
    第二: 不会进行sort排序;

4.4、Spark Shuffle参数调优

(1)spark.shuffle.file.buffer

  • 默认值:32k
  • 参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。
  • 调优建议:可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。

(2)spark.reducer.maxSizeInFlight

  • 默认值:48m
  • 参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据
  • 调优建议:适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。

(3)spark.shuffle.io.maxRetries

  • 默认值:3
  • 参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。
  • 调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。

(4)spark.shuffle.io.retryWait

  • 默认值:5s
  • 参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。
  • 调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性

(5)spark.shuffle.memoryFraction(Spark1.6)

  • 默认值:0.2
  • 参数说明:该参数代表了Executor内存中分配给shuffle read task进行聚合操作的内存比例,默认是20%。
  • 调优建议:如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。

(6)spark.shuffle.manager

  • 默认值:sort
  • 参数说明:该参数用于设置ShuffleManager的类型。Spark1.6使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。
  • 调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。

(7)spark.shuffle.sort.bypassMergeThreshold

  • 默认值:200
  • 参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。
  • 调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量(200)。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。

(8)spark.storage.memoryFraction(具体参见统一内存模型)

  • 默认值:0.6
  • 参数说明:该参数用于设置RDD持久化数据在executor内存中的占比,默认是0.6,根据你选择的不同的持久化数据策略级别,如果内存不够了时,可能数据就不会持久化或者数据会写入到内存中。
  • 调优建议:如果spark任务中,有较多的RDD持久化数据操作,这个值可以适当提高一些,保证持久化的数据在能够容乃在内存中。如果spark任务中shuffle操作较多,那么这个参数的值适当降低一些比较合适,更多的内存空间给shuffle操作。

 

 

5、数据倾斜调优

5.1、据倾斜发生时的现象

(1)绝大多数task执行得都非常快,但个别task执行极慢;

(2)绝大数task执行很快,有的task直接报OOM (Jvm Out Of Memory) 异常;

5.2、数据倾斜原理

在进行任务计算shuffle操作的时候,第一个task和第二个task各分配到了1万条数据;需要5分钟计算完毕;第一个和第二个task,可能同时在5分钟内都运行完了;第三个task要98万条数据,98 * 5 = 490分钟 = 8个小时;
本来另外两个task很快就运行完毕了(5分钟),第三个task数据量比较大,要8个小时才能运行完,就导致整个spark作业,也得8个小时才能运行完。最终导致整个spark任务计算特别慢。

5.3、数据倾斜定位

(1)主要是根据log日志信息去定位:

数据倾斜只会发生在shuffle过程中。常用的并且可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。
出现数据倾斜时,可能就是代码中使用了这些算子中的某一个所导致的。因为某个或者某些key对应的数据,远远的高于其他的key。

(2)分析定位逻辑

一个job会划分成很多个stage,首先要看的,就是数据倾斜发生在第几个stage中。

可以在任务运行的过程中,观察任务的UI界面,可以观察到每一个stage中运行的task的数据量,从而进一步确定是不是task分配的数据不均匀导致了数据倾斜。(每个task的运行时间,每个task处理的数据量)。

(3)某个task莫名其妙内存溢出的情况

直接看yarn-client模式下本地log的异常栈,或者是通过YARN查看yarn-cluster模式下的log中的异常栈

(4)查看导致数据倾斜的key的数据分布情况

5.4、数据倾斜原因

(1)数据本身问题

  • key本身分布不均衡(包括大量的key为空);
  • key的设置不合理。

(2)spark使用不当

  • shuffle时的并发度不够;
  • 计算方式有误。

5.5、数据倾斜的后果

(1)spark中的stage的执行时间受限于最后那个执行完成的task,因此运行缓慢的任务会拖垮整个程序的运行速度(分布式程序运行的速度是由最慢的那个task决定的)。

(2)过多的数据在同一个task中运行,将会把executor内存撑爆,导致OOM内存溢出。

5.6、数据倾斜调优

有如下8中方案:

(1):使用Hive ETL预处理数据

适用场景:导致数据倾斜的是Hive表;

实现思路:通过Hive来进行数据预处理(即Hive ETL预先对数据按照key进行聚合,或者是预先和其他表进行join);

实现原理:从根源上解决了数据倾斜,避免了在Spark中执行shuffle类算子;数据本身就存在分布不均匀的问题,故Hive预处理还是会出现数据倾斜问题,治标不治本;只是避免了Spark程序发生数据倾斜。

优点:简单便捷,Spark作业的性能会大幅度提升;

缺点:治标不治本,Hive ETL中还是会发生数据倾斜。

(2):过滤少数导致倾斜的key

适用场景:如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大;

实现思路:如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。(Spark SQL中可以使用where子句过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。)

实现原理:将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。

优点:实现简单,而且效果也很好。

缺点:适用场景不多,大多数情况下,导致倾斜的key还是很多的。

(3):提高shuffle操作的并行度(效果差)

适用场景:增加task是处理数据倾斜最简单的一种方案。

实现思路:对RDD执行shuffle算子时,给shuffle算子传入一个参数,如reduceByKey(1000),该参数就设置shuffle算子执行时shuffle read task的数量。比如group by、join等,需要设置一个参数,即spark.sql.shuffle.partitions,该参数代表了shuffle read task的并行度,该值默认是200;

实现原理:增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。

优点:实现起来比较简单,可以有效缓解和减轻数据倾斜的影响。

缺点:只是缓解了数据倾斜而已,没有彻底根除问题,且效果有限。

这种方案只能说是在发现数据倾斜时尝试使用的第一种手段

(4):两阶段聚合(局部聚合+全局聚合)

适用场景:对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时。

实现思路:核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个key都打上一个随机数,比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1hello, 1) (1hello, 1) (2hello, 1) (2hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1hello, 2) (2hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。

实现原理:将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去掉随机前缀,再次进行全局聚合,就可以得到最终的结果。

优点:对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜。

缺点:仅仅适用于聚合类的shuffle操作,适用范围相对较窄。

(5):将reduce join转为map join

适用场景:在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M或者一两G);

实现思路:使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据连接起来
 实现原理:普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜。具体原理如下图所示。


 优点:对join操作导致的数据倾斜,效果非常好,因为根本就不会发生shuffle,也就根本不会发生数据倾斜。
 缺点:适用场景较少,因为这个方案只适用于一个大表和一个小表的情况。需要将小表进行广播,会比较消耗内存资源,driver和每个Executor内存中都会驻留一份小RDD的全量数据。

(6):采样倾斜key并分拆join操作

适用场景:两个RDD/Hive表进行join的时候,如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。
 实现思路:

  •  1、对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。
  •  2、然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。
  •  3、接着需要join的另一个RDD也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。
  •  4、再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。
  •  5、而另外两个普通的RDD就照常join即可。
  •  6、最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。


 实现原理:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,可以将少数几个key分拆成独立RDD,并附加随机前缀打散成n份去进行join,此时这几个key对应的数据就不会集中在少数几个task上,而是分散到多个task进行join了。
 优点:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,采用该方式可以用最有效的方式打散key进行join。而且只需要针对少数倾斜key对应的数据进行扩容n倍,不需要对全量数据进行扩容。避免了占用过多内存。
 缺点:如果导致倾斜的key特别多的话,比如成千上万个key都导致数据倾斜,那么这种方式也不适合

(7):使用随机前缀和扩容RDD进行join

 适用场景:如果在进行join操作时,RDD中有大量的key导致数据倾斜。
 实现思路:

  •  1、首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。
  •  2、然后将该RDD的每条数据都打上一个n以内的随机前缀
  •  3、同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀
  •  4、最后将两个处理后的RDD进行join即可。

 实现原理:将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“方案(6)”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高
 优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。
 缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,对内存资源要求很高。

(8):把上面的几种数据倾斜的解决方案综合的灵活运行

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值