Spark性能调优:调度分配更多资源

来源:互联网 发布:人工智能计算器 iphone 编辑:程序博客网 时间:2024/05/29 14:08

资源调优概述

性能调优的王道就是增加和分配更多的资源,性能和速度上的提升是显而易见的。基本上,在一定范围之内,增加资源与性能的提升是成正比的。Spark的资源参数,基本都可以在spark-submit命令中作为参数设置。资源参数设置的不合理,可能会导致没有充分利用集群资源,作业运行会极其缓慢;或者设置的资源过大,队列没有足够的资源来提供,进而导致各种异常。因此我们必须对Spark作业的资源使用原理有一个清晰的认识,并知道在Spark作业运行过程中,有哪些资源参数是可以设置的,以及如何设置合适的参数值。


Spark作业基本运行原理

详细原理见上图:
我们使用spark-submit提交一个Spark作业之后,这个作业就会启动一个对应的Driver进程。根据你使用的部署模式(deploy-mode)不同,Driver进程可能在本地启动,也可能在集群中某个工作节点上启动。Driver进程本身会根据我们设置的参数,占有一定数量的内存和CPU core。而Driver进程要做的第一件事情,就是向集群管理器(可以是Spark Standalone集群,也可以是其他的资源管理集群如 YARN作为资源管理集群)申请运行Spark作业需要使用的资源,这里的资源指的就是Executor进程。YARN集群管理器会根据我们为Spark作业设置的资源参数,在各个工作节点上,启动一定数量的Executor进程,每个Executor进程都占有一定数量的内存和CPU core。

在申请到了作业执行所需的资源之后,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进程的内存或者所在节点的磁盘文件中。

因此Executor的内存主要分为三块:第一块是让task执行我们自己编写的代码时使用,默认是占Executor总内存的20%;第二块是让task通过shuffle过程拉取了上一个stage的task的输出后,进行聚合等操作时使用,默认也是占Executor总内存的20%;第三块是让RDD持久化时使用,默认占Executor总内存的60%。

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

以上就是Spark作业的基本运行原理的说明,大家可以结合上图来理解。理解作业基本原理,是我们进行资源参数调优的基本前提。


分配哪些资源?

主要是多分配executor、cpu per executor、memory per executor、driver memory、spark.default.parallelism、spark.storage.memoryFraction


在哪里分配这些资源?

num-executors

  • 该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

executor-memory

  • 该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

executor-cores

  • 该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

driver-memory

  • 该参数用于设置Driver进程的内存。Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism

  • 该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。
  • 参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

spark.storage.memoryFraction

  • 参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。
  • 参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

spark.shuffle.memoryFraction

  • 参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。
  • 参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

在我们在生产环境中,提交spark作业时,用的spark-submit shell脚本,里面调整对应的参数

spark-submit \--master yarn-cluster \--class cn.spark.sparktest.core.WordCountCluster \--num-executors 20 \ 配置executor的数量--driver-memory 500m \ 配置driver的内存(影响不大)--executor-memory 2G \ 配置每个executor的内存大小--executor-cores 6 \ 配置每个executor的cpu core数量--conf spark.default.parallelism=360 \ 每个stage的task数量:num-executors * executor-cores *(2~3--conf spark.storage.memoryFraction=0.5 \ rdd持久化在executor内存中占的比例,默认0.6--conf spark.shuffle.memoryFraction=0.3 \ shuffle聚合操作数据可以使用的executor内存比例,默认0.2/usr/local/SparkTest.jar \

调节到多大算是最大呢?

资源参数的调优,没有一个固定的值,需要根据自己的实际情况(包括Spark作业中的shuffle操作数量、RDD持久化操作数量以及spark web ui中显示的作业gc情况),合理地设置参数。

  • 第一种情况,如果使用的是Spark Standalone模式,公司集群上搭建了一套Spark集群,你心里应该清楚每台机器还能够给你使用的大概有多少内存,多少cpu core,那么,设置的时候就根据这个实际的情况去调节每个spark作业的资源分配。比如说你的每台机器能够给你使用4G内存,2个cpu core。一共有20台机器,那么启动20个executor,平均每一个executor分配4G内存,2个cpu core。

  • 第二种情况是基于Yarn的集群。因为Yarn 使用资源队列资源调度。应该去查看你的spark作业要提交到的资源队列大概有多少资源?比如资源队列总共有500G内存,100个cpu core,如果自己的spark作业准备使用50个executor,平均每个executor就是10G内存,2个cpu core。

一个原则:你能使用的资源有多大就尽量去调节到最大的大小(包括executor的数量,大约在几十个到上百个不等,然后executor内存和executor cpu core 调节到最大)。


为什么调节了资源以后,性能可以提升?

这里写图片描述

以Standalone集群模式举例,首先Driver里运行Application,并且Application有自己的SparkContexxt,SparkContext里的DAGScheduler和TaskScheduler会将我们的算子切割成大量的task提交到Application的executor上面去执行,可能一个Executor会分配100个task。

增加executor
如果executor数量比较少,那么,能够并行执行的task数量就比较少,就意味着,我们的Application的并行执行的能力就很弱。
比如有3个executor,每个executor有2个cpu core,那么同时能够并行执行的task就是6个。6个执行完以后,再换下一批6个task。当增加了executor数量以后,就意味着,能够并行执行的task数量也就变多了,比如原先是6个,现在可能可以并行执行10个,甚至20个,100个。那么并行能力就比之前提升了数倍,数十倍,相应的,性能(执行的速度),也能提升数倍~数十倍。

增加每个executor的cpu core
增加每个executor的cpu core也是增加了执行的并行能力。比如原本20个executor,每个才2个cpu core,能够并行执行的task数量,就是40个task。现在每个executor的cpu core增加到了5个,那么能够并行执行的task数量就是100个task,执行的速度就提升了2.5倍。

增加每个executor的内存量
增加了内存量以后,对性能的提升大概有三点:
1. 如果需要对RDD进行cache,那么更多的内存就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘,从而减少了磁盘IO。
2. 对于shuffle操作,在reduce端会需要内存来存放拉取的数据并进行聚合。如果内存不够,也会写入磁盘。如果给executor分配更多内存以后,就有更少的数据需要写入磁盘,甚至不需要写入磁盘,从而减少了磁盘IO,提升了性能。
3. 对于task的执行,可能会创建很多对象。如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC(垃圾回收,包括minor GC和full GC),频繁垃圾回收速度会很慢。内存加大以后,带来更少的GC,避免了速度变慢,从而速度变快了。