spark调优

性能调优

1.参数调优

1.1 优化资源配置

Spark性能调优的第一步,就是为任务分配更多的资源,在一定范围内,增加资源的分配与性能的提升是成正比的
调节原则:尽量将任务分配的资源调节到可以使用的资源的最大限度

由于Yarn使用资源队列进行资源的分配和调度,在表写submit脚本的时候,就根据Spark作业要提交到的资源队列,进行资源的分配,比如资源队列有400G内存,100个CPU core,那么指定50个Executor,每个Executor分配8G内存,2个CPU core

/usr/local/spark/bin/spark-submit \
--class com.atguigu.spark.WordCount \
--num-executors 80 \  一般为50~100
--driver-memory 6g \  一般为1G~5G
--executor-memory 6g \  一般为6G~10G
--executor-cores 3 \   一般为3
--master yarn-cluster \   一般为实际生产环境一定使用yarn-cluster
--queue root.default \
--conf spark.yarn.executor.memoryOverhead=2048 \
--conf spark.core.connection.ack.wait.timeout=300 \
/usr/local/spark/spark.jar
  • 增加driver内存
    影响不大
  • 增加Executor·个数
    在资源允许的情况下,增加Executor的个数可以提高执行task的并行度。比如有4个Executor,每个Executor有2个CPU core,那么可以并行执行8个task,如果将Executor的个数增加到8个(资源允许的情况下),那么可以并行执行16个task,此时的并行能力提升了一倍。
  • 增加每个Executor的CPU core个数
    在资源允许的情况下,增加每个Executor的Cpu core个数,可以提高执行task的并行度。比如有4个Executor,每个Executor有2个CPU core,那么可以并行执行8个task,如果将每个Executor的CPU core个数增加到4个(资源允许的情况下),那么可以并行执行16个task,此时的并行能力提升了一倍。
  • 增加每个Executor的内存量
    在资源允许的情况下,增加每个Executor的内存量以后,对性能的提升有三点:
    • 可以缓存更多的数据(即对RDD进行cache),写入磁盘的数据相应减少,甚至可以不写入磁盘,减少了可能的磁盘IO;
    • 可以为shuffle操作提供更多内存,即有更多空间来存放reduce端拉取的数据,写入磁盘的数据相应减少,甚至可以不写入磁盘,减少了可能的磁盘IO;
    • 可以为task的执行提供更多内存,在task的执行过程中可能创建很多对象,内存较小时会引发频繁的GC,增加内存后,可以避免频繁的GC,提升整体性能。

1.2 并行度调节

并行度指各个stage的task的数量
如果并行度设置不合理而导致并行度过低,会导致资源的极大浪费
在资源允许的前提下,并行度要设置的尽可能大,达到可以充分利用集群资源
官网建议task数量应该设置为Spark作业总CPU core数量的2~3倍

val conf = new SparkConf()
  .set("spark.default.parallelism", "500")

1.3 调节本地化等待时长

根据Spark的task分配算法,Spark希望task能够运行在它要计算的数据算在的节点(就近原则),可以避免数据的网络传输
如果超过等待时长就会降级

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

是实际上task可能不会被分配到它处理的数据所在的节点,因为这些节点可用的资源可能已经用尽,此时,Spark会等待一段时间,默认3s,,如果等待指定时间后仍然无法在指定节点运行,那么会自动降级,尝试将task分配到比较差的本地化级别所对应的节点上,比如将task分配到离它要计算的数据比较近的一个节点,然后进行计算,如果当前级别仍然不行,那么继续降级。

当task要处理的数据不在task所在节点上时,会发生数据的传输。task会通过所在节点的BlockManager获取数据,BlockManager发现数据不在本地时,户通过网络传输组件从数据所在节点的BlockManager处获取数据。

在Spark项目开发阶段,可以使用client模式对程序进行测试,此时,可以在本地看到比较全的日志信息,日志信息中有明确的task数据本地化的级别,如果大部分都是PROCESS_LOCAL,那么就无需进行调节,但是如果发现很多的级别都是NODE_LOCAL、ANY,那么需要对本地化的等待时长进行调节,通过延长本地化等待时长,看看task的本地化级别有没有提升,并观察Spark作业的运行时间有没有缩短。

注意,过犹不及,不要将本地化等待时长延长地过长,导致因为大量的等待时长,使得Spark作业的运行时间反而增加了

1.4 shuffle参数调优

1.4.1 调节map端缓冲区大小
val conf = new SparkConf()
  .set("spark.shuffle.file.buffer", "64")
  • map端缓冲的默认配置是32KB
  • 如果shuffle的map端处理的数据量比较大,但是map端缓冲的大小是固定的,可能会出现map端缓冲数据频繁spill溢写到磁盘文件中的情况,使得性能非常低下
  • 通过调节map端缓冲的大小,可以避免频繁的磁盘IO操作,进而提升Spark任务的整体性能
1.4.2 调节reduce端拉取数据缓冲区大小
val conf = new SparkConf()
  .set("spark.reducer.maxSizeInFlight", "96")
  • 默认为48MB
  • shuffle reduce task的buffer缓冲区大小决定了reduce task每次能够缓冲的数据量,也就是每次能够拉取的数据量
  • 如果内存资源较为充足,适当增加拉取数据缓冲区的大小,可以减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能
1.4.3 调节reduce端拉取数据重试次数
val conf = new SparkConf()
  .set("spark.shuffle.io.maxRetries", "6") 默认是3

Shuffle过程中,reduce task拉取属于自己的数据时,如果因为网络异常等原因导致失败会自动进行重试。对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如6次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败

1.4.4 调节reduce端拉取数据等待间隔
val conf = new SparkConf()
  .set("spark.shuffle.io.retryWait", "20s")  默认值为5s

reduce task拉取属于自己的数据时,如果因为网络异常等原因导致失败会自动进行重试,在一次失败后,会等待一定的时间间隔再进行重试,可以通过加大间隔时长(比如20s),以增加shuffle操作的稳定性

1.4.5调节SortShuffle排序操作阈值

默认是200,shuffle reduce task的数量小于这个值会使用bypassSortShuffleWriter,他不用在map端排序,性能更好
我们可以适当调大这个值

val conf = new SparkConf()
  .set("spark.shuffle.sort.bypassMergeThreshold", "400")

1.5 内存调优

Spark内存管理

1.5.1 降低cache操作的内存占比

saprk1.6以后使用统一内存管理
由于动态占用机制的实现,shuffle过程需要的内存过大时,会自动占用Storage的内存区域,因此无需手动进行调节,但是也是可以调节占比的

1.5.2 调节Executor堆外内存

Executor的堆外内存主要用于程序的共享库、Perm Space、 线程Stack和一些Memory mapping等, 或者类C方式allocate object

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

stage的task在运行的时候,可能要从一些Executor中去拉取shuffle map output文件,但是Executor可能已经由于内存溢出挂掉了,其关联的BlockManager也没有了,这就可能会报出shuffle output file cannot find,executor lost,task lost,out of memory等错误,此时,就可以考虑调节一下Executor的堆外内存,也就可以避免报错,与此同时,堆外内存调节的比较大的时候,对于性能来讲,也会带来一定的提升

默认情况下,Executor堆外内存上限大概为300多MB,在实际的生产环境下,对海量数据进行处理的时候,这里都会出现问题,导致Spark作业反复崩溃,无法运行,此时就会去调节这个参数,到至少1G,甚至于2G、4G

--conf spark.yarn.executor.memoryOverhead=2048
1.5.3 调节连接等待时长

在Spark作业运行过程中,Executor优先从自己本地关联的BlockManager中获取某份数据,如果本地BlockManager没有的话,会通过TransferService远程连接其他节点上Executor的BlockManager来获取数据。

如果task在运行过程中创建大量对象或者创建的对象较大,会占用大量的内存,这回导致频繁的垃圾回收,但是垃圾回收会导致工作现场全部停止,也就是说,垃圾回收一旦执行,Spark的Executor进程就会停止工作,无法提供相应,此时,由于没有响应,无法建立网络连接,会导致网络连接超时。

在生产环境下,有时会遇到file not found、file lost这类错误,在这种情况下,很有可能是Executor的BlockManager在拉取数据的时候,无法建立连接,然后超过默认的连接等待时长60s后,宣告数据拉取失败,如果反复尝试都拉取不到数据,可能会导致Spark作业的崩溃。这种情况也可能会导致DAGScheduler反复提交几次stage,TaskScheduler返回提交几次task,大大延长了我们的Spark作业的运行时间。

--conf spark.core.connection.ack.wait.timeout=120

调节连接等待时长后,通常可以避免部分的XX文件拉取失败、XX文件lost等报错

2.代码调优

2.1 RDD复用并持久化

要避免相同的算子和计算逻辑之下对RDD进行重复的计算
在这里插入图片描述
在这里插入图片描述
持久化:
当多次对同一个RDD执行算子操作时,每一次都会对这个RDD以之前的父RDD重新计算一次,导致资源的极大浪费,通过持久化将公共RDD的数据缓存到内存/磁盘中,之后对于公共RDD的计算都会从内存/磁盘中直接获取RDD数据
比如累加器遇到的问题其实就是rdd执行了两次

  • RDD的持久化是可以进行序列化的,当内存无法将RDD的数据完整的进行存放的时候,可以考虑使用序列化的方式减小数据体积,将数据完整存储在内存中。
  • 如果对于数据的可靠性要求很高,并且内存充足,可以使用副本机制,可以看一下rdd的持久化机制

2.2 优先filter原则,filter后压缩

获取到初始RDD后,应该考虑尽早地过滤掉不需要的数据,一反面避免对无用数据的操作,而且可以减少对内存的占用
filter后执行coalesce均衡分区的数据量和压缩分区
但是我们filter过滤之后会

  • 每个partition的数据量变小了,如果还按照之前与partition相等的task个数去处理当前数据,有点浪费task的计算资源
  • 每个partition的数据量不一样,有的很少,有的没怎么过滤就还有很多,可能会数据倾斜

我们可以在filter操作之后,使用coalesce算子针对每个partition的数据量各不相同的情况,压缩partition的数量,而且让每个partition的数据量尽量均匀紧凑,以便于后面的task进行计算操作,在某种程度上能够在一定程度上提升性能

repartition与coalesce都可以用来进行重分区,其中repartition只是coalesce接口中shuffle为true的简易实现,coalesce默认情况下不进行shuffle,但是可以通过参数进行设置。

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

  1. A > B(多数分区合并为少数分区)
    ① A与B相差值不大 此时使用coalesce即可,无需shuffle过程。
    ② A与B相差值很大
    此时可以使用coalesce并且不启用shuffle过程,但是会导致合并过程性能低下,所以推荐设置coalesce的第二个参数为true,即启动shuffle过程。
  2. A < B(少数分区分解为多数分区) 此时使用repartition即可,如果使用coalesce需要将shuffle设置为true,否则coalesce无效。

2.3 parttition算子

比如map算子,他是对每条数据执行一次,假设一个partition有1万条数据,那么map算子中的function要执行1万次
mapPartition算子,由于一个task处理一个RDD的partition,那么一个task只会执行一次function,function一次接收所有的partition数据,效率比较高
但是如果使用mapPartitions算子,但数据量非常大时,function一次处理一个分区的数据,如果一旦内存不足,此时无法回收内存,就可能会OOM,即内存溢出
foreachPartition优化数据库操作
我么一般写入数据库用foreachPartition算子
foreach算子完成数据库的操作,由于foreach算子是遍历RDD的每条数据,因此,每条数据都会建立一个数据库连接
foreachPartition一个分区的数据只需要创建一次数据库连接即可,只需要向数据库发送一次SQL语句和多组参数

2.4 repartition解决SparkSQL低并行度

用户设置的并行度只对于Spark SQL以外的所有Spark的stage生效

Spark SQL的并行度不允许用户自己指定,Spark SQL自己会默认根据hive表对应的HDFS文件的split个数自动设置Spark SQL所在的那个stage的并行度,用户自己通spark.default.parallelism参数指定的并行度,只会在没Spark SQL的stage中生效

这就可能spaeksql默认的task较少,而数据量比较大,每个task要执行较大的数据量

这就可能表现为第一个有Spark SQL的stage速度很慢,而后续的没有Spark SQL的stage运行速度非常快

对于Spark SQL查询出来的RDD,立即使用repartition算子,去重新进行分区,这样可以重新分区为多个partition,从repartition之后的RDD操作,由于不再设计Spark SQL,因此stage的并行度就会等于你手动设置的值
在这里插入图片描述

2.5 算子本身,比如reduceByKey的本地聚合

  1. 本地聚合后,在map端的数据量变少,减少了磁盘IO,也减少了对磁盘空间的占用;
  2. 本地聚合后,下一个stage拉取的数据量变少,减少了网络传输的数据量;
  3. 本地聚合后,在reduce端进行数据缓存的内存占用减少;
  4. 本地聚合后,在reduce端进行聚合的数据量减少。

3.内存调优

3.1 Kryo序列化

Java的序列化机制使用方便,不需要额外的配置,在算子中使用的变量实现Serializable接口即可,但是,Java序列化机制的效率不高,序列化速度慢并且序列化后的数据所占用的空间依然较大

Kryo序列化机制比Java序列化机制性能提高10倍左右,Spark之所以没有默认使用Kryo作为序列化类库,是因为它不支持所有对象的序列化,同时Kryo需要用户在使用前注册需要序列化的类型,不够方便,但从Spark 2.0.0版本开始,简单类型、简单类型数组、字符串类型的Shuffling RDDs 已经默认使用Kryo序列化方式了

public class MyKryoRegistrator implements KryoRegistrator
{
  @Override
  public void registerClasses(Kryo kryo)
  {
    kryo.register(StartupReportLogs.class);
  }
}
//创建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", "hht.demo.MyKryoRegistrator"); 

3.2 MEMORY_AND_DISK

内存不够,我们可以用内存+磁盘的方式,那你知道内存不足存储时rdd怎么处理的吗
首先是动态内存,但是执行内存也不一定刚好就用不完
那么就会从内存移除最老的rdd,不cache或者放到磁盘

3.3 降低cache操作的内存占比

现在都是动态内存,spark自己就处理了

3.4 增加每个executor的内存量

加资源是最简单暴力的方法,集群资源富裕是可以这样操作的

3.5 shuffle类

  1. 数据倾斜,可能会导致每个task数据量太大,内存溢出
  2. shuffle 时也会有map和reduce端内存问题,可以避免就尽量避免
  3. 不能避免时,在数据量大时可以加大分区量,避免一个task数据过多

3.6 算子导致的内存溢出

例如下列map算子,每条记录都产生了1000个对象,这肯定很容易产生内存溢出的问题

rdd.map(x=>for(i <- 1 to 10000) yield i.toString)

解决,分到更多的分区,减少每个task的压力

rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)

4 shuffle调优

4.1 广播变量

task中的算子中如果使用了外部的变量,每个task都会获取一份变量的复本,这就造成了内存的极大消耗
广播变量在每个Executor保存一个副本,此Executor的所有task共用此广播变量,这让变量产生的副本数量大大减少
如果本地没有,BlockManager就会从Driver或者其他节点的BlockManager上远程拉取变量的复本
共享变量之广播变量

4.2 解决数据倾斜的问题

数据倾斜解决办法

其实核心就是4个方向,有时需要结合使用

  1. 抽出倾斜key,过滤掉或者单独处理后拼接
  2. key打散,把一个key的数据一定会分到一个task,打散来分到不同的task缓解压力
  3. 广播变量避免shuffle
  4. 资源方向,加分区数,让每个分区数据更少
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

orange大数据技术探索者

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值