Spark性能调优

分配更多的资源:

性能优化调优的王道,就是增加和分配更多的资源,这对于性能和速度上的提升是显而易见的。
基本上,在一定范围之内,增加资源与性能的提升,是成正比的;
写完了一个复杂的spark作业之后,进行性能调优的时候,首先第一步,就是要来调节最优的资源配置
在这个基础之上,如果说你的spark作业,能够分配的资源达到了你的能力范围的顶端之后,无法再分配更多的资源了,公司资源有限
那么才是考虑去做后面的这些性能调优的点。 

分配哪些资源:

executor‐memoryexecutor‐coresdriver‐memory

在哪里可以设置这些资源:

实际的生产环境中,提交spark任务时,使用spark‐submit shell脚本,在里面调整对应的参数。

提交任务的脚本:

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

参数调节的大小:

第一种情况:Standalone模式

先计算出公司Spark集群上的所有资源,每台节点的内存大小和CPU核数,
比如:一共有50台Worker节点每台节点8G内存10个CPU
实际任务在给定资源的时候,可以给50个Executor每个Executor的内存8G每个Executor使用10个CPU

第二种情况:Yarn模式

先计算出Yarn集群的所有大小,比如一共500G内存100个CPU
这个时候可以分配的最大资源,给定50个Executor每个Executor的内存10G每个Executor使用2个CPU
使用原则:你能使用的资源有多大,就尽量去调节到最大的大小(Executor的数量:几十个到上百个不等;Executor的内存Exector的CPU个数

Spark的并行度:

spark作业中,各个stage的task的数量,也就代表了spark作业在各个阶段stage的并行度。
分配完所能分配的最大资源了,然后对应资源去调节程序的并行度,如果并行度没有与资源相匹配,那么导致你分配下去的资源都浪费掉了。同时并行运行,还可以让每个task要处理的数量变少(很简单的原理,合理设置并行度,可以充分利用集群资源,减少每个task处理数据量,而增加性能加快运行速度。)

如何提高并行度:

可以设置task的数量,至少设置成与Spark Application的总CPU core数量相同(最理想情况,150个core,分配150task,一起运行,差不多同一时间运行完毕
官方推荐,task数量设置成Spark Application的总CPU core数量的2~3倍

如何设置task数量:

设置参数spark.defalut.parallelism
默认是没有值的,如果设置了值为10,它会在shuffle的过程才会起作用
比如 val rdd2 = rdd1.reduceByKey(_+_)
此时rdd2的分区数就是10,rdd1的分区数不受这个参数的影响。
可以通过在构建SparkConf对象的时候设置,例如:
new SparkConf().set("spark.defalut.parallelism","500") 

给RDD重新设置partition的数量:

使用rdd.repartition重新分区,该方法会生成一个新的rdd使其分区数变大
此时由于一个partition对应一个task,那么对应的task个数越多,通过这种方式也可以提高并行度

提高sparksql运行的task数量:

通过设置参数spark.sql.shuffle.partitions=500(默认为200),可以适当增大,来提高并行度

RDD的重用和持久化:

可以把多次使用到的rdd,也就是公共rdd进行持久化避免后续需要再次重新计算提升效率

如何对RDD进行持久化:

可以调用rdd的cache或者persist方法。

1.cache方法默认是把数据持久化到内存中 。例如:rdd.cache ,其本质还是调用了persist方法

2.persist方法中有丰富的缓存级别,这些缓存级别都定义在StorageLevel这个object中,可以结合实际的应用场景合理的设置缓存级别。例如:rdd.persist(StorageLevel.MEMORY_ONLY),这是cache方法的实现。

RDD持久化的时候可以采用序列化:

1.如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许会导致OOM内存溢出

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

3.序列化的方式,唯一的缺点就是,在获取数据的时候需要反序列化。但是可以减少占用的空间和便于网络传输

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

5.为了数据的高可靠性,而且内存充足,可以使用双副本机制进行持久化。持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次;持久化的每个数据单元存储一份副本放在其他节点上面,从而进行容错一个副本丢了不用重新计算,还可以使用另外一份副本。这种方式,仅仅针对你的内存资源极度充足。 

广播变量引入:

Spark中分布式执行的代码需要传递到各个Executor的task上运行。对于一些只读固定的数据,每次都需要Driver广播到各个Task上,这样效率低下

广播变量允许将变量只广播(提前广播)给各个Executor。该Executor上的各个task再从所在节点的BlockManager(负责管理某个Executor对应的内存和磁盘上的数据)获取变量,而不是从Driver获取变量,从而提升了效率

广播变量,初始的时候,就在Drvier上有一份副本。通过在Driver把共享数据转换成广播变量。 

task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中尝试获取变量副本;如果本地没有,那么就从Driver远程拉取广播变量副本,并保存在本地的BlockManager中

此后这个Executor上的task,都会直接使用本地的BlockManager中的副本。那么这个时候所有该Executor中的task都会使用这个广播变量的副本。也就是说一个Executor只需要在第一个task启动时获得一份广播变量数据之后的task从本节点的BlockManager中获取相关数据

Executor的BlockManager除了从driver上拉取,也可能从其他节点的BlockManager上拉取变量副本网络距离越近越好。 

使用后的性能分析:

不使用广播变量的内存开销为100G,使用后的内存开销5G,这里就相差了20倍左右的网络传输性能损耗和内存开销,使用广播变量后对于性能的提升和影响,还是很可观的。
广播变量的使用不一定会对性能产生决定性的作用。比如运行30分钟的spark作业,可能做了广播变量以后,速度快了2分钟,或者5分钟。但是一点一滴的调优,积少成多,最后还是会有效果的。

如何使用广播变量:

1.通过SparkContext的broadcast方法把数据转换成广播变量类型为Broadcast

2.然后Executor上的BlockManager可以拉取该广播变量的副本获取具体的数据获取广播变量中的值可以通过调用其value方法

Spark序列化:

Spark在进行任务计算的时候,会涉及到数据跨进程的网络传输数据的持久化,这个时候就需要对数据进行序列化

Spark默认采用Java的序列化器。默认java序列化的优缺点如下:

优点处理起来方便,不需要我们手动做其他操作,只是在使用一个对象和变量的时候需要实现Serializble接口

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

Kryo序列化:

Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10
所以Kryo序列化优化以后,可以让网络传输的数据变少,在集群中耗费的内存资源大大减少

Kryo序列化启用后生效的地方:

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

1.算子函数中使用到的外部变量

算子中的外部变量可能与driver需要涉及到网络传输,就需要用到序列化。最终可以优化网络传输的性能优化集群中内存的占用和消耗

2.持久化RDD时进行序列化,StorageLevel.MEMORY_ONLY_SER

将rdd持久化时对应的存储级别里,需要用到序列化。最终可以优化内存的占用和消耗持久化RDD占用的内存越少task执行的时候创建的对象,就不至于频繁

的占满内存频繁发生GC

3. 产生shuffle的地方,也就是宽依赖

下游的stage中的task,拉取上游stage中的task产生的结果数据跨网络传输,需要用到序列化。最终可以优化网络传输的性能

如何开启Kryo序列化:

1.在构建SparkConf的时候设置相关参数

2.注册需要通过Kryo序列化的一些自定义类

fastutil介绍:

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

fastutil能够提供更小的内存占用更快的存取速度,我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set

fastutil的好处:

1.fastutil集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度

2.fastutil也提供了64位的arrayset和list,以及高性能快速的实用的IO类,来处理二进制和文本类型的文件

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

4.fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中

5.fastutil还提供了一些JDK标准类库中没有的额外功能(比如双向迭代器)。

6.fastutil除了对象和原始类型为元素的集合,fastutil也提供引用类型的支持,但是对引用类型是使用等于号(=)进行比较的,而不是equals()方法。

7.fastutil尽量提供了在任何场景下都是速度最快的集合类库

 算子函数使用了外部变量:

1.可以使用Broadcast广播变量优化;

2.可以使用Kryo序列化类库提升序列化性能和效率

3.如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量

首先从源头上就减少内存的占用(fastutil),通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内存占用。

算子函数里使用了比较大的集合:

在你的算子函数里,也就是task要执行的计算逻辑里面,如果有逻辑中,出现要创建比较大的MapList等集合

可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历存取等集合操作

那么此时,可以考虑将这些集合类型使用fastutil类库重写,使用了fastutil集合类以后,就可以在一定程度上,减少task创建出来的集合类型的内存占用

避免Executor内存频繁占满频繁唤起GC导致性能下降。 

fastutil的使用:

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

第二步:平时使用List(Integer)的替换成IntList即可。

基本都是类似于IntList的格式,前缀就是集合的元素类型;特殊的就是MapInt2IntMap,代表了key‐value映射的元素类型。

本地化级别:

1.PROCESS_LOCAL:进程本地化

代码和数据在同一个进程中,也就是在同一个Executor中;计算数据的task由Executor执行数据在Executor的BlockManager中性能最好

2.NODE_LOCAL:节点本地化

代码和数据在同一个节点中;比如说数据作为一个HDFS block块,就在节点上,而task在节点上某个Executor中运行;或者是数据和task在一个节点上的不同Executor中;数据需要在进程间进行传输性能其次

3.RACK_LOCAL:机架本地化

数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输性能比较差

4.ANY:无限制

数据和task可能在集群中的任何地方,而且不在一个机架中性能最差

数据本地化等待时长:默认3s。

JVM调优的原因:Java虚拟机内存不足。

以上是关于spark性能调优的内容,若有不对或不全面的地方,欢迎指正和补充。

posted @ 2020-03-08 15:18  薄荷柠檬茶  阅读(481)  评论(0)    收藏  举报