Spark性能优化实操经验

Spark调优经验

编程部分

环境创建

IDE中可以通过如下方式创建Spark的上下文,其中master指定了上下文环境,一版在开发或是测试时,指定为local或者local[*]即可,这两种方法都是以本地运行Spark程序,前者代表单线程运行,后者代表多线程运行,如果想要指定具体的线程数量,可以指定为local[4],如果不进行指定,则默认等于机器核数(比如8核CPU那就是8线程)。

在本地运行较大文件时,尽可能不使用local[*],而是使用显式地指定核数,因为如果将所有线程都用于worker计算,可能会导致内存不足的错误。

val spark = SparkSession.builder()
  .master("local[*]")
  .getOrCreate()

如果完成了算子开发及打包后,想要提交到集群运行,那么在代码里就不需要指定master了。比如使用cluster模式提交任务到spark集群,但是代码中指定了master("local[*]"),这时只会在集群的driver中使用local模式运行任务,而不会用到集群资源。所以打包时只需要以下方式创建即可:

val spark = SparkSession.builder()
  .getOrCreate()

重复计算

Spark是以RDD或Dataframe来构成算子流程的,而通常它是不会进行缓存的,以下面为例:

val df = spark
  .read
  .format("csv")
  .load(file_path)
  
columns.map(c=>
  expand(df.select(c._1, time_col, time_col_as_long)
)

expand方法中有action操作会触发计算,当对每一列调用expand方法时,虽然参数里都指定是df,但实际上在触发计算操作时这个df才会真正地被加载数据,也就是说如果column有10列,那么就会出现10次读文件操作,这样会显著降低运行效率。

为了避免重复计算,最好地方法就是把需要经常用到的RDD或Dataframe进行缓存,缓存方式有2种,一种是cache,另一种是persistcache就是将RDD或Dataframe中的数据缓存到内存里,相当于persist(StorageLevel.MEMORY_ONLY),如果数据量过大,这种方式显然会导致内存不足。所以对于大数据集,可以使用persist(StorageLevel.MEMORY_AND_DISK)persist(StorageLevel.MEMORY_AND_DISK_SER)进行缓存,也就是当内存不够的时候将数据缓存到磁盘上,后者会读数据进行序列化,可以进一步减小存储对象大小,但是会影响读写效率。

分区

Spark默认是通过根据HDFS文件进行分区的(通常是128MB分为一个partition),如果数据文件是在本地一个csv中,那一开始读进来就是只有一个partition。我们知道在当运行spark时,Scheduler会根据partition来将stage划分为多个task,每个task由一个core来执行,所以如果只有一个partition,那么即时在之前声明了多线程执行,但因为只分配了一个task,最终也会导致实际运行时只有一个core执行。

带来的问题就是哪怕前面声明了使用多线程,但由于数据本身只在一个分区,且如果没有额外的划分操作(如reduceByKey),那么在运行时也只会使用单核单线程计算。所以可以使用repartition来强制重新分区。

并行计算

Spark的精髓就在于它可以支持并行计算,所以在代码里最好能够将一个大流程划分为多个可以并行执行、互不相交的小流程,比如多使用mapforeach操作等。

Broadcast

如果有一个较小的rdd或dataframe在许多task中都会用到,可以使用broadcast操作将其广播到各个executor上,相当于每台机器都有这样一个数据备份,避免重复计算和数据传输。

窗口函数

针对窗口函数,比如lag,是需要通过over指定window的,在window里通常会以某种方式来将数据分区,比如reduceByKey,每个区对应一个窗口,从而实现计算并行化。

但在某些场景下数据是没法分区的,比如要对全量数据计算两两相邻数据的变化量,在运行时也会提示“所有数据被划分到一个区,会带来严重的性能损耗”。关于这种情况我暂时也没有找到特别好的解决方法,有一点可以注意的是当对全量数据执行窗口函数的时候,如果没法对行分区,那尽可能只使用尽可能少的列。

df.withColumn(variation_name, col(col_name) - lag(col_name, 1).over(fake_window))

df.select(col_name)
  .withColumn(variation_name, col(col_name) - lag(col_name, 1).over(fake_window))

根据测试情况,下面一种执行方法会比上面的执行方法效率提高不少。

Shuffle

除了数据读取,Spark另一个性能消耗重点是Shuffle操作,Shuffle操作通常会在reducegroupjoin等语句被触发,执行Shuffle时数据会在整个spark节点间传输。不过不同的Shuffle算子带来的时间消耗是不同的,这里给出的建议是尽可能少地去使用groupjoin这两种操作(除非有广播RDD,这种情况可以使用join),可以用reduce去替换group

提交部分

提交模式

如果不是在IDE开发或测试的情况下,更推荐用spark-submit去提交job,执行spark-submit要求本地也安装了spark。

spark-submit有很多种提交模式,除了前面提到的local模式,还有集群模式、yarn模式等,这里只考虑集群模式,也就是把job提交到spark standalone集群。

提交到集群有2中方式,分别是client模式和cluster模式。

  • client模式:它代表在本地启动driver,进行任务分配,由cluster启动worker,进行具体的任务执行,然后任务执行结果会返回给driver进行整合及返回。因为这种方式会涉及到大量的本地-集群间通讯,所以效率比较低下。它使用7077端口,提交方式如下:
spark-submit --master spark://ip:7077
  • cluster模式:它代表driver和worker都在集群节点上创建,本地把job提交后就和之后的执行没有任何关系了。在集群上,driver会随机选择一个节点创建,也就是说有一个节点会既有driver,也有worker,因此在分配内存和核数时也需要注意这一点(比如executor内存为30GB,分配driver内存6GB,那么worker最大也只能分配24GB。这种方式的好处是节点间通信都在集群内部,效率会高一些,但使用这种方式,本地提交后是无法直接获得程序输出的,只可以通过文件等形式间接读取结果。它使用6066端口,提交方式如下
spark-submit --master spark://ip:6066 --deploy-mode cluster 

提交参数

spark支持各种提交参数配置,这里只例举我使用到的一部分:

  • –num-executors:节点数,可以在集群8080端口的GUI的Alive Workers看到;
  • –executor-cores: 每个节点的核数,集群8080端口的GUI的Cores in use会显示总核数,除以节点数就是每个节点的核数,节点数*每个节点的核数就是分配给该job用于计算的总核数,即最大并行度;
  • –executor-memory:每个节点用于计算的内存大小,集群8080端口的GUI的Memory in use会显示总内存大小,除以节点数就是每个节点的内存大小;
  • –driver-memory:driver内存大小;
  • –conf spark.default.parallelism=?:默认并行度,通常设置为与总核数相等即可;
  • –class:项目启动类,在使用cluster模式提交时这一项似乎是必要的(哪怕在打包时已经指定启动类)。

另外spark 2.2.0版本后支持自动调整spark.storage.memoryFraction和spark.shuffle.memoryFraction,所以这两项是不需要在提交参数里显式设置的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值