一、性能调优
1.分配更多资源
/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 \ 配置每个executor的cpu core数量
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
2.调节并行度
官方推荐:task数量,设置成spark application总cpu core数量的2~3倍,比如150个cpu core,基本要设置300-500;
设置方式:
spark.default.parallelism
SparkConf conf = new SparkConf().set("spark.default.parallelism", "500");
3.RDD持久化
4.使用广播变量
在某些情况下,某些算子会用到算子外的变量,这样就会导致算子内的所有task任务都会复制一份外部变量,会增加内存的消耗。使用广播变量后,变成每个节点的executor共用一份变量副本,此后该executor的所有task都共用所在executor的变量副本,大大减少了外部变量副本个数,从而提高了性能。
5.使用Kryo序列化
kryo序列化机制,一旦启用以后,会生效的几个地方:
(1)算子函数中使用到的外部变量
(2)持久化RDD时进行序列化,StorageLevel.MEMORY_ONLY_SER
(3)shuffle
设置方式:SparkConf.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)
6.使用fastutil优化数据格式
fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue;
fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度;
fastutil也提供了64位的array、set和list,以及高性能快速的,以及实用的IO类,来处理二进制和文本类型的文件;
fastutil最新版本要求Java 7以及以上版本。
fastutil其实没有你想象中的那么强大,也不会跟官网上说的效果那么一鸣惊人。广播变量、Kryo序列化类库、fastutil,都是之前所说的,对于性能来说,类似于一种调味品,烤鸡,本来就很好吃了,然后加了一点特质的孜然麻辣粉调料,就更加好吃了一点。分配资源、并行度、RDD架构与持久化,这三个就是烤鸡;broadcast、kryo、fastutil,类似于调料。
7.调节数据本地化等待时长
Spark在Driver上,对Application的每一个stage的task,进行分配之前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;Spark的task分配算法,优先,会希望每个task正好分配到它要计算的数据所在的节点,这样的话,就不用在网络间传输数据;
但是呢,通常来说,有时,事与愿违,可能task没有机会分配到它的数据所在的节点,为什么呢,可能那个节点的计算资源和计算能力都满了;所以呢,这种时候,通常来说,Spark会等待一段时间,默认情况下是3s钟(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后,实在是等待不了了,就会选择一个比较差的本地化级别,比如说,将task分配到靠它要计算的数据所在节点,比较近的一个节点,然后进行计算。
但是对于第二种情况,通常来说,肯定是要发生数据传输,task会通过其所在节点的BlockManager来获取数据,BlockManager发现自己本地没有数据,会通过一个getRemote()方法,通过TransferService(网络数据传输组件)从数据所在节点的BlockManager中,获取数据,通过网络传输回task所在节点。
对于我们来说,当然不希望是类似于第二种情况的了。最好的,当然是task和数据在一个节点上,直接从本地executor的BlockManager中获取数据,纯内存,或者带一点磁盘IO;如果要通过网络传输数据的话,那么实在是,性能肯定会下降的,大量网络传输,以及磁盘IO,都是性能的杀手。
(1)什么时候要调节这个参数?
观察日志,spark作业的运行日志,推荐大家在测试的时候,先用client模式,在本地就直接可以看到比较全的日志。
日志里面会显示,starting task。。。PROCESS LOCAL、NODE LOCAL观察大部分task的数据本地化级别
如果大多都是PROCESS_LOCAL,那就不用调节了
如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长
调节完,应该是要反复调节,每次调节完以后,再来运行,观察日志
看看大部分的task的本地化级别有没有提升;看看,整个spark作业的运行时间有没有缩短
你别本末倒置,本地化级别倒是提升了,但是因为大量的等待时长,spark作业的运行时间反而增加了,那就还是不要调节了
(2)如何调节
new SparkConf().set(“spark.locality.wait”, “10”)
二、JVM调优
1.降低cache操作的内存占比
注意:需复习下spark与jvm内存机制以及新生代老年代的工作机制
spark中,堆内存又被划分成了两块儿,一块儿是专门用来给RDD的cache、persist操作进行RDD数据缓存用的;另外一块儿,就是我们刚才所说的,用来给spark算子函数的运行使用的,存放函数中自己创建的对象。
默认情况下,给RDD cache操作的内存占比,是0.6,60%的内存都给了cache操作了。但是问题是,如果某些情况下,cache不是那么的紧张,问题在于task算子函数中创建的对象过多,然后内存又不太大,导致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作。性能影响会很大。
针对上述这种情况,大家可以在之前我们讲过的那个spark ui。yarn去运行的话,那么就通过yarn的界面,去查看你的spark作业的运行统计,很简单,大家一层一层点击进去就好。可以看到每个stage的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长。此时就可以适当调价这个比例。
解决方法:
SparkConf SparkConf = new SparkConf().set(“Spark.storage.memoryFraction”, “0.4”) // 默认是0.6(60%)
2.调节executor堆外内存与连接等待时长
有时候,如果你的spark作业处理的数据量特别特别大,几亿数据量;然后spark作业一运行,时不时的报错,shuffle file cannot find,executor、task lost,out of memory(内存溢出);
可能是说executor的堆外内存不太够用,导致executor在运行的过程中,可能会内存溢出;然后可能导致后续的stage的task在运行的时候,可能要从一些executor中去拉取shuffle map output文件,但是executor可能已经挂掉了,关联的block manager也没有了;所以可能会报shuffle output file not found;resubmitting task;executor lost;spark作业彻底崩溃。
上述情况下,就可以去考虑调节一下executor的堆外内存。也许就可以避免报错;此外,有时,堆外内存调节的比较大的时候,对于性能来说,也会带来一定的提升。
解决方法:
/usr/local/spark/bin/spark-submit
–class com.ibeifeng.sparkstudy.WordCount
–num-executors 80
–driver-memory 6g
–executor-memory 6g
–executor-cores 3
–master yarn-cluster
–queue root.default
--conf spark.yarn.executor.memoryOverhead=2048
--conf spark.core.connection.ack.wait.timeout=300
/usr/local/spark/spark.jar
${1}
三、shuffle调优
1.合并map端输出文件
shuffle,一定是分为两个stage来完成的。因为这其实是个逆向的过程,不是stage决定shuffle,是shuffle决定stage。
reduceByKey,在某个action触发job的时候,DAGScheduler,会负责划分job为多个stage。划分的依据,就是,如果发现有会触发shuffle操作的算子,比如reduceByKey,就将这个操作的前半部分,以及之前所有的RDD和transformation操作,划分为一个stage;shuffle操作的后半部分,以及后面的,直到action为止的RDD和transformation操作,划分为另外一个stage。
每一个shuffle的前半部分stage的task,每个task都会创建下一个stage的task数量相同的文件,比如下一个stage会有100个task,那么当前stage每个task都会创建100份文件;会将同一个key对应的values,一定是写入同一个文件中的;不同节点上的task,也一定会将同一个key对应的values,写入下一个stage,同一个task对应的文件中。
shuffle的后半部分stage的task,每个task都会从各个节点上的task写的属于自己的那一份文件中,拉取key, value对;然后task会有一个内存缓冲区,然后会用HashMap,进行key, values的汇聚;(key ,values);
task会用我们自己定义的聚合函数,比如reduceByKey(+),把所有values进行一对一的累加;聚合出来最终的值。就完成了shuffle。
(1)如何开启map端输出文件合并机制
new SparkConf().set("spark.shuffle.consolidateFiles", "true")
开启了map端输出文件的合并机制之后:
第一个stage,同时就运行cpu core个task,比如cpu core是2个,并行运行2个task;每个task都创建下一个stage的task数量个文件;
第一个stage,并行运行的2个task执行完以后;就会执行另外两个task;另外2个task不会再重新创建输出文件;而是复用之前的task创建的map端输出文件,将数据写入上一批task的输出文件中。
第二个stage,task在拉取数据的时候,就不会去拉取上一个stage每一个task为自己创建的那份输出文件了;而是拉取少量的输出文件,每个输出文件中,可能包含了多个task给自己的map端输出。
2.调节map端内存缓冲与reduce端内存占比
默认,map端内存缓冲是每个task,32kb。
默认,reduce端聚合内存比例,是0.2,也就是20%。
如果map端的task,处理的数据量比较大,但是呢,你的内存缓冲大小是固定的。可能会出现什么样的情况?
每个task就处理320kb,32kb,总共会向磁盘溢写320 / 32 = 10次。
每个task处理32000kb,32kb,总共会向磁盘溢写32000 / 32 = 1000次。
在map task处理的数据量比较大的情况下,而你的task的内存缓冲默认是比较小的,32kb。可能会造成多次的map端往磁盘文件的spill溢写操作,发生大量的磁盘IO,从而降低性能。
reduce端聚合内存,占比。默认是0.2。如果数据量比较大,reduce task拉取过来的数据很多,那么就会频繁发生reduce端聚合内存不够用,频繁发生spill操作,溢写到磁盘上去。而且最要命的是,磁盘上溢写的数据量越大,后面在进行聚合操作的时候,很可能会多次读取磁盘中的数据,进行聚合。
默认不调优,在数据量比较大的情况下,可能频繁地发生reduce端的磁盘文件的读写。
这两个点之所以放在一起讲,是因为他们俩是有关联的。数据量变大,map端肯定会出点问题;reduce端肯定也会出点问题;出的问题是一样的,都是磁盘IO频繁,变多,影响性能。
调节方法:
调节map task内存缓冲:spark.shuffle.file.buffer,默认32k(spark 1.3.x不是这个参数,后面还有一个后缀,kb;spark 1.5.x以后,变了,就是现在这个参数)
调节reduce端聚合内存占比:spark.shuffle.memoryFraction,0.2
3.HashShuffleManager与SortShuffleManager
SortShuffleManager与HashShuffleManager两点不同:
(1)SortShuffleManager会对每个reduce task要处理的数据,进行排序(默认的)。
(2)SortShuffleManager会避免像HashShuffleManager那样,默认就去创建多份磁盘文件。一个task,只会写入一个磁盘文件,不同reduce task的数据,用offset来划分界定。
①需不需要数据默认就让spark给你进行排序?就好像mapreduce,默认就是有按照key的排序。如果不需要的话,其实还是建议搭建就使用最基本的HashShuffleManager,因为最开始就是考虑的是不排序,换取高性能;
②什么时候需要用sort shuffle manager?如果你需要你的那些数据按key排序了,那么就选择这种吧,而且要注意,reduce task的数量应该是超过200的,这样sort、merge(多个文件合并成一个)的机制,才能生效把。但是这里要注意,你一定要自己考量一下,有没有必要在shuffle的过程中,就做这个事情,毕竟对性能是有影响的。
spark.shuffle.manager:hash、sort、tungsten-sort
new SparkConf().set("spark.shuffle.manager", "hash")
new SparkConf().set("spark.shuffle.manager", "tungsten-sort")
// 默认就是,new SparkConf().set("spark.shuffle.manager", "sort")
new SparkConf().set("spark.shuffle.sort.bypassMergeThreshold", "550")
四、算子调优
1.MapPartitions提升Map类操作性能
当数据量比较大的时候,不建议用mapPartitions,因为分区后,可能某个分区数据量过大,导致OOM;而使用map算子时,在处理完一部分数据且内存不够时,会进行回收,通常不会出现OOM;所以数据量较小时,可以考虑使用mapPartitions算子进行调优
2.filter过后使用coalesce减少分区数量
在使用filter算子后,可能会出现某些分区数量较多,某些分区数量较少,这样就会导致数据倾斜,建议使用coalesce进行分区压缩
3.使用foreachPartition优化写数据库性能
使用foreach算子会进行大量的内存消耗,尤其是访问数据库,而当使用foreachPartition,则一个分区调用一次function,大大减少了内存消耗和数据库访问,但是要注意的是,如果分区后数据量很大,会导致OOM,这个时候就不适合使用。
4.使用reparation解决SparkSQL低并行度的性能问题
spark并行度的调节策略对于SparkSQL是不生效的,用户设置的并行度只对于SparkSQL以外的spark的stage生效。
SparkSQL的并行度不允许用户自己指定,SparkSQL自己会默认根据hive表对应的HDFS文件的split个数自动设置SparkSQL所在的那个stage的并行度,用户设置的并行度只对于SparkSQL以外的spark的stage生效。
由于SparkSQL所在stage的并行度无法手动设置,如果数据量较大,并且此stage中后续的transformation操作有着复杂的业务逻辑,而SparkSQL自动设置的task数量很少,这就意味着每个task要处理为数不少的数据量,然后还要执行非常复杂的处理逻辑,这就可能表现为第一个有SparkSQL的stage速度很慢,而后续没有SparkSQL的stage运行速度非常快。
为了解决SparkSQL无法设置并行度和task数量的问题,可以使用reparation算子。
SparkSQL这一步的并行度和task数量肯定是没有办法去改变了,但是,对于SparkSQL查询出来的RDD,立即使用reparation算子,去重新进行分区,这样可以重新分区为多个partition,从reparation之后的RDD操作,由于不再设计SparkSQL,因此stage的并行度就会等于你手动设置的值,这样就避免了SparkSQL所在的stage只能用少量的task处理大量数据并执行复杂的算法逻辑。
5.reduceByKey预聚合
对map端给下个stage每个task创建的输出文件中,写数据之前,就会进行本地的combiner操作,也就是说对每一个key,对应的values,都会执行你的算子函数( + _)
用reduceByKey对性能的提升:
①在本地进行聚合以后,在map端的数据量就变少了,减少磁盘IO。而且可以减少磁盘空间的占用
②下一个stage,拉取数据的量,也就变少了。减少网络的数据传输的性能消耗
③在reduce端进行数据缓存的内存占用变少了
④reduce端,要进行聚合的数据量也变少了
reduceByKey在什么情况下使用呢?
①非常普通的,比如说,就是要实现类似于wordcount程序一样的,对每个key对应的值,进行某种数据公式或者算法的计算(累加、类乘)
②对于一些类似于要对每个key进行一些字符串拼接的这种较为复杂的操作,可以自己衡量一下,其实有时,也是可以使用reduceByKey来实现的。但是不太好实现。如果真能够实现出来,对性能绝对是有帮助的。(shuffle基本上就占了整个spark作业的90%以上的性能消耗,主要能对shuffle进行一定的调优,都是有价值的)