Spark性能优化

1、分配更多的资源

1.1 增加executor

1.2 增加每个executor的cpu core

增加executor的并行能力,一个cpu core运行一个task

1.3 增加每个executor的内存

1)如果需要对RDD进行cache,那么更多的内存就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘,减少了磁盘IO。

2)对于shuffle操作,reduce端会需要内存来存放拉取的数据并进行聚合。如果内存不够,也会写入磁盘。如果给executor分配置更多内存以后,就有更少的数据需要写入磁盘,甚至不需要写入磁盘,减少了磁盘IO,提升了性能。

3)对于task的执行,可能会创建很多对象。如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC,垃圾回收,MINOR  T full GC。内存加大以后,带来更少的GC,垃圾回收,避免了速度变慢,速度变快了。

2、调节spark的并行能力

2.1 Task数量,至少设置成与Spark Application的总cpu core数量相同(最理想情况,比如总共150个cpu core,分配了150个task,一起运行,差不多同一时间运行完毕)

2.2 官方推荐task数量设置成Spark Application总cpu core数量的2 ~ 3 倍,比如150个cpu core,基本要设置task数量为300 ~ 500;

2.3 设置方法:

spark.default.parallelism

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

3、RDD重构

3.1 RDD架构重构与优化

尽量去复用RDD,差不多的RDD可以抽取成为一个共同的RDD,供后面的RDD计算时反复使用。

3.2 公共RDD一定要实现持久化

持久化要根据不同场景选择 cache 或 persist 方法

3.3 持久化,是可以进行序列化的

主要是指persist方法,选择不同的持久化方式

1) 纯内存MEMORY_ONLY:如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,导致OOM内存举出;

2) MEMORHY_ONLY_SER序列化内存存储:当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑,使用序列化的方式在纯内存中存储。将RDD的每个partition的数据,序列化成一个大的字节数组,就一个对象;序列化后,大大减少内存的空间占用。

序列化的方式缺点就是,在获取数据的时候,需要反序列化。

3)内存 + 磁盘MEMORY_AND_DISK 或 MEMORY_AND_DISK_SER

如果序列化纯内存方式还是导致OOM内存溢出,就只能考虑磁盘的方式,内存 + 磁盘的普通方式(无序列化方式)

MEMORY_AND_DISK:使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用

MEMORY_AND_DISK_SER:基本含义同MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。

4)DISK_ONLY纯磁盘

使用未序列化的Java对象格式,将数据全部写入磁盘文件中

5)MEMORY_ONLY_2MEMORY_AND_DISK_2 等等双副本机制

为了数据的高可靠性,而且内存充足,可以使用双副本机制进行持久化

对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其它节点上。这种基于副本的持久化机制主要用于进行容错。假如某个节点挂掉,节点的内存或磁盘中的持久化数据丢失了,那么后续对RDD计算时还可以使用该数据在其它节点上的副本。如果没有副本的话,就只能将这些数据从源头处重新计算一遍。

4、使用广播变量

广播变量,初始的时候就在Driver上有一份副本。

task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,尝试获取变量副本;如果本地没有,那么就从Driver远程拉取变量副本,并保存在本地的BlockManager中;此后这个executor上的task,都会直接使用本地的BlockManager中的副本。executor的Blockmanager除了从driver上拉取,也可能从其它节点的BlockManager上拉取变量副本,距离越近越好。

5、优化序列化方法

默认情况下,Spark内部是使用Java的序列化机制,ObjectOutputStream/ObjectInputStream,对象输入输出流机制来进行序列化。这种默认的序列化好处在于处理起来比较方便,也不需要手动去增加多少代码 ,只要求在算子里面使用的变量,必须是实现Serializable接口的,可序列即可。

缺点在于,默认的序列化机制的效率不高,序列化的速度比较慢;序列化以后的数据,占用的内存空间相对比较大。

解决方法:

可手动进行序列化Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制速度快,序列化后的数据更小,大概是Java序列化机制的1/10。

Kryo序列化机制,一旦启用以后,会生效的几个地方:

1)算子函数中使用到的外部变量,使用Kryo以后:优化网络传输的性能,可以优化集群中内存的占用和消耗;

2)持久化RDD,优化内存的占用和消耗;持久化RDD占用的内存越少,task执行的时候,创建的对象不容易导致频繁地占满内存,频繁发生GC;

3)shuffle:可以优化网络传输的性能

6、使用fastutil扩展的集合框架

6.1 fastutil介绍

fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊的map、set、list和queue;

fastutil能够提供更小的内存占用,更快的存取速度;使用fastutil提供的集合类替代JDK原生的Map、List、Set,可以减少内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值时,提供更快的存取速度;

fastutil也提供了64位的array、set和list,以及高性能快速、实用的IO类,以处理二进制和文本类型的文件;

fastutil最新版本要求Java 7以及以上版本。

6.2 Spark中应用fastutil的场景

1)如果算子函数使用了外部变量:第一,可以使用Broadcast广播变量优化;第二,可以使用Kryo序列化类库,提升序列化性能和效率;第三,如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量,首先从源头上就减少内存的占用,通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内存占用;

2)在算子函数时,也就是task要执行的计算逻辑里面,如果出现要创建比较大的Map、List等集合,可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历、存取等集合操作,那么 可以考虑将这些集合类型使用fastutil类库重写,使用fastutil集合类以后,就可以在一定程序上减少task创建出来的集合类型的内存占用,避免executor内存频繁占满,频繁唤起GC,导致性能下降。

6.3 fastutil的使用

第一步:在pom.xml中引用fastutil的包

<dependency>
    <groupId>fastutil</groupId>
    <artifactId>fastutil</artifactId>
    <version>5.0.9</version>
</dependency>

速度比较慢,可能是从国外的网去拉取jar包

List<Integer> => IntList

基本都是类似于IntList的格式,前缀就是集合的元素类型;特殊的就是Map,Int2IntMap,代表了key-value映射的元素类型。除此之外,还支持object、reference。

7、进程本地化,代码和数据在同一个进程中

Spark的Driver上,对Application的每一个stage的task进行分配前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;Spark的task分配算法,优化会希望每个task正好分配到它要计算的数据所在的节点,这样便不需要网络间传输数据;但通常来说是难以实现的,有时更会出现task没有机会分配到它的数据所在的节点,可能该节点的计算资源和计算能力都满了。这种情况下,Spark会等待一段时间,默认情况下为3秒(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后等待不了了,就会选择一个比较差的本地化级别,比如说,将task分配到靠它要计算的数据所在节点的相对较近一个节点,然后进行计算。

因此可以适当调节等待时间。

7.1 如何判断要调节

观察Spark作业的运行日志,在进行测试时,可先用client模式,在本地就可以直接看到比较全的日志。日志里面会显示starting task...,PROCESS LOCAL、NODE LOCAL。观察大部分task的数据本地化级别,如果大多都是PROCESS LOCAL,那就不用调节了;如果发现好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长,反复进行调节,观察日志,查看大部分的task的本地化级别有没有提升,整个spark作业的运行时间有没有缩短。

7.2 怎么调节

spark.locality.wait,默认是3s;6s,10s

默认情况下,下面3个的等待时长跟spark.locality.wait一样:

spark.locality.wait.process

sprk.locality.wait.node

spark.locality.wait.rack

new SparkConf().set("spark.locality.wait","10")

8、降低cache操作的内存占比

Spark中,堆内存又被划分成为了两块,一块是专门用来给RDD的cache、persist操作进行RDD数据缓存用;另外一块就是给Spark算子函数的运行使用,存放函数中自己创建的对象。默认情况下,给RDD cache操作的内存占比为0.6。但问题是,如果某些情况下,cache不是那么紧张,问题在于task算子函数中创建的对象过多,然后内存又不太大,导致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作,性能影响会很大。

在yarn运行的话,可通过yarn界面查看spark作业的运行统计。一层一层点击进去,可看到每个stage的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长。此时可以适当调节这个比例。

降低cache操作的内存占比,可用persist选择将一部分缓存的RDD数据写入磁盘,或者序列化方式,配合Kryo序列化类,减少RDD缓存的内存占用;降低cache操作内存占比;对应的,算子函数的内存占比就提升。此时可减少minor gc频率,同时降低full gc的频率,对性能的提升有一定的帮助。

调节方法:

spark.storage.memoryFraction

9、修改executor堆外内存

9.1 作业运行情况

有时Spark作业处理的数据量特别大,几亿数据量;在Spark作业运行时偶尔有报错,比如shuffle file cannot fid,executor、task lost,out of memory(内存溢出);

可能是executor的堆外内存不够用,导致executor在运行的过程中出现内存溢出,而后续stage的task在运行的时候,可能要从要从一些executor中去拉取shuffle map output文件,但executor可能已经挂掉了,关联的block monager也没有了;所以可能会报shuffle output file not found;resubmitting task;executor lost;spark作业彻底崩溃。

上述情况下,可以考虑调节executor的堆外内存,兴许就可以避免报错;此外,有时堆外内存调节较大时,对性能也有一定的提升。

9.2 如何调节

--conf spark.yarn.executor.memoryOverhead = 2048

切记:spark-submit脚本里面,用--conf的方式添加配置;在spark作业代码中使用new SparkConf().set()这种方式是没有用的

默认情况下,这个堆外内存上限大概是300多M;而通常在项目中真正处理大数据的时候,这里都会出现问题导致Spark作业反复崩溃无法运行;此时可调节这个参数到至少1G(1024M),甚至是2G、4G。通常这个参数调节上去以后,就会避免掉某些JVM OOM的异常问题,同时也会让整体Spark作业的性能得到较大的提升。

9.3 运程拉取数据建立网络异常

当上面的block manager本地没有数据就会尝试建立远程的网络连接,并且去拉取数据,此时如果对应的block manager由于oom崩溃,就会连接没有响应,无法建立网络连接;spark默认的网络连接的超时时长为60s,60s没有连接则任务失败。

比如一些情况下会报某某file、一串file id、uuid... not found、file lost。这种情况下,很有可能是有那份数据的executor在jvm gc,所以拉取数据的时候建立不了连接。若超过默认60s,任务失败。

报错几次,几次都拉取不到数据时可能会导致Spark作业崩溃;也可能会导致DAGScheduler,反复提交几次stage;TaskScheduler反复提交几次task,大大延长Spark作业的运行时间。

此时可以考虑调节连接的超时时长:

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

切记:spark-submit脚本里面,用--conf的方式添加配置;在spark作业代码中使用new SparkConf().set()这种方式是无效的

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值