目录
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的内存主要分为三块:
- 第一块是让task执行我们自己编写的代码时使用;
- 第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用;
- 第三块是让RDD缓存时使用。
3.2、 spark的内存模型
(1)静态内存模型
实际上就是把我们的一个executor分成了三部分:
- 一部分是Storage内存区域,默认0.6;
- 一部分是execution区域,默认0.2;
- 还有一部分是其他区域,默认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机制运行条件:
- shuffle map task数量小于spark.shuffle.sort.bypassMergeThreshold参数的值;
- 不是聚合类的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):把上面的几种数据倾斜的解决方案综合的灵活运行