spark优化要点(开发)

背景
为什么需要调优??
程序都是能跑的,集群还是那个集群,但是有可能另外一个会调优的人和你写的代码的运行的速度要几倍甚至几十倍

1.开发调优

1.1 原则一:避免创建重复的RDD

我们有一份数据 ,student.txt
第一个需求 :wordCount val stuRDD = sc.textFile(“e://sparkData//student.txt”)
第二个需求:算有多少个学生 val stuRDD01 = sc.textFile(“e://sparkData//student.txt”)
如果创建两份就会加载两次,浪费性能。但是根据我们的需要来说,同样的算子,需要使用两次,那怎么办??
进行持久化即可:
sc.textFile(“e://sparkData//student.txt”).cache()

1.2 原则二:尽可能使用同一个RDD

这种是大家在开发中,经常写着写着就忘记了
举个例子:

    val namesRDD = starsRDD.map(_._1)
    val name2LengthRDD = namesRDD.map(name => (name,name.length))

    // 这两个map是可以合并的
    val name2LengthRDD01 =  starsRDD.map(tuple => (tuple._1,tuple._1.length))
	下面的这种方式写RDD性能更优,因为减少了一次RDD的计算
1.3 原则三:对多次使用的RDD进行持久化

要注意的持久化级别的选择:
1.优先采用MEMORY_ONLY,d但是前提是你的内存足够大,否则可能导致OOM(out of memory 异常)
2.如果MEMORY_ONLY内存不足,就采用MEMORY_ONLY_SER持久化级别,序列化之后,把数据占用的内存变少了,但是序列化和之后使用的反序列化得消耗cpu
3.以上是纯内存持久化,速度很快,但是如果MEMORY_ONLY_SER还是内存不够,那么就采用MEMORY_AND_DISK_SER,采用这种策略,会优先的把数据放在内存中,内存不足放入磁盘
4.不建议使用纯的DISK方案,这样很慢,_2在一些特殊场景(Spark Streaming 容错要求更高)使用以外,一般不建议

1.4 原则四:尽量避免使用shuffle类算子

减少分区
Broadcast + map + filter 代替 join

对于join,大表join小表,可以考虑把小表的数据广播到executor中,通过map + filter的操作完成join的功能

1.5 原则五:使用map-side预聚合的shuffle操作

定要使用shuffle操作,无法用map类的算子来替代,那么尽量使用可以map-side预聚合的算子。
就是用reduceByKey 代替groupByKey
如果同样的一个需求,用reduceByKey的性能比groupByKey好很多,可以大大减少数据的网络传输

1.6 原则六:使用高性能的算子

有些需求,很多算子都能使用,但是性能不一样,用性能更高算子解决
例如:
使用reduceByKey/aggregateByKey替代groupByKey
使用mapPartitions替代普通map,使用mapPartitions会出现OOM(内存溢出)的问题
使用foreachPartitions替代foreach,类似mapPartitions替代普通map,相比于上面的来说 ,这是一个action算子 ,读写数据库例子
使用filter之后进行coalesce操作

补充:repartition 和 coalesce 的使用场景
repatriation有shuffle,一般是把分区数变多,目的提高并行度
val rdd02 = rdd01.filter(xxx) --> 有的分区会过滤很多,有的可能过滤的很少
coalesce 一般来说是把分区数变少,就是把分区数合并,
rdd02.coalesce() ,并行度虽然降低了,但是资源利用率更高,反而可能提高性能

如果需要减少的分区特别少,
rdd01 是20个分区–》rdd02: 5个分区, val rdd02 = rdd01.coalesce(5,true)/rdd01.repartition(5),这样比较好

1.7 原则七:广播大变量

好处是:
如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,
只驻留一份变量副本,Executor有多个,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,
可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。

1.8 原则八:使用Kryo优化序列化性能

spark的序列化? java 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]))
1.9 原则九:优化数据结构

对象,字符串,集合都比较占用内存
字符串代替对象
数组代替集合
使用原始类型(比如Int、Long)替代字符串

使用起来太难,不实用

2.0 资源调优

在executor里面,内存会被分为几个部分:
第一块是让task执行我们自己编写的代码时使用,默认是占Executor总内存的20%;
第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%; spark.shuffle.memoryFraction
spark.shuffle.memoryFraction
用来调节executor中,进行数据shuffle所占用的内存大小默认是0.2

第三块是让RDD持久化时使用,默认占Executor总内存的60%。spark.storage.memoryFraction
spark.storage.memoryFraction
用来调节executor中,进行数据持久化所占用的内存大小,默认是0.6

2.1

理解

补充:spark如何配置参数
1.在代码中如何配置参数:

conf.set(key,value)
conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")

对于资源调优,有下面几个参数:
num-executors
作业中,一共给多少个executor
参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。
设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

executor-memory
参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,
而且跟常见的JVM OOM异常,也有直接的关联。
640G内存 32g * 20 = 640G

20个executor
可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,就代表了你的Spark作业申请到的总内存量(也就是所有Executor进程的内存总和),
这个量是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的总内存量最好不要超过资源队列最大总内存的1/3~1/2,避免你自己的Spark作业
占用了队列所有的资源,导致别的同学的作业无法运行。

executor-cores
每个executor有多少个cpu 核心
这个核心指的并不是物理核心,指的是逻辑核心
i7 4核8线程

参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。
同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

driver-memory
给dirver程序分配的内存,当有collect操作的时候,需要把dirver的内存给大一点

spark.default.parallelism
参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

spark.default.parallelism = num-executors * executor-cores (2–3倍)
这样设置之后,那么每个cpu 都是有2-3个task

10个executor 每个executor里面有4个core ,设置spark.default.parallelism = 120
运行executor的core有多少个 40个 ,120/40 = 每个core的task

task的总数,肯定得比分配到cpu core的数量多, 反之浪费资源,一般就是2-3倍比较合适

如何设置并行度:

如何设置一个Spark Application的并行度?

 1.spark.defalut.parallelism   
   默认是没有值的,如果设置了值比如说10,是在shuffle的过程才会起作用(val rdd2 = rdd1.reduceByKey(_+_) 
 	//rdd2的分区数就是10,rdd1的分区数不受这个参数的影响)

      new SparkConf().set(“spark.defalut.parallelism”,500)
	  
 2、如果读取的数据在HDFS上,增加block数,默认情况下split与block是一对一的,而split又与RDD中的partition对应,所以增加了block数,也就提高了并行度。
 3、RDD.repartition,给RDD重新设置partition的数量
 4、reduceByKey的算子指定partition的数量
      val rdd2 = rdd1.reduceByKey(_+_,10)  val rdd3 = rdd2.map.filter.reduceByKey(_+_)
 5、val rdd3 = rdd1.join(rdd2)  
 	rdd3里面partiiton的数量是由父RDD中最多的partition数量来决定,因此使用join算子的时候,增加父RDD中partition的数量。
 6、spark.sql.shuffle.partitions 
 	//spark sql中shuffle过程中partitions的数量

3.spark调优数据倾斜调优

map filter 这种会发生数据倾斜吗??

问题:数据倾斜肯定是某些key发生了数据倾斜,那么如何知道是哪些key倾斜了??
1000万条取2万条出来

val sampledPairs = pairs.sample(false, 0.1)
val sampledWordCounts = sampledPairs.countByKey()
sampledWordCounts.foreach(println(_))

解决:
1.打散
2.过滤

2.脚本里面可以配置参数

//使用格式: 
./bin/spark-submit \
  --class <main-class> \
  --master <master-url> \
  --deploy-mode <deploy-mode> \
  --conf <key>=<value> \
  ... # other options
  <application-jar> \
  
//举例:
./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 \

3.在配置文件中调节参数
conf/spark-defaults.conf配置文件中读取配置选项。 在conf/spark-defaults.conf配置文件中,每行是key-value对,中间可以是用空格进行分割,也可以直接用等号进行分割。

问题 :这三个地方都可以配置参数 ,对于同样的一个参数在三个地方都配置了,而且参数的value不一样,那么到底是那个生效??

优先级 :
在代码中的优先级最高 --》 意味一旦写入,在其他地方都不能修改了,除非出现修改代码,打包运行,这样不可取,除非有些参数写入就不修改了,在这里面配置比较合适
在脚本中优先级其次 --》 非常灵活,一般来说比较适合在这里面写入参数
在配置文件中最低 --》 以上两种的配置参数,都是针对于该应用的参数,配置文件是全局的参数,优先级最低,更加适合写有些全部都需要用到的参数

2.资源调优
3.数据倾斜调优
4.shuffle调优几个部分

spark 优化原则

  1. 尽量让计算操作在一个rdd里面进行
// 错误的做法。
// 有一个<Long, String>格式的RDD,即rdd1。
// 接着由于业务需要,对rdd1执行了一个map操作,创建了一个rdd2,而rdd2中的数据仅仅是rdd1中的value值而已,也就是说,rdd2是rdd1的子集。
JavaPairRDD<Long, String> rdd1 = ...
JavaRDD<String> rdd2 = rdd1.map(...)
// 分别对rdd1和rdd2执行了不同的算子操作。
rdd1.reduceByKey(...)
rdd2.map(...)
//说明: rdd2  是 k-v类型的rdd1 的v 经过某个操作转变过来的 

正确做法:

JavaPairRDD<Long, String> rdd1 = ...  .Cache()
rdd1.reduceByKey(...)
rdd1.map(tuple._2...)

减少了RDD的生成

  1. 尽量减少shuffle
// 传统的join操作会导致shuffle操作。
// 因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。
val rdd3 = rdd1.join(rdd2)
// Broadcast+map的join操作,不会导致shuffle操作。
// 使用Broadcast将一个数据量较小的RDD作为广播变量。
val rdd2Data = rdd2.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)
// 在rdd1.map算子中,可以从rdd2DataBroadcast中,获取rdd2的所有数据。
// 然后进行遍历,如果发现rdd2中某条数据的key与rdd1的当前数据的key是相同的,那么就判定可以进行join。
// 此时就可以根据自己需要的方式,将rdd1当前数据与rdd2中可以连接的数据,拼接在一起(String或Tuple)。
val rdd3 = rdd1.map(rdd2DataBroadcast...)
// 注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用。
// 因为每个Executor的内存中,都会驻留一份rdd2的全量数据。

猜你喜欢

转载自blog.csdn.net/Aying_seeyou/article/details/104153882