Spark调优技术点

注:加*的是spark2.0弃用的性能调优项

1 性能调优

1.1 分配资源

spark‐submit \
‐‐master spark://node1:7077 \
‐‐class cn.itcast.WordCount \
‐‐num‐executors 10 \	# 配置executor的数量
‐‐driver‐memory 6g \	# 配置driver的内存(影响不大)
‐‐executor‐memory 6g \ 	# 配置每一个executor的内存大小
‐‐executor‐cores 3 \ 	# 配置每一个executor的cpu个数
/export/servers/wordcount.jar

1.2 并行度(task数量)

官方推荐并行度为总CPU core 的2~3倍。
spark.default.parallelism 对 SparkSQL以外的spark应用是有效的,但是SparkSQL无法设置并行度,而是默认根据hive表对应的HDFS文件的block自动设置SparkSQL所在阶段的并行度。

//也可以在 submit中设定--conf
SparkConf conf = new SparkConf().set("spark.default.parallelism","500")

举例
50个executor,每个executor有3个cpu core,总共150个cpu core
设置并行度为300-500,即有300-500个task

1.3 广播变量

外部变量会拷贝副本给每一个task
广播变量就仅仅拷贝副本给每一个executor
广播变量拉取的途径有两个,一个是Driver远程拉取变量副本保存在executor的BlockManager中,另一个是就近从其他executor的BlockManager拉取变量副本。

val extractIndexListMapBC:Broadcast[mutable.HashMap[String, ArrayBuffer[Int]]] = sc.broadcast(extractIndexListMap)

1.4 Kryo序列化

kryo序列化后的数据比默认的序列化serialize后的数据小,前者是后者的十分之一。
三个应用场景:
1 外部变量
2 持久化RDD:反复多次被操作的RDD对象需要持久化,以免每次操作都从头计算。
3 shuffle

conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
conf.registerKryoClasses(Array(classOf[CategorySort]))

1.5 fastutil优化数据格式

扩展了Java的集合类库,提供了更小的内存占用和更快的存取速度。

外部变量是大集合
广播变量
kryo序列化
fastutil
涉及到需要消耗性能的遍历或多次存取的集合
  • 广播变量、Kryo序列化、fastutil 不一定对性能产生决定性作用,可能会提速3%-15%(30min 缩短1-5min)
  • shuffle调优,可能提速50%(30min 缩短15min)
  • 算子调优,比如用reduceByKey改写groupByKey,执行本地聚合,可能提速60%(30min 缩短20min)
  • 最根本有效的方法是跟公司申请更多的资源,比如资源更大的YARN队列,可能提速95%(30min 缩短至1min)
// List<Integer> 对应着fastutil的 IntList
Map<String, Map<String, IntList>> fastutilDateHourExtractMap = new HashMap<String, Map<String, IntList>>();

1.6 调节数据本地化等待时长

  • PROCESS_LOCAL 进程本地化
  • NODE_LOCAL 节点本地化
  • NO_PREF 数据从哪里获取都一样,因为task计算的数据不在集群中,比如在MySQL里
  • RACK_LOCAL 机架本地化
  • ANY 数据和task可能在集群的任何地方,且不在同一机架中

在client模式下观察spark作业的运行日志,如果大多数都是PROCESS_LOCAL,就不用调节了。如果大多数都是其他级别,就反复调节数据本地化的等待时长,每次调节后运行,观察日志,看看大部分的task的本地化级别有没有提升、整个spark作业的运行时间有没有缩短。
有时候因为大量的等待时长,spark作业的运行时间反而增加了,出现这种冲突的时候要以运行时间为第一位,不可本末倒置。

//spark.locality.wait.process
//spark.locality.wait.node
//spark.locality.wait.rack

//默认是3s
//可以6s、10s地调节
new SparkConf().set("spark.locality.wait","10")

2 JVM调优

以client模式提交作业,可以直接出现不断刷新的log。观察报错信息,进行JVM调优。

2.1 降低cache操作的内存占比

spark中的堆内存分两块——存储内存和执行内存。

  • 存储内存:用来给RDD的cache、persist操作进行RDD数据缓存
  • 执行内存:用来给spark算子函数运行使用,存放函数中自己创建的对象

可以通过yarn的界面查看spark作业的运行统计,了解每个stage的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长,说明task算子函数创建的对象过多,导致执行内存不够,这时候就可以降低cache操作的内存占比。

SparkConf SparkConf = new SparkConf()
						.set(“Spark.storage.memoryFraction”,0.4)  // 默认是0.6(60%)

spark.storage.memoryFraction 默认0.6,可以0.5 -> 0.4 -> 0.2地调节,边调节边观察。

2.2 增加堆外内存

频繁报错 spark作业崩溃
out of memory
shuffle output file cannot find;
resubmitting task;
executor lost.

executor的堆外内存不够用,导致内存溢出,executor挂掉。后继的stage的task去挂掉的executor拉取shuffle map output文件,但是executor挂掉,关联的block manager也没有了,就报错。
增加executor的堆外内存,也许可以避免报错。
必须在spark-submit的脚本里设置参数,不能在代码里设置参数。

spark‐submit \
# ...
--master yarn-cluster \
# 设置堆外内存2G (至少1G,甚至2G、4G)
--conf spark.yarn.executor.memoeryOverhead=2048

2.3 增加连接等待时长

(file的uuid) not found;
file lost

偶然情况下,上下游两个executor的BlockManager连接,准备拉取数据,正好赶上上游executor在进行GC。如果超过了连接等待时长,GC还没有结束,连接失败spark就会报错,并且重新提交stage,让TaskScheduler提交task。这样会大大延长spark作业的运行时间。
可以考虑调节连接等待时长。
必须在spark-submit的脚本里设置参数,不能在代码里设置参数。

spark‐submit \
# ...
# 设置连接等待时长300s (默认60s)
--conf spark.core.connection.ack.wait.timeout=300

2.4 GC导致的shuffle文件拉取失败

shuffle file not found

下游task与上游task建立连接后多次拉取数据,如果在拉取的过程中,赶上上游task所在executor进行GC,下游task就会等待一段时间spark.shuffle.io.retryWait 默认5s,超时就重试,次数不超过spark.shuffle.io.maxRetries 默认3次
如果executor进行GC的次数较多较久,可以适当调大这两个参数。

3 shuffle调优

spark作业一旦牵扯到shuffle,就有50-90%的性能消耗在shuffle上。10%用来运行map等操作,90%耗费在两种shuffle操作——groupByKey和countByKey上。

shuffle参数调优的三种方法(优先级由高到低,推荐第二种方法):

  1. 在程序中硬编码 例如 sparkConf.set(“spark.shuffle.file.buffer”,“64k”)
  2. 提交 application 时在命令行指定 例如 spark-submit --conf spark.shuffle.file.buffer=64k --conf 配置信息=配置值 …
  3. 修改 SPARK_HOME/conf/spark-default.conf 配置文件

*3.1 合并map端输出文件

输出文件数量减少,网络传输的性能消耗也大大减少。
前提是使用HashShuffleManager这个shuffle类。spark2.0以后的版本已经弃用HashShuffle和优化后的HashShuffle了。

不设置合并:第一个stage的每个task都会给第二个stage的每个task创建一份map端的输出文件;第二个stage的每个task会到上一个stage的各个节点上拉取每一个task输出的属于自己的那一份文件。
不设置合并
设置合并:第一个stage的每个executor同时运行cpu core个task,比如cpu core是2个,并行运行两个task,每个task都创建下一个stage的task数量个文件。这两个task执行完后,就执行另外两个task,这另外两个task不会再重新创建输出文件,而是复用之前的task创建的map端输出文件,将数据写入上一批task的输出文件中。这样,第二个stage的task拉取的每个输出文件中可能包含了多个task给自己的map端输出。
注意,要实现输出文件合并的效果,必须是一批task先执行,然后下一批task再执行,才能复用之前的输出文件。
在这里插入图片描述
shuffle中的写磁盘操作级别就是性能消耗最严重的部分。

//默认是不开启的 "false"
new SparkConf().set("spark.shuffle.consolidataFiles","true")

前面经过性能调优,调大executor数、cpu core数、并行度,后面就需要开启合并map端输出文件,配置这个参数可以提速50%左右。
如果的确不需要SortShuffleManager的排序机制,那么使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

3.2 调节map端内存缓冲与reduce端内存占比

如何判断是否需要调节该参数?看Spark UI,查看详情。如果发现shuffle磁盘的write和read的运行次数过多,就可以考虑调节这两个参数。调节方法是,spark.shuffle.file.buffer参数每次扩大一倍的方式进行调整,spark.shuffle.memoryFraction参数每次增加0.1进行调整。

  • map端输出文件,首先是写到内存上的,内存写满了就溢写到磁盘上,默认给的内存大小是32k。该阶段task处理的数据量比较大的情况下,可能会造成多次的磁盘文件的spill溢写操作,发生大量的磁盘IO,从而降低性能。在在内存资源充足的情况下,可以适当调大该参数,降低写磁盘频率。
  • reduce端用来进行聚合操作的内存,默认给的聚合内存占executor总内存的比例是0.2。这里会和RDD缓存数据和广播变量一起共用存储内存,即spark.storage.memoryFraction=0.6,两部分会互相动态借用。如果shuffle数据量比较大,reduce task拉取过来的数据很多,那么就会频繁发生reduce端聚合内存不够用,频繁发生spill操作,溢写到磁盘上去。而且最要命的是,磁盘上溢写的数据量越大,后面在进行聚合操作的时候,很可能会多次读取磁盘中的数据,进行聚合。
    在这里插入图片描述

实践中发现,合理调节这两个参数,性能会有1%~5%的提升。

3.3 控制shuffle reduce端缓冲大小,以避免OOM

spark.reducer.maxSizeInFlight默认48M
该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。
如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96M),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。
内存不充足的话,则要调小该参数,否则计算太多拉取来的数据的时候,会创建大量的对象,撑爆执行内存,spark程序跑不起来。此时宁可少量多次拉取数据,这是典型的以执行换性能的原理,要先跑起来,其次才考虑调优的问题。

实践中发现,合理调节该参数,性能会有1%~5%的提升。

4 算子调优

4.1 MapPartitions提升Map类操作性能

一个partition有多少条数据,普通的map方法就要执行多少次,而mapPartition则以分区为单位,只执行一次,性能比较高。
前提是分区内数据量不能太大,不然一次性进入内存计算,容易造成OOM,这时候就不能用mapPartition。

在能用mapPartition的情况下,性能可以提高到30%

4.2 filter过后使用coalesce减少分区数量

filter过滤数据后,会出现两种情况:

  1. 各个分区的数据量减少,后继再用相同数量的task处理数据是比较浪费资源的行为;
  2. 各个分区的数据量不均匀,有的过滤得多,有的过滤得少,后继运行会出现数据倾斜。

这就需要coalesce减少分区数量,便于后面task进行计算操作。

4.2 foreachPartition优化写数据库性能

将计算好的数据写入MySQL数据库中,每次写入都要创建一次数据库连接。然而数据库连接的创建和销毁都是非常耗费性能的。
可以用foreachPartition代替foreach,以分区为单位批量写入数据库,减少数据库连接次数,提高效率。
但是和mapPartition一样,要求分区数据不能太多,不然会出现OOM。

4.3 使用repartition解决SparkSQL低并行度的性能问题

SparkSQL无法设置并行度,只能默认根据hive表对应的HDFS文件的block块自动设置SparkSQL所在阶段的并行度。如果不处理的话,SparkSQL所在阶段的所有RDD都是相同的分区数量,直到出现shuffle,下一个阶段的分区数才和手动设置的并行度一样。
为了解决这一问题,可以在SparkSQL读数据后调用repartition算子,转换成分区数更多的RDD。

4.4 reduceByKey本地聚合

如果需要对每个key对应的值进行诸如 累加、累乘、字符串拼接等操作,可以使用reduceByKey。相比普通的shuffle操作(groupByKey),reduceByKey先在map端进行本地聚合,再输出shuffle文件,被reduce端拉取。
这样做对性能的提升体现在以下几个方面:

  1. map端数据量减少,从而减少IO,同时也减少对磁盘空间的占用;
  2. 下一个阶段拉取的数据量减少,从而减少网络传输的性能消耗;
  3. 在reduce端进行数据缓存占用的内存减少了;
  4. reduce端要聚合的数据减少了。

5 trouble shooting 故障排查

5.1

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值