spark优化

rdd的计算对于transformation是lazy形式,只有在ation时才会进行计算。并且计算结果默认是临时的,用过即丢弃。

1.对多次使用的RDD进行(缓存)持久化:

cache/persist后的rdd,没有使用前千万不要unpersist,unpersist就把缓存给清空了。

cache() -- MEMORY_ONLY = persist() = persist(MEMORY_ONLY)
persist()=persist(MEMORY_ONLY) 有四种类型:--MEMORY_ONLY | MEMORY_ONLY_SER | MEMORY_AND_DISK | MEMORY_AND_DISK_SER

cache和unpersisit两个操作比较特殊,他们既不是action也不是transformation。cache会将标记需要缓存的rdd,真正缓存是在第一次被相关action调用后才缓存;unpersisit是抹掉该标记,并且立刻释放内存。

2.使用广播变量:
开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能,函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC(内存回收)的频率。

注:广播出的的变量如果发生了更新,那就在得知更新后进unpersist,然后拿到更新后的数据重新广播出去。

 

3.避免使用collect()导致Driver 内存紧张,尽量少用

4.尽量避免使用shuffle类的算子:
使用广播变量+filter 代替join,避免shuffle,从而优化spark
使用Broadcast将一个数据量较小的RDD作为广播变量

val list1 = List((jame,23), (wade,3), (kobe,24)) 
val list2 = List((jame,cave), (wade,bulls), (kobe,lakers)) 
val rdd1 = sc.makeRDD(list1) 
val rdd2 = sc.makeRDD(list2)
val rdd2Data = rdd2.collect() 
val rdd2Bc = sc.broadcast(rdd2Data) 
def function(tuple: (String,Int)): (String,(Int,String)) ={ 
for(value <- rdd2Bc.value){ 
if(value._1.equals(tuple._1)){
  return (tuple._1,(tuple._2,value._2.toString)) 
  } 
(tuple._1,(tuple._2,null)) 
}
 
val rdd3 = rdd1.map(function(_))

5.使用有combiner的shuffle类算子:
reduceByKey:这个算子在map端是有combiner的,在一些场景中可以使用reduceByKey代替groupByKey算子。

6.使用高性能的算子:
使用mapPartition替代map
使用foreachPartition替代foreach

7.对RDD使用filter进行大量数据过滤之后使用coalesce减少分区数
使用repartitionAndSortWithinPartitions替代repartition与sort类操作
使用repartition和coalesce算子操作分区。

8.Spark官方建议:
尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

 

使用kryo序列化:
Spark中,主要有三个地方涉及到了序列化:
1.在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输。(Driver 端定义对象在Executor使用)
2.将自定义的类型作为RDD的泛型类型时(比如JavaRDD<SXT>,SXT是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
3.使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。
4.task序列化:
Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10
Sparkconf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer").registerKryoClasses(new Class[]{SpeedSortKey.class})

5.自定义类序列化可参考

https://blog.csdn.net/qq_34896163/article/details/85932463

spark-submit提交作业后这个作业会启动一个对应的driver进程。根据部署模式driver可能会在本地或者集群中某个工作节点启动。driver本身会更具我们设置的参数,占用一定量的内存和CPU code,
而driver进程做的第一件事情就是向集群管理器(如yarn)申请运行spark作业需要使用的资源(Executor)。yarn集群管理器会根据我们提交作业是设置的资源参数,在各个节点上启动一定数量的executor,每个executor进程都占有一定的内存和CPU code。
申请到所需资源后,Driver进程就会开始调度和执行我们编写的作业代码了。Driver进程会将我们编写的Spark作业代码分拆为多个stage,每个stage执行一部分代码片段,并为每个stage创建一批task,然后将这些task分配到各个Executor进程中执行。task是最小的计算单元,负责执行一模一样的计算逻辑(也就是我们自己编写的某个代码片段),只是每个task处理的数据不同而已。一个stage的所有task都执行完毕之后,会在各个节点本地的磁盘文件中写入计算中间结果,然后Driver就会调度运行下一个stage。下一个stage的task的输入数据就是上一个stage输出的中间结果。如此循环往复,直到将我们自己编写的代码逻辑全部执行完,并且计算完所有的数据,得到我们想要的结果为止。

  Spark是根据shuffle类算子来进行stage的划分。如果我们的代码中执行了某个shuffle类算子(比如reduceByKey、join等),那么就会在该算子处,划分出一个stage界限来。可以大致理解为,shuffle算子执行之前的代码会被划分为一个stage,shuffle算子执行以及之后的代码会被划分为下一个stage。因此一个stage刚开始执行的时候,它的每个task可能都会从上一个stage的task所在的节点,去通过网络传输拉取需要自己处理的所有key,然后对拉取到的所有相同的key使用我们自己编写的算子函数执行聚合操作(比如reduceByKey()算子接收的函数)。这个过程就是shuffle。

  当我们在代码中执行了cache/persist等持久化操作时,根据我们选择的持久化级别的不同,每个task计算出来的数据也会保存到Executor进程的内存或者所在节点的磁盘文件中。

task的执行速度是跟每个Executor进程的CPU core数量有直接关系的。
一个CPU core同一时间只能执行一个线程。而每个Executor进程上分配到的多个task,都是以每个task一条线程的方式,多线程并发运行的。如果CPU core数量比较充足,而且分配到的task数量比较合理,那么通常来说,可以比较快速和高效地执行完这些task线程。

--executor-cores:参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。
Executor的CPU core数量设置为2~4个较为合适,executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,避免影响其他同事的作业运行。

--executor-memory:该参数用于设置每一个Executor进程的内存。Executor内存的大小,很多时候直接决定了spark作业的性能,而且跟常见的JVM OOM异常,也有直接关联
每一个Executor进程的内存设置为4G~8G较为合适,但是这也是一个参考值,具体的设置还是得根据不同部门的资源队列来定。申请的总内存量最好不要超过资源队列最大总内存的1/3~1/2,避免你自己的Saprk作业占用了队列所有的资源,导致别的同事的作业无法正常运行

--totoal-executor-cores:指定当前application使用多少core
--driver-cores

--driver-memory:该参数用于设置Driver进程的内存。
Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。
以上的参数也可以配置在默认的配置文件:spark/conf/spark-defaults.conf,这里优先级最高

并行度调节:
设置分区数
(1)sc.textFile(xx,minnumpartition) java/scala
(2)sc.parallelize(xx.num) --java/scala
(3)sc.makeRDD(xx,num) --scala
(4)sc.parallelizePairs(xx,num) --java


(1)介绍:在执行spark的应用程序时,spark集群会启动driver和executor两种JVM进程。
   - driver为主控进程,负责创建sparkContext上下文对象,提交spark作业,并将作业转化为计算任务,在各个executor进程间协调任务的调度(一个)
   - executor进程,负责为工作节点执行具体的计算任务,并将结果返回给driver(collect就在driver上),同时需要持久化RDD提供存储功能。(多个)

spark 算子的用法:https://zhuanlan.zhihu.com/p/68910426

spark资源调度:
1集群启动->worker节点向master节点汇报资源情况,master掌握资源情况
2.spark提交作业后->会启动一个driver->在driver端创建两个对象DAGScheduler和TaskScheduler
3.DAGScheduler对象是任务调度的高层调度器,主要作用就是将DAG根据RDD之间的宽窄依赖关系划分为一个个的Stage,
然后将这些Stage以TaskSet的形式提交给TaskScheduler(TaskScheduler是任务调度的低层调度器,这里TaskSet其实就是一个集合,里面封装的就是一个个的task任务,也就是stage中的并行度task任务)
4.TaskSchedule会遍历TaskSet集合,拿到每个task后会将task发送到计算节点Executor中去执行
5.task在Executor线程池中的运行情况会向TaskScheduler反馈,当task执行失败时,则由TaskScheduler负责重试,将task重新发送给Executor去执行,默认重试3次。
如果重试3次依然失败,那么这个task所在的stage就失败了。
stage失败了则由DAGScheduler来负责重试,重新发送TaskSet到TaskSchdeuler,Stage默认重试4次。如果重试4次以后依然失败,那么这个job就失败了。
job失败了,Application就失败了。因此一个task默认情况下重试3*4=12次。
6.TaskScheduler不仅能重试失败的task,还会重试straggling(落后,缓慢)task(也就是执行速度比其他task慢太多的task)。
如果有运行缓慢的task那么TaskScheduler会启动一个新的task来与这个运行缓慢的task执行相同的处理逻辑。
两个task哪个先执行完,就以哪个task的执行结果为准。这就是Spark的推测执行机制。在Spark中推测执行默认是关闭的。推测执行可以通过spark.speculation属性来配置。
spark-submit提交作业后这个作业会启动一个对应的driver进程。根据部署模式driver可能会在本地或者集群中某个工作节点启动。driver本身会更具我们设置的参数,占用一定量的内存和CPU

资源粒度
粗粒度资源申请(Spark):Applicatioin执行之前,将所有的资源申请完毕,task直接执行在已申请好的资源上,效率高,资源利用率低。
细粒度资源申请(MapReduce):Application执行之前不需要先去申请资源,而是在job内的task执行前自己取申请资源,然后执行,效率低,资源利用率高

 

posted @ 2020-01-10 14:58  ~清风煮酒~  阅读(12)  评论(0编辑  收藏  举报