spark性能调优与数据倾斜

1 spark性能调优

          常规性能调节

1.1 分配资源

          资源包括:executorcpu per executormemory per executordrivermemory

          提交作业的时候采取如下方式        

                     /usr/local/spark/bin/spark-submit\

                    --class cn.spark.sparktest.core.WordCountCluster \

                    --num-executors 3 \  配置executor的数量

                    --driver-memory 100m \  配置driver的内存(影响不大)

                    --executor-memory 100m \  配置每个executor的内存大小

                    --executor-cores 3 \  配置每个executorcpu core数量

                   /path/to/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar\

       分配资源的一个原则:你能使用的资源有多大,就调节到多大

       ①增加executor

       增加executor的数量之后,意味着能够并行执行的task数量也就增大

       ②增加executor per core

       增加每个executor的cpu core,也意味着能够并行执行的task数量也就增大

       ③增加memory per executor

       增加每个executor的内存,可以缓存更多RDD,将更少的数据写入磁盘甚至不写,减少磁盘IO

       增加每个executor的内存,对于shuffle操作,reduce端需要内存存放拉取的数据,如果内存不够,将存入磁盘

       对于task的执行,会创建很多对象,如果内存过小,会频繁导致JVM堆内存满溢,频繁GC(minor gc、full gc)


1.2  调节并行度

        并行度:spark作业中,各个stage的task数量,也就代表了spark作业在各个阶段(stage)的并行度。

       当你的资源充足时,例如有100个executor,每个executor有2个cpu core,每个executor 的内存10G,但是,你现在只有150个task,平均分配之后,还剩下20个cpu core处于空闲状态。

      task的数量推荐设置:总cpu core数量的2-3倍,因为实际运行情况,一些task运行速度较快,另外一些task运行速度较慢,如果你的task数量设置刚好与cpu core一致,运行快的task所在节点资源处于空闲状态。  

   SparkConfconf = new SparkConf().set("spark.default.parallelism", "500")


1.3 抽取公共的RDD

       默认情况下,多次对一个RDD执行算子,会对这个RDD以及之前的父RDD重新计算一次(这种情况要避免)持久化

       从一个RDD到几个不用的RDD,算子和逻辑都一致,因为人为的疏忽,导致计算多次(这种情况要避免)抽取一个大的共用RDD

      持久化,是可以进行序列化的,如果正常将数据持久化到内存了能会导致内存满溢,这个时候可以将数据序列化,序列化唯一缺点,获取数据需要反序列化,消耗一定的性能;持久化还支持双副本的形式(这种方式仅仅在你的内存资源足够充分的情况下使用)。

     公共的RDD一定要持久化,避免重读计算。


1.4  广播大变量

       默认情况下,task执行的算子中,使用了外部变量,每个task都会获取一份变量的副本,比如map,本身数据结构就占用很多内存,有1000个task,这种情况下,map会拷贝1000份,通过网络传输到各个task中,网络传输就会消耗你的spark作业性能;map副本,传输到各个task中会占用内存,不必要的内存消耗和占用可能会引起JVM GC,可能会引起RDD持久化到磁盘。

      将大变量广播出去,不是每一个task一个副本,而是每一个executor一个副本

      广播变量初始的时候,只有在driver上有一份副本,task运行时,要想使用广播变量中的数据,首先会在自己本地的executor对应的BlockManager中,尝试获取变量值,如果本地没有,就会从Driver远程拉取变量副本,并保存在本地BlockManager中;此后,该节点上的task都会从本地BlockManager中获取广播变量的值。

    BlockManager负责管理对应自己的executor上的内存和磁盘上的数据,它可能会从Driver上面去获取变量副本值,也有可能从距离较近的其它节点上拉取数据。


1.5 使用kyro序列化      

          set("spark.serializer","org.apache.spark.serializer.KryoSerializer")

          SparkConf.registerKryoClasses()

          默认情况下,spark内部使用的是java序列化机制,ObjectOutputStream/ ObjectInputStream

         这种序列化机制处理比较方便,不需要我们手动去做什么事情,但是序列化机制效率不高,占用内存空间相对较大

         使用kyro序列化机制,速度要快,占用内存较少。

       

          使用kyro序列化机制之后,生效的地方有以下几个几点:

          ①算子函数中使用到的外部变量

          ②持久化RDD是进行序列化 

                      StorageLevel.MEMORY_ONLY_SER

          ③shuffle过程,节点与节点之间的task会互相进行大量的网络拉取和传输文件,这些数据既然是网络传输,也会进行序列化,序列化之后数据量少,也会提升网络传输的性能


1.6 数据本地化等待时长

          spark将一个application切分成多个基于stage的task分配到executor节点上时,会计算出每个task要计算的哪个分片数据,Spark的task分配算法,会优先将每个task分配到它要计算的数据所在的节点,这样的话,就不需要数据的网络传输了;但是,有些时候,task没有机会分配到数据所在的节点,因为那个节点的计算资源和计算能力都满了,通常在这个时候,spark会等待一段时间,默认是3s,如果在等待一段时间后,还是不行,就选择一种较差的本地化级别将task分配其它节点上。

         spark提供了以下几种数据本地化级别

         ①PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是说在同一个executor中,数据在executor的BlockManager中,性能最好;

         ②NODE_LOCAL:节点本地化,代码和数据在同一个节点中,即数据和task在一个节点的不同executor中,数据需要进程间通信;

         ③NO_PREF : 对于task,在哪个节点上获取数据都一样,比如从mysql中获取数据源

         ④RACK_LOCAL:机架本地化,数据和task处于同一个机架的不用节点上,数据需要网络传输

         ⑤ANY : 数据和task不在同一个机架上,性能最差

        对于应用来说,最好的,当然是进程本地化,直接从本地executor的BlockManager中获取数据,纯内存,或者少量IO操作;如果需要通过网络传输数据的话,性能肯定会下降。

       通过调节spark.locality.wait这个参数,可以调节数据本地化等待时长,默认是3s

       查看spark的日记,starting task ...... PROCESS_LOCAL,就不需要调节该参数,如果是其它本地化数据等待策略,可以考虑调节该值。     

       newSparkConf().set("spark.locality.wait", "10")


2  JVM调优

     该调优对spark性能提升并没有太大影响,不过对线上spark的OOM异常有直接影响

     Spark在一个Executor中的内存分为三块,一块是execution内存,一块是storage内存,一块是other内存。

  • execution内存是执行内存,文档中说join,aggregate都在这部分内存中执行,shuffle的数据也会先缓存在这个内存中,满了再写入磁盘,能够减少IO。其实map过程也是在这个内存中执行的。
  • storage内存是存储broadcast,cache,persist数据的地方。
  • other内存是程序执行时预留给自己的内存。
      发生内存溢出的区域就是 execution区域,因为 storage区域用于缓存RDD,如果不够用会采取一定策略删除部分RDD。


2.1   降低cache内存占比    

      spark.storage.memoryFraction :该参数用于分配Executor内存中storage内存占比;

      Hotspot  JVM堆内存可以细分为年轻代和老年代,年轻代又可以划分为Eden区域、Survivor(From)区域、 Survivor(To)区域;默认占比8:1:1

      我们在执行spark任务的时候,创建的对象都存放在Eden区域;当Eden区域满溢后(spark运行是产生的对象及其多),就会触发minor gc,把不在使用的对象从内存中删除,存活下来的对象放入Survivor(From)区域,但是如果存活下来的对象占比为1.5,一个Survivor区域放不下,此时会将对象直接存放到老年代中;

     在minor gc开始的时候,对象只会存在Eden和Survivor(From)区域,Survivor(To)区域是空的;紧接着进行minor gc,Eden区域中存活的对象会被复制到Survivor(To)区域,而Survivor(From)区域中存活下来的对象,会根据他们的年龄决定去向,年龄达到阈值后将会存入到老年代中,没有达到阈值的对象将会被复制到Survivor(To)区域;经过这次gc之后,Eden区域和From区域已经被清空,这个时候from区域和to区域交换角色,新的from就是上一次的to,不管怎么样,都会保证to区域是空闲的。minor gc一直会重复这个过程。

      如果你的堆内存较小,可能会导致频繁minor gc,会导致短时间内,有些短生命周期对象,年龄过大,进入老年代中。

     老年代中可能会因为堆积一堆短生命周期对象而导致内存不足,进行full gc,该过程很慢,很消费性能;

     不管是minorgc,还是full gc,都会导致stop the world.

     而spark堆内存又被划分成两块,一个用于RDD的cache、persist操作缓存数据使用,另外一部分用来给spark算子函数使用,存放算子中创建的对象等;默认情况下,RDD cache的占比为0.6。可以适当调整该比例。


2.2  堆外内存

      spark引入堆外内存,使之可以直接在节点上的系统内存中开辟空间,存储经过序列化之后的数据。即spark可以直接操作系统的堆外内存,相比堆内内存,堆外内存不受JVM管理,也就没有gc操作,一定程度上提升了性能。

     如果spark作业运行时不时的报错,shufflefile cannot findexecutortasklostoutof memory(内存溢出);有可能是因为堆外内存不够用导致executor挂掉,对应的BlockManager也就不存在了,远程通信拉取不到数据,spark任务崩溃;可以适当提高该参数。    

     --confspark.yarn.executor.memoryOverhead=2048


2.3 连接等待时长

       spark的task会优先在同一个节点的executor中的BlockManager中获取计算数据,但如果此时数据不在同一个节点上,就需要去网络传输拉取数据,但如果那个节点正在进行gc操作,就会没有响应,application卡住,spark默认超时时间为60s,超过这个时间,spark任务宣告失败。    

       --conf spark.core.connection.ack.wait.timeout=300


3 shuffle调优

      spark在执行groupbykey、reducebykey等操作时,shuffle环节的调优很关键,shuffle对spark的作业性能影响很高。

      shuffle分为shuffle write与shuffle fetch

      shuffle write: executor的map端,第一批并行任务的每个task会在map端输出一定数量(与下一个stage的task数量相等)的文件,下一批并行任务的每一个task不会在输出文件,而是复用第一批task创建出的文件。而map端在写数据的时候,会将数据写入内存缓冲区中,当内存缓冲区满溢之后,在spill写入到磁盘落地。(优化后的HashShuffleManager

      shuffle fetch : executor的reduce端,每一个task会从map端的输出文件拉取属于自己的文件并进行聚合运算,计算数据也是先在执行内存(堆内或堆外),该区域满溢,才将reduce结果刷盘。


3.1 合并map端输出文件

       基本上spark的性能都消耗在shuflle,而map端输出文件越多,影响性能越大    

      默认情况下不开启map端文件合并的,这个时候map端输出文件个数为M*R,可以通过参数将该功能开启

      map端磁盘文件IO减少,reduce端拉取文件数量也减少,可以减少网络传输。

      newSparkConf().set("spark.shuffle.consolidateFiles", "true")


3.2 map端缓冲区与reduce端执行内存占比

      默认map端缓冲区的大小为32k,如果不够大,缓冲区内存满溢之后,会进行数据刷盘,多次数据刷盘会导致性能降低,可以适当调大该参数,减少刷盘IO次数

      默认reduce端的executor的内存的0.2划分给shuffle聚合计算,该区域是执行内存,如果内存占比小,刷盘次数也较高,可以适当调大该参数,减少刷盘IO次数

     map端内存缓冲  spark.shuffle.file.buffer  32

     reduce端执行内存占比  spark.shuffle.memoryFraction   0.2


3.3  HashShuffleManagerSortShuffleManager

       不同点:SortShuffleManager会对每个reduce task处理的数据进行排序(默认)

                      SortShuffleManager一个task,只会写入一个磁盘文件,不同task的数据,使用offset来划分界定。

      SortShuffleManager的运行机制有两种:普通运行机制和bypass机制

      ①普通运行机制:数据会先写入到内存中,当达到阈值后,会将内存中的数据按照ket进行排序,排序之后将数据溢写到磁盘并清空内存中数据;一个task将数据写入磁盘,会产生多个临时文件,最后会将所有的临时文件merge为一个文件;

       ②bypass机制:当shuffle map task的数量小于spark.shuffle.sort.bypassMergeThreshold对应的阈值时,将触发bypass机制,该过程与HashShuffleManager过程一致,只不过在最后会merge归并。而该机制与普通SortShuffleManager运行机制的不同在于:不会排序,会减少一部分性能开销。

       ③总结:需不需要进行数据排序,如果不需要建议使用HashShuffleManager;当你的数据需要排序,将使用SortShuffleManager,一定要让你的redude task大于200,否则sort、merge不生效,一定要考量一下,这个过程很耗性能;如果你不需要排序,但希望一个task的文件合并为一个,就使用bypass机制。      

       newSparkConf().set("spark.shuffle.manager", "hash")


4 算子调优


 4.1 map与mapPartitions

               普通的map算子一次只计算一条数据,而mapPartitions一次处理一个分区的数据

               普通的map操作不会导致OOM,mapPartitions可能会出现OOM

               将一些普通map错做转换为mapPartitions

4.2   filter之后数据平衡

             数据经过了filter算子之后,RDD中的每个partition数据量可能就不太一样了

            ①每个partition数据量变少了,但是在后面进行处理的task处理不变,有点浪费资源

            ②每个partition的数据量不一样,会导致后面进行处理的时候,每个task处理的数据量不同,会导致数据倾斜

            ③解决办法: 针对filter之后的RDD调用方法coalesce(int numPartitions) ,减少partition的数量

 4.3  foreach与foreachPartition

            使用foreach,会对每一条数据进行一次算子操作,比如将数据写入数据库中,每执行一条数据,就要创建一个数据库连接

            而使用foreachPartition之后,会将一个partition数据进行一次算子操作

4.4 sparkSQL的并行度

            设置了spark.default.parallelism,使用sparkSQL进行数据源读取的时候,不会有效;sparkSQL默认根据Hive表或者HBase中数据对应的HDFS上文件block,自动设备sparkSQL的并行度,自己定义的并行度之后在没有sparkSQL的stage中生效。

            这个就产生了一个问题?你的第一个stage的并行度只有20个task,每个task处理的数据量太大而导致spark运行缓慢。

           可以对sparkSQL生成的RDD进行重新分区,例如dataFrame.javaRDD().repartition(1000)

4.5 reducebykey本地聚合

           使用了reducebykey算子,会在map端进行本地聚合之后,再将数据拉取到reduce端进行聚合,这样的好处:

           ①本地聚合之后,map端的数据量减少,减少磁盘IO,减少磁盘占用空间

           ②下一个stage拉取数据量也变少,减少网络传输性能消耗

           ③在reduce端进行数据缓存内存占用变少

           ④reduce端要进行聚合的数据也减少


5 报错解决方案


5.1 reduce端缓冲导致OOM

         map端的task是不断的输出数据,数据量可能很大,reduce端的task并不是等到map端task将属于自己的那份数据全部写入磁盘之后才去拉取。而是map端写一点数据,reduce端task就会去拉取一小部分数据,之后立即进行聚合、算子函数的执行。

        每次reduce端拉取数据的大小由reduce端缓冲区buffer决定,拉取过来的数据首先放在buffer区,然后才用executor分配的执行内存去进行后续聚合计算。       

        spark.reducer.maxSizeInFlight  48,可以将该参数调小一点,这样每次拉取的数据量少,就不会导致OOM

        但是如果map端的输出的数据量不是特别大,可以将该参数调大一点,可以减少reduce网络传输次数,减少reduce端聚合操作次数(资源足够的情况下)

5.2  shuffle拉取文件失败

        当map端的JVM进程正在执行minor gc/full gc,就会导致executor进程内的工作线程全部停止,而此时reduce端的task去拉取数据时,等待一段时间后,没有拉取到,可能就会报出:shuffle file not found.      

        spark.shuffle.io.maxRetries 3      shuffle拉取数据失败,最多重试几次(默认3次)

        spark.shuffle.io.retryWait    5         每一次重试拉取数据的时间间隔(默认是5s)

        默认情况下,reduce端的task拉取数据,会反复重试3次,每次时间间隔为5s,也就是会等待15s,就会报出shuffle file not found.

5.3   yarn-client模式导致网卡流量激增

       spark-submit提交作业后, driver进程在本地启动之后,将向ResourceManager申请启动ApplicationMaster,ResourceManager选择其中一个NodeManager节点启动ApplicationMaster进程,ApplicationMaster向ResourceManager申请资源,在NodeManager上启动executor进程,executor反向注册到driver,driver接收到属于自己的executor进程之后,就可以进行task作业的分配与调度进行计算。

        在yarn-client模式下,资源的申请(ApplicationManager)与作业的调度(driver)是分离的。driver是在本地启动,负责任务的调度,需要跟yarn集群中运行的executor进行频繁通信,此时你的本地机器可能会引起网卡流量激增。

        改用yarn-cluster模式。

5.4  yarn-cluster模式JVM内存溢出

          spark-submit提交作业后将向ResourceManager申请启动ApplicationMaster,ResourceManager选择其中一个NodeManager节点启动ApplicationMaster进程,Driver进程在ApplicationMaster进程中启动,之后ApplicationMaster向ResourceManager申请资源,在NodeManager上启动executor进程,executor进程向ApplicationMaster进行反向注册,之后ApplicationMaster进程进行任务的分配与调度进行计算。

          在yarn-cluster模式下,资源的申请(ApplicationManager)与作业的调度(driver)是在一个节点上的。性能上较yarn-client模式佳。

          driver进程运行在JVM的PermGen(方法区)中,在yarn-client模式下,会默认配置128M,如果在yarn-cluster模式下,默认就是82M,可能会PermGen out of Memory error。          

         在spark-submit 脚本提交时添加 --conf spark.driver.extraJavaOptions="-XX:PermSize=128M-XX:MaxPermSize=256M"

         而在spark sql中,如果有很多or语句等,会报出jvm stack overflow,JVM栈内存溢出,建议不要搞这么复杂的sql语句,将复杂的sql语句拆分开进行运算

5.5 checkpoint的使用

         持久化RDD大部分正常,但是有时候会出现内存中某一部分数据丢失,或者存储在磁盘上的文件丢失,这个时候就会出现问题;这种情况下就可以选择对这个RDD进行checkpoint操作,进行checkpoint操作,会将RDD的数据,持久化一份到指定的文件系统上(HDFS)


6 数据倾斜

        同一个stage中各个task处理的数据差距较大,比如其中一个task处理的数据有100万,另外一个task处理1万条数据,造成数据倾斜。

        出现数据倾斜的原因只可能发生了shuffle操作,因为某个key,或者某些key对应的数据量远远高于其他的key。

6.1  聚合源数据

        spark的源数据来源于hive(HDFS)表,而这些hive表中的数据一般也是经过hive etl生成的。如果hive etl生成的数据倾斜,可以直接在etl过程中,对数据进行聚合,比如按照key进行分组,并将value采用一种特殊的格式拼接到一个字符串中。这样一个key就对应一条数据,在spark作业中就不需要对key进行groupbykey操作了。可能就不需要shuffle操作了,也就没有数据倾斜问题。

         你可能没有办法就一个key的数据聚合成一条数据,可以将聚合粒度放粗,比如数据中包括city字段,可以先按照city进行聚合,将原先的数据量减少,减轻数据倾斜的现象和问题。

6.2  过滤导致倾斜的key

       如果你能够接受某些数据直接丢弃,就可以将数据倾斜的key直接在spark SQL中使用where条件过滤掉

6.3  shuffle reduce端并行度

       提高shuffle reduce端task数量,就可以让每一个task计算的数据量变少,可能缓解甚至基本解决掉数据倾斜。

       在进行shuffle算子的时候,后面在传一个参数,该参数代表task的数量。这个没有从根本上解决数据倾斜,只是尽可能缓解数据倾斜问题

6.4   随机key实现双重聚合

       将原始数据的key加上一个10以内的随机数作为前缀,将数据按照新的key进行聚合计算,之后在将结果中的key的前缀去掉,再次进行聚合计算。这样做的目的就是将原来一样的key转换成不一样的key。

       使用场景:groupbykey、reducebykey

		/**
		 * 第一步,给每个key打上一个随机数
		 */
		JavaPairRDD mappedClickCategoryIdRDD = clickCategoryIdRDD.mapToPair(
				
				new PairFunction, String, Long>() {

					private static final long serialVersionUID = 1L;
		
					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						Random random = new Random();
						int prefix = random.nextInt(10);
						return new Tuple2(prefix + "_" + tuple._1, tuple._2);
					}
					
				});
		
		/**
		 * 第二步,执行第一轮局部聚合
		 */
		JavaPairRDD firstAggrRDD = mappedClickCategoryIdRDD.reduceByKey(
				
				new Function2() {

					private static final long serialVersionUID = 1L;

					@Override
					public Long call(Long v1, Long v2) throws Exception {
						return v1 + v2;
					}
					
				});
		
		/**
		 * 第三步,去除掉每个key的前缀
		 */
		JavaPairRDD restoredRDD = firstAggrRDD.mapToPair(
				
				new PairFunction, Long, Long>() {

					private static final long serialVersionUID = 1L;
		
					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						long categoryId = Long.valueOf(tuple._1.split("_")[1]);  
						return new Tuple2(categoryId, tuple._2);  
					}
					
				});
		
		/**
		 * 第四步,第二轮全局的聚合
		 */
		JavaPairRDD clickCategoryId2CountRDD = restoredRDD.reduceByKey(
				
				new Function2() {

					private static final long serialVersionUID = 1L;

					@Override
					public Long call(Long v1, Long v2) throws Exception {
						return v1 + v2;
					}
					
				});


6.5 将reduce  join转换为map join

        使用场景:join

        将其中一个RDD的作为广播变量,之后在mapPartitions的时候,手工进行join。未走shuffle,就不会导致数据倾斜。

       

                /**
		 * reduce join转换为map join
		 */
		
		List> userInfos = userid2InfoRDD.collect();
		final Broadcast>> userInfosBroadcast = sc.broadcast(userInfos);
		
		JavaPairRDD sessionid2FullAggrInfoRDD = userid2PartAggrInfoRDD.mapToPair(
				
				new PairFunction, String, String>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						// 得到用户信息map
						List> userInfos = userInfosBroadcast.value();
						
						Map userInfoMap = new HashMap();
						for(Tuple2 userInfo : userInfos) {
							userInfoMap.put(userInfo._1, userInfo._2);
						}
						
						// 获取到当前用户对应的信息
						String partAggrInfo = tuple._2;
						Row userInfoRow = userInfoMap.get(tuple._1);
						
						String sessionid = StringUtils.getFieldFromConcatString(
								partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
						
						int age = userInfoRow.getInt(3);
						String professional = userInfoRow.getString(4);
						String city = userInfoRow.getString(5);
						String sex = userInfoRow.getString(6);
						
						String fullAggrInfo = partAggrInfo + "|"
								+ Constants.FIELD_AGE + "=" + age + "|"
								+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
								+ Constants.FIELD_CITY + "=" + city + "|"
								+ Constants.FIELD_SEX + "=" + sex;
						
						return new Tuple2(sessionid, fullAggrInfo);
					}
					
				});


6.6    sample采样将数据倾斜的key单独处理

          通过数据采样,将出现次数最多的一个或者几个key的数据单独抽取出来,原来的一个RDD就变成两个RDD,这两个RDD在分别进行join操作,join操作之后生成两个RDD,在将这两个RDD进行join合并为最终RDD。如下图:

 spark性能调优与数据倾斜_第1张图片

           使用场景:优先使用6.5节;如果两个RDD中数据差不多,就使用6.6节。案例代码如下:

           不适用场景:导致数据倾斜的RDD的key特别多。

 

		JavaPairRDD sampledRDD = userid2PartAggrInfoRDD.sample(false, 0.1, 9);
		
		JavaPairRDD mappedSampledRDD = sampledRDD.mapToPair(
				
				new PairFunction, Long, Long>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						return new Tuple2(tuple._1, 1L);
					}
					
				});
		
		JavaPairRDD computedSampledRDD = mappedSampledRDD.reduceByKey(
				
				new Function2() {

					private static final long serialVersionUID = 1L;
		
					@Override
					public Long call(Long v1, Long v2) throws Exception {
						return v1 + v2;
					}
					
				});
		
		JavaPairRDD reversedSampledRDD = computedSampledRDD.mapToPair(
				
				new PairFunction, Long, Long>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						return new Tuple2(tuple._2, tuple._1);
					}
					
				});
		
		final Long skewedUserid = reversedSampledRDD.sortByKey(false).take(1).get(0)._2;  
		
		JavaPairRDD skewedRDD = userid2PartAggrInfoRDD.filter(
				
				new Function, Boolean>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Boolean call(Tuple2 tuple) throws Exception {
						return tuple._1.equals(skewedUserid);
					}
					
				});
			
		JavaPairRDD commonRDD = userid2PartAggrInfoRDD.filter(
				
				new Function, Boolean>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Boolean call(Tuple2 tuple) throws Exception {
						return !tuple._1.equals(skewedUserid);
					}
					
				});
		
		JavaPairRDD skewedUserid2infoRDD = userid2InfoRDD.filter(
				
				new Function, Boolean>() {

					private static final long serialVersionUID = 1L;
		
					@Override
					public Boolean call(Tuple2 tuple) throws Exception {
						return tuple._1.equals(skewedUserid);
					}
					
				}).flatMapToPair(new PairFlatMapFunction, String, Row>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Iterable> call(
							Tuple2 tuple) throws Exception {
						Random random = new Random();
						List> list = new ArrayList>();
						
						for(int i = 0; i < 100; i++) {
							int prefix = random.nextInt(100);
							list.add(new Tuple2(prefix + "_" + tuple._1, tuple._2));
						}
						
						return list;
					}
					
				});
		
		JavaPairRDD> joinedRDD1 = skewedRDD.mapToPair(
				
				new PairFunction, String, String>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						Random random = new Random();
						int prefix = random.nextInt(100);
						return new Tuple2(prefix + "_" + tuple._1, tuple._2);
					}
					
				}).join(skewedUserid2infoRDD).mapToPair(
						
						new PairFunction>, Long, Tuple2>() {

							private static final long serialVersionUID = 1L;
		
							@Override
							public Tuple2> call(
									Tuple2> tuple)
									throws Exception {
								long userid = Long.valueOf(tuple._1.split("_")[1]);  
								return new Tuple2>(userid, tuple._2);  
							}
							
						});
		
		JavaPairRDD> joinedRDD2 = commonRDD.join(userid2InfoRDD);
		
		JavaPairRDD> joinedRDD = joinedRDD1.union(joinedRDD2);
		
		JavaPairRDD sessionid2FullAggrInfoRDD = joinedRDD.mapToPair(
				
				new PairFunction>, String, String>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(
							Tuple2> tuple)
							throws Exception {
						String partAggrInfo = tuple._2._1;
						Row userInfoRow = tuple._2._2;
						
						String sessionid = StringUtils.getFieldFromConcatString(
								partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
						
						int age = userInfoRow.getInt(3);
						String professional = userInfoRow.getString(4);
						String city = userInfoRow.getString(5);
						String sex = userInfoRow.getString(6);
						
						String fullAggrInfo = partAggrInfo + "|"
								+ Constants.FIELD_AGE + "=" + age + "|"
								+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
								+ Constants.FIELD_CITY + "=" + city + "|"
								+ Constants.FIELD_SEX + "=" + sex;
						
						return new Tuple2(sessionid, fullAggrInfo);
					}
					
				});


6.7  采用随机数和扩容表进行join

          选取一个RDD,用flatMap将RDD扩容,将该RDD中每一条数据扩为10条;

          将两外一个RDD,做map映射,key加上一个10以内的随机数;

          最后将两个RDD进行join操作;

         局限性:你的两个RDD都很大,无法将RDD扩容很大,10倍扩容只能缓解数据倾斜

		JavaPairRDD expandedRDD = userid2InfoRDD.flatMapToPair(
				
				new PairFlatMapFunction, String, Row>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Iterable> call(Tuple2 tuple)
							throws Exception {
						List> list = new ArrayList>();
						
						for(int i = 0; i < 10; i++) {
							list.add(new Tuple2(0 + "_" + tuple._1, tuple._2));
						}
						
						return list;
					}
					
				});
		
		JavaPairRDD mappedRDD = userid2PartAggrInfoRDD.mapToPair(
				
				new PairFunction, String, String>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(Tuple2 tuple)
							throws Exception {
						Random random = new Random();
						int prefix = random.nextInt(10);
						return new Tuple2(prefix + "_" + tuple._1, tuple._2);  
					}
					
				});
		
		JavaPairRDD> joinedRDD = mappedRDD.join(expandedRDD);
		
		JavaPairRDD finalRDD = joinedRDD.mapToPair(
				
				new PairFunction>, String, String>() {

					private static final long serialVersionUID = 1L;

					@Override
					public Tuple2 call(
							Tuple2> tuple)
							throws Exception {
						String partAggrInfo = tuple._2._1;
						Row userInfoRow = tuple._2._2;
						
						String sessionid = StringUtils.getFieldFromConcatString(
								partAggrInfo, "\\|", Constants.FIELD_SESSION_ID);
						
						int age = userInfoRow.getInt(3);
						String professional = userInfoRow.getString(4);
						String city = userInfoRow.getString(5);
						String sex = userInfoRow.getString(6);
						
						String fullAggrInfo = partAggrInfo + "|"
								+ Constants.FIELD_AGE + "=" + age + "|"
								+ Constants.FIELD_PROFESSIONAL + "=" + professional + "|"
								+ Constants.FIELD_CITY + "=" + city + "|"
								+ Constants.FIELD_SEX + "=" + sex;
						
						return new Tuple2(sessionid, fullAggrInfo);
					}
					
				});


7 spark sql中数据倾斜

7.1 聚合源数据

7.2 过滤倾斜key

     在SQL中使用where条件过滤

7.3 提供shuffle并行度

       sqlContext.setConf("spark.sql.shuffle.partitions", "1000");  默认200
7.4 随机key实现双重聚合

 

import java.util.Random;
import org.apache.spark.sql.api.java.UDF2;

public class RandomPrefixUDF implements UDF2 {

	private static final long serialVersionUID = 1L;

	@Override
	public String call(String val, Integer num) throws Exception {
		Random random = new Random();
		int randNum = random.nextInt(10);
		return randNum + "_" + val;
	}
}
public class RemoveRandomPrefixUDF implements UDF1 {

	private static final long serialVersionUID = 1L;

	@Override
	public String call(String val) throws Exception {
		String[] valSplited = val.split("_");
		return valSplited[1];
	}

}

将上诉自定义的UDF注册

sqlContext.udf().register("random_prefix", new RandomPrefixUDF(), DataTypes.StringType);
sqlContext.udf().register("remove_random_prefix", new RemoveRandomPrefixUDF(), DataTypes.StringType);

SQL如下

String _sql = 
				"SELECT "
					+ "product_id_area,"
					+ "count(click_count) click_count,"
					+ "group_concat_distinct(city_infos) city_infos "
				+ "FROM ( "
					+ "SELECT "
						+ "remove_random_prefix(product_id_area) product_id_area,"
						+ "click_count,"
						+ "city_infos "
					+ "FROM ( "
						+ "SELECT "
							+ "product_id_area,"
							+ "count(*) click_count,"
							+ "group_concat_distinct(concat_long_string(city_id,city_name,':')) city_infos " 
						+ "FROM ( "
							+ "SELECT "  
								+ "random_prefix(concat_long_string(product_id,area,':'), 10) product_id_area,"
								+ "city_id,"
								+ "city_name "
							+ "FROM tmp_click_product_basic "
						+ ") t1 "
						+ "GROUP BY product_id_area "
					+ ") t2 "  
				+ ") t3 "
				+ "GROUP BY product_id_area ";  


7.5 reduce join 转换成map join

       ① 可以将表转换成RDD,进行手动map join

       ②spark sql 内置的map join,默认如果有一张表在10M以内,就会将该表进行broadcast,然后执行map  join

       ③自己可以去调节阈值: sqlContext.setConf("spark.sql.autoBroadcastJoinThreshold", "20971520");

7.6 采样倾斜key

       spark sql 不适用,因为是spark core算子

7.7 随机key与扩容表

        spark sql + spark core

		JavaRDD rdd = sqlContext.sql("select * from product_info").javaRDD();
		JavaRDD flattedRDD = rdd.flatMap(new FlatMapFunction() {

			private static final long serialVersionUID = 1L;

			@Override
			public Iterable call(Row row) throws Exception {
				List list = new ArrayList();
				
				for(int i = 0; i < 10; i ++) {
					long productid = row.getLong(0);
					String _productid = i + "_" + productid;
					
					Row _row = RowFactory.create(_productid, row.get(1), row.get(2));
					list.add(_row);
				}
				
				return list;
			}
			
		});
		
		StructType _schema = DataTypes.createStructType(Arrays.asList(
				DataTypes.createStructField("product_id", DataTypes.StringType, true),
				DataTypes.createStructField("product_name", DataTypes.StringType, true),
				DataTypes.createStructField("product_status", DataTypes.StringType, true)));
		
		DataFrame _df = sqlContext.createDataFrame(flattedRDD, _schema);
		_df.registerTempTable("tmp_product_info");  
		
		String _sql = 
				"SELECT "
					+ "tapcc.area,"
					+ "remove_random_prefix(tapcc.product_id) product_id," 
					+ "tapcc.click_count,"
					+ "tapcc.city_infos,"
					+ "pi.product_name,"
					+ "if(get_json_object(pi.extend_info,'product_status')=0,'自营商品','第三方商品') product_status "
				+ "FROM ("
					+ "SELECT "
						+ "area,"
						+ "random_prefix(product_id, 10) product_id,"
						+ "click_count,"
						+ "city_infos "
					+ "FROM tmp_area_product_click_count "
				+ ") tapcc "
				+ "JOIN tmp_product_info pi ON tapcc.product_id=pi.product_id ";

你可能感兴趣的:(spark性能调优与数据倾斜)