spark优化指南

目录

一、代码优化

1. 基本原则

2. 算子优化

2.1 reduceByKey/aggregateByKey替代groupByKey

2.2 mapPartitions(foreachPartitions)替代map(foreach)

2.3 使用filter之后进行coalesce操作

2.4 repartitionAndSortWithinPartitions替代repartition与sort类操作

二、资源配置

1. 预估内存和cpu

2. 参数设置

2.1 executor-memory

2.2 num-executors

2.3 executor-cores

2.4 driver-memory

2.5 spark.default.parallelism

2.6 spark.storage.memoryFraction

2.7 spark.shuffle.memoryFraction


spark作为基于内存、分布式计算框架,具有运算速度快特性。然而,在用spark处理海量数据实际业务中,并不是那么简单的。如果没有对Spark任务进行合理的调优,Spark任务的执行速度会很慢。本文介绍几种常用的调优手段。

一、代码优化

在spark代码开发中,我们应遵循一些基本spark开发原则,并注意算子优化。

1. 基本原则

spark使用dag对rdd的关系进行建模,描述了rdd的依赖关系,这种关系也被称之为lineage。在spark作业中,有以下几个基本原则:

  • 对于同一份数据源,只应该创建一个rdd;
  • 在对不同的数据执行算子操作时还要尽可能地复用一个rdd;
  • 对多次action的rdd进行持久化;
  • 尽量避免使用shuffle类算子,如groupBy、reduceByKey、distinct和join等;
  • 采用kryo序列化;
  • 数据结构优化;
  • broadcast使用;

rdd dag采用lazy,当有action触发才会执行。rdd多次action会导致计算复用性差,可以对rdd持久化内存(或磁盘,一般选用内存cache)。

Spark默认使用的是Java的序列化机制,但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。

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

# 注册要序列化的自定义类型

conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型(如HashMap、LinkedList)。

算子在用到外部变量,应使用broadcast广播变量,会保证每个Executor的内存中,只驻留一份变量副本。减少变量在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC。

val list1 = ...

val list1Broadcast = sc.broadcast(list1)

rdd1.map(list1Broadcast...)

2. 算子优化

2.1 reduceByKey/aggregateByKey替代groupByKey

reduceByKey和aggregateByKey算子都会在shuffle输出数据前,使用用户自定义的函数对每个节点本地的相同key进行预聚合(combine)。而groupByKey算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,磁盘IO以及网络传输开销较大,性能相对来说比较差。

2.2 mapPartitions(foreachPartitions)替代map(foreach)

一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。例如,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。

2.3 使用filter之后进行coalesce操作

通常对一个rdd执行filter算子过滤掉rdd中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少rdd的partition数量,将rdd中的数据压缩到更少的partition中去。因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。

2.4 repartitionAndSortWithinPartitions替代repartition与sort类操作

如果需要在repartition重分区之后,还要进行排序,建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能可能是要高的。

二、资源配置

在提交spark任务时,我们需要对任务参数进行配置。

1. 预估内存和cpu

对于公司集群,不太可能有很多闲置的资源供我们随意挥霍。如何合理预估自己提交任务需要的内存和资源,是必须考虑的。一般处理数据源首先transform成rdd,而rdd:内存=spark.storage.memoryFraction(固定值,默认为0.6)。因此,可以根据数据源大小决定job内存下限。对于hdfs文件,可以用hadoop命令查询其文件大小:

hdfs dfs -du -s -h data/

21.1 G 21.1 G .

假设查询数据大小为N,那么所需内存下限=N/spark.storage.memoryFraction。比如21.1G数据至少需要35.2G内存,至于内存的上限受限于计算过程复杂度,需要自己逐渐往上加内存,一般公司集群queue也会限定内存上限。比如在用ml处理特征,索引编码和分箱会耗费大量内存(20-30倍下限才能运行,个人测试过)。

cpu配置相对轻松点,一般集群core相对多的。只需要满足cpu cores=num-executors * executor-cores<=queue限定cores。

2. 参数设置

在初步确定spark运行任务所需内存和cores之后,接下来就需要精细到driver和executors中。

2.1 executor-memory

该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark任务的性能,而且跟常见的JVM OOM异常,也有直接的关联。一般设置4G-8G。对于数据量大,但处理流程不复杂业务可适当设置小点,如4G;但对于处理流程复杂业务(spark ml特征工程索引、分箱等操作),主要可设置大些,如8-10G。

2.2 num-executors

该参数用于设置spark任务总共要用多少个executor进程来执行,一般和executor-memory、executor-cores配合使用,原则上遵循executor-memory * num-executors * executor-cores=job需要内存,比如总内存设定40G,executor-memory=4G,executor-cores=2,则num-executors=5。

2.3 executor-cores

该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。一般设置为2-4个,和executor-memory、num-executors配合使用,同上。

2.4 driver-memory

该参数用于设置Driver进程的内存。一般设置1-2G即可,但需要注意当对rdd(dataframe)拉取到driver处理,必须确保driver有足够的内存,否则容易出现OOM内存溢出。如rdd大小8G,采用saveAsTextFile持久化hdfs,driver-memory>=13.4G(8/0.6)。

2.5 spark.default.parallelism

该参数用于设置每个stage的默认task数量。如果不设置该参数,spark就会根据hdfs的block数量来设置task数量。需要注意的是,如果executor cores>=task数量,那么spark实际任务以task数量为准,多余executor将被浪费。一般设置该参数为num-executors * executor-cores的2-3倍较为合适。比如executors total cores=10,那么spark.default.parallelism应设置为20-30。

2.6 spark.storage.memoryFraction

该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。

当有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark任务中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现任务由于频繁的gc导致运行缓慢(通过spark web ui可以观察到任务的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

2.7 spark.shuffle.memoryFraction

该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。在shuffle操作时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

当shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现任务由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

参考资料

https://blog.csdn.net/lingbo229/article/details/80729068

https://blog.csdn.net/lingbo229/article/details/80729034

猜你喜欢

转载自blog.csdn.net/baymax_007/article/details/84328174