Spark优化和故障处理

1 Spark性能优化

1.1 直接方式

直接从硬件下手(内存、磁盘、机器…)

1.2 常规性能调优

1.2.1 最优资源配置

最优资源配置:为当前任务分配更多的资源,在一定的范围内,增加资源的分配和性能是成正比的,尽量将任务分配的资源调节到可以使用的资源的最大限度。

名称说明
–num-executors配置Executor的数量
–driver-memory配置Driver内存(影响不大)
–executor-memory配置每个Executor的内存大小
–executor-cores配置每个Executor的CPU core数量
1.2.2 RDD优化

缓存

必须对多次使用的RDD进行持久化的操作,通过持久化将公共的RDD的数据缓存到内存和磁盘中:如果当前的内存资源不够可以考虑将RDD的持久化进行序列化处理以便于减少数据的体积;如果当前的对数据的可靠性要求很高,并且内存很充足,可以使用其副本的机制。

filter

在处理过程中,应该尽早的使用filter进行过滤处理,从而减少对内存资源的占用,提升的spark的执行效率(肯定选择的是先过滤再连接)

1.2.3 并行度的调节

理想的并行度的设置,应该尽可能的让并行度和资源相互匹配,简而言之就是在资源允许的范围内将并行度设置的尽可能的大,达到可以充分的利用集群的资源

官方回答,task的数量应该设置为Spark作业的总CPU核数的2~3倍

//spark并行度的设置
val conf = new SparkConf().set("spark.default.parallelism", "500")
1.2.4 广播大变量

一般情况下,task的算子如果使用到了task之外的变量,每个task都会获得一份重复的副本,这就造成了内存极大的消耗

广播变量在Executor保存一个副本,这样使得Executor内的task共用一个副本,大大减少的副本占用的内内存量

广播变量的初始阶段

开始的时候,广播变量只会在driver中有一份副本,当task运行的时候,想要尝试获取广播变量的中的数据,此时会先向本地的Executor对应的BlockManager中获取尝试获取该变量,如果本地没有该变量,就会向driver或者其他节点的BlockManager上远程拉取变量的副本,并由本地的BlockManager进行管理,之后此Executor的所有task都会直接从本地的BlockManager中获取该变量

1.2.5 Kryo序列化

kryo序列化和java序列化都可以,但是kryo序列化比java的性能高,但是它不支持所有对象的序列化

/*
*Kryo序列化机制配置代码
*/

public class MyKryoRegistrator implements KryoRegistrator
{
  @Override
  public void registerClasses(Kryo kryo)
  {
    kryo.register(StartupReportLogs.class);
  }
}
/*
*Kryo序列化机制配置代码
*/

//创建SparkConf对象
val conf = new SparkConf().setMaster().setAppName()
//使用Kryo序列化库,如果要使用Java序列化库,需要把该行屏蔽掉
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");  
//在Kryo序列化库中注册自定义的类集合,如果要使用Java序列化库,需要把该行屏蔽掉
conf.set("spark.kryo.registrator", "MyKryoRegistrator");
1.2.6 调节本地化等待时长

Spark作业运行的过程中,diver会对每个stage的task进行分配(spark是希望他要去往运算的数据所在节点,因为这样就可以避免了网络传输),但是一般来说呢,task可能不会被分配到数据所在节点,因为这些节点此时资源可能已经用尽,但是此时spark会等待3s(可以手动设置)等待机会,如果最后还是不能满足愿望,spark会自动的降级,尝试将task分配到比较差本地化节点上进行计算,如果还不行,会再次降级

名称解析
PROCESS_LOCAL进程本地化,task和数据在同一个Executor中,性能最好。
NODE_LOCAL节点本地化,task和数据在同一个节点中,但是task和数据不在同一个Executor中,数据需要在进程间进行传输。
RACK_LOCAL机架本地化,task和数据在同一个机架的两个节点上,数据需要通过网络在节点之间进行传输。
NO_PREF对于task来说,从哪里获取都一样,没有好坏之分。
ANYtask和数据可以在集群的任何地方,而且不在一个机架中,性能最差。

当我们查看输出日志的时候,如果发现很多的级别都是NODE_LOCAL、ANY,那么需要对本地化的等待时长进行调节,通过延长本地化等待时长,查看task的本地化级别有没有提升,并且观察spark作业的运行时间

//Spark本地化等待时长设置(默认3s)
val conf = new SparkConf().set("spark.locality.wait", "6")

不要将本地化等待时长过分的延长,因为会因为大量的等待时长,反而使得Spark作业的运行时间增加了

1.3 算子调优

1.3.1 mappartitions

对比map

对于普通的map算子是对每一条数据进行处理,而mapPartitions算子对RDD中的一个分区进行操作

缺点

如果使用的是mapPartitions算子,但是数据量非常大的时候,function一次处理一个分区的数据,如果一旦内存不足的话,此时无法回收内存,就会OOM,即内存溢出,因此在项目中需要先预计估算一下RDD的数据量,每个partition的数据量,以及分配给每个Executor的内存资源,资源允许的情况下,当然考虑使用mappartitions算子

1.3.2 foreachpartition优化数据库操作

在实际的生产环境中,通常使用foreachpartition算子来完成数据库的写入操作,一个分区创建唯一的数据库连接,后续的sql操作时,只需要向数据库发送一次sql语句和多组的参数

对比foreach

当建立数据库连接的时候,foreach算子是对RDD的每条数据进行数据库连接,资源浪费极大

1.3.3 filter与coalesce的配合使用

在spark任务中我们一般会选择使用filter算子对RDD中的数据进行过滤,但是一旦过滤后,每个分区的数据量可能会存在较大的差异,此时刚好可以联合使用coalesce将分区进行重新分配,使得每个新分区的数据量差不多且分区数目降低,避免了资源的浪费

分区A–>分区B

假设我们希望将原本的分区个数A通过重新分区变为B,那么有以下几种情况:

1、A > B(多数分区合并为少数分区)

① A与B相差值不大

此时使用coalesce即可,无需shuffle过程。

② A与B相差值很大

此时可以使用coalesce并且不启用shuffle过程,但是会导致合并过程性能低下,所以推荐设置coalesce的第二个参数为true,即启动shuffle过程。

2、A < B(少数分区分解为多数分区)

此时使用repartition即可,如果使用coalesce需要将shuffle设置为true,否则coalesce无效。

1.2.4 repartition解决SparkSQL低并行度问题

当我们手动设置分区的并行度(spark.default.parallelism),不会在sparksql的stage中生效,这个时候我们可以用到repartition算子

具体使用

对于sparksql查询出来的RDD,我们可以立即通过repartition算子进行重新分区,这样就会出现多个partition,从repartition之后的RDD操作,由于不再设计SparkSql,因此stage的并行度就会等于你手动设置的值

1.2.5 优先使用reducebykey本地聚合

简而言之,reducebykey会进行map端的预聚合(combine)

reducebykey原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dheHiW9R-1616993161283)(../../AppData/Roaming/Typora/typora-user-images/image-20210328225417886.png)]

对比groupbykey

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fo0d5Ez6-1616993161285)(../../AppData/Roaming/Typora/typora-user-images/image-20210328225513929.png)]

1.4 shuffle调优

1.4.1 调整map端缓冲区的大小

当map端的task处理数据过多,就会导致溢写到map缓冲区的次数变多,因此调正map端的缓存大小就很重要

//map端缓存设置(默认大小为32KB)
val conf = new SparkConf().set("spark.shuffle.file.buffer", "64")
1.4.2 调节reduce端拉取数据缓冲区大小

shuffle reduce task 的buffer缓冲区大小决定了reduce task 每次都能缓冲的数据量,也就是reduce每次拉取的数据量,在资源允许的范围下,可以尽量增大reduce缓冲区的大小以便于较少其拉取的次数也就是减少了网络IO的次数

//educe端数据拉取缓冲区配置(默认大小为48MB)
val conf = new SparkConf().set("spark.reducer.maxSizeInFlight", "96")
1.4.3 调节reduce端拉取数据重试次数

在实际操作过程中,可能会遇到reduce拉取数据延时导致拉取次数失败进行多次重试,对于那些特别耗时的shuffle操作的作业,建议增加重试最大次数

//reduce端拉取数据重试次数配置(默认是3)
val conf = new SparkConf().set("spark.shuffle.io.maxRetries", "6")
1.4.4 调节reduce端拉取数据等待间隔

spark shuffle过程中,reduce task拉取属于自己的数据时,可能会因为网络的问题导致失败会自动重试,这里我们可以增大我们拉取的一个时间间隔,以增加其稳定性

//reduce端拉取数据等待间隔配置(默认是5s)
val conf = new SparkConf().set("spark.shuffle.io.retryWait", "60s")
1.4.5 调节SortShuffle排序操作阈值

对于SortShuffleManager,如果shuffle reduce task 的数量小于某一固定值的时候不会去排序(默认是200)(spark.shuffle.sort.bypassMergeThreshold),可以适当增大其值减少排序的开销

//reduce端拉取数据等待间隔配置(默认是200)
val conf = new SparkConf().set("spark.shuffle.sort.bypassMergeThreshold", "400")

1.5 JVM调优

1.5.1 降低cache操作的内存占比

当我们使用的还是静态内存分配的时候,可以设置storage内存占比,统一内存则不用,它会采用动态占用的机制

//Storage内存占比设置(默认是0.6)
val conf = new SparkConf().set("spark.storage.memoryFraction", "0.4")
1.5.2 调节Executor堆外内存

如果Spark作业处理的数据量非常大,达到几亿的数据量,此时运行Spark作业会时不时地报错,例如shuffle output file cannot find,executor lost,task lost,out of memory等,这可能是Executor的堆外内存不太够用,导致Executor在运行的过程中内存溢出。

#Executor堆外内存配置(默认上限大概为300多MB)
--conf spark.yarn.executor.memoryOverhead=2048
1.5.3 调节连接等待时长

task需要运行的数据时会优先从本地的BlockManager中获取数据,如果本地没有再去远程连接其他节点上的BlockManager来获取数据,但是难免会遇到网络连接超时的情况,可以手动设置l连接等待时长

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

2 故障处理

2.1 控制reduce端缓冲大小以避免OOM

我们可以设想一下,假设现在task的数据量很大,有多个maptask,下游有多个reduce处理,虽然我们增加了reduce缓冲区(默认48MB)可以使其每次拉取的数量变多,但是恰恰是因为每次拉取的数量变多,导致每个Executor需要处理的数据也就相对应的变多,这样不就会导致OOM的产生嘛,这就是典型的性能换执行,因此我们这个时候就需要手动的去减少reduce缓存区的大小(虽然和之间提到的性能相互矛盾,但是我们目的根源一定是要保证任务能够运行,再考虑性能的优化)

2.2 JVM GC导致的shuffle文件拉取失败

在我们一个executor的task要向另外一个executor拉取数据的时候,这个时候这个目的地的executor正在执行jvm的gc过程(执行此过程期间会使得本executor内的全部任务终止),这就会导致了拉取半天没有数据,最终报错shuffle file not found,因此我们可以手动调整每次拉取的间隔时长以及拉取失败重试的次数

val conf = new SparkConf()
  .set("spark.shuffle.io.maxRetries", "60") //重试次数
  .set("spark.shuffle.io.retryWait", "60s") //间隔时长

2.3 解决各种序列化导致的报错

case 类包含序列化,如果在实际问题中遇到报错信息中含有Serializable等类似词汇,那么可能就是序列化出现问题,1、作为RDD的元素类型的自定义类必须是序列化的;2、算子函数可以使用外部的自定义变量,但必须是序列化的;3、不可以在RDD的元素类型,算子函数里使用第三方不支持序列化的类型,比如:Connection

2.4 解决算子函数返回NULL导致的问题

在一些算子的函数里面有返回值,但我们在处理的时候却不希望有返回值,如果设置成为NULL,又会报错,因而我们会设置返回特殊值比如-1这样,具体过程就是返回-1再通过filter过滤,最后调用coalesce算子进行优化

2.5 解决YARN-CLIENT模式导致的网卡流量激增问题

当我们使用yarn-client模式执行spark任务的时候,我们知道这个时候的driver端是在本地的,虽然说这种运行模式可以在本地直接输出日志,但是会使得driver和其他节点上的task进行网络通信,如果task数量过多、种类过多,会造成大量的网卡流量激增,因此在实际的生产环境中我们都是选择yarn-cluster模式运行任务

2.6 解决YARN-CLUSTER模式的JVM栈内存溢出无法执行问题

yarn-client模式下,dirver端是运行在本地机器上的,spark使用的JVM的PermGen的配置,是本地机器的spark-class文件,jvm的永久代是128MB,但是在在YARN-cluster模式下,Driver运行在YARN集群的某个节点上,使用的是没有经过配置的默认设置,PermGen永久代大小为82MB。这就会使得在client模式可以在cluster模式不行,因而可以手动的设置Driver永久代的大小

#设置Driver永久代的大小(默认是128MB,最大的是256MB)
--conf spark.driver.extraJavaOptions="-XX:PermSize=128M -XX:MaxPermSize=256M"

2.7 解决SparkSQL导致的JVM栈内存溢出

当SparkSQL的sql语句有成百上千的or关键字时,就可能会出现Driver端的JVM栈内存溢出。

JVM栈内存溢出基本上就是由于调用的方法层级过多,产生了大量的,非常深的,超出了JVM栈深度限制的递归

根据实际的生产环境试验,一条sql语句的or关键字控制在100个以内,通常不会导致JVM栈内存溢出。

2.8 持久化与checkpoint的使用

为了避免任务的重复计算操作,我们可以将使用多的RDD进行cache操作,同时为了避免数据的丢失,也可以选择对某个RDD进行checkpoint操作,就是将数据持久化一份到容错文件系统上(HDFS)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

友培

数据皆开源!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值