Spark调优之性能调优

1.1分配更多资源

1.1.1 分配哪些资源

Executor的数量
每个Executor所能分配的CPU数量
每个Executor所能分配的内存量
Driver端分配的内存数量

1.1.2 在哪里分配这些资源

在生产环境中,提交spark作业时,用的spark-submit shell脚本,里面调整对应的参数:
/usr/local/spark/bin/spark-submit
–class cn.spark.sparktest.core.WordCountCluster
–num-executors 3 \ 配置executor的数量
–driver-memory 100m \ 配置driver的内存(影响不大)
–executor-memory 100m \ 配置每个executor的内存大小
–total-executor-cores 3 \ 配置所有executor的cpu core数量
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar \

1.1.3 调节到多大,算是最大呢

常用的资源调度模式有Spark Standalone和Spark On Yarn。比如说你的每台机器能够给你使用60G内存,10个cpu core,20台机器。那么executor的数量是20。平均每个executor所能分配60G内存和10个cpu core。

1.1.4 为什么多分配了这些资源以后,性能会得到提升?

一、增加executor:
如果executor数量比较少,那么,能够并行执行的task数量就比较少,就意味着,我们的Application的并行执行的能力就很弱。
比如有3个executor,每个executor有2个cpu core,那么同时能够并行执行的task,就是6个。6个执行完以后,再换下一批6个task。
增加了executor数量以后,那么,就意味着,能够并行执行的task数量,也就变多了。比如原先是6个,现在可能可以并行执行10个,甚至20个,100个。那么并行能力就比之前提升了数倍,数十倍。相应的,性能(执行的速度),也能提升数倍~数十倍。

二、增加每个executor的cpu core,也是增加了执行的并行能力。原本20个executor,每个才2个cpu core。能够并行执行的task数量,就是40个task。
现在每个executor的cpu core,增加到了4个。能够并行执行的task数量,就是80个task。就物理性能来看,执行的速度,提升了2倍。

三、增加每个executor的内存量。增加了内存量以后,对性能的提升,有三点:
1、如果需要对RDD进行cache,那么更多的内存,就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘。减少了磁盘IO。
2、对于shuffle操作,reduce端,会需要内存来存放拉取的数据并进行聚合。如果内存不够,也会写入磁盘。如果给executor分配更多内存以后,就有更少的数据,需要写入磁盘,甚至不需要写入磁盘。减少了磁盘IO,提升了性能。
3、对于task的执行,可能会创建很多对象。如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC,垃圾回收,minor GC和full GC。(速度很慢)。内存加大以后,带来更少的GC,垃圾回收,避免了速度变慢,速度变快了。

1.2、调节并行度

1.2.1、并行度的概念

就是指的是Spark作业中,各个stage的task数量,代表了Spark作业的在各个阶段(stage)的并行度。

1.2.2、如果不调节并行度,导致并行度过低,会怎么样?

比如现在spark-submit脚本里面,给我们的spark作业分配了足够多的资源,比如50个executor,每个executor有10G内存,每个executor有3个cpu core。基本已经达到了集群或者yarn队列的资源上限。task没有设置,或者设置的很少,比如就设置了100个task,你的Application任何一个stage运行的时候,都有总数在150个cpu core,可以并行运行。但是你现在,只有100个task,平均分配一下,每个executor分配到2个task,ok,那么同时在运行的task,只有100个,每个executor只会并行运行2个task。每个executor剩下的一个cpu core, 就浪费掉了。
你的资源虽然分配足够了,但是问题是,并行度没有与资源相匹配,导致你分配下去的资源都浪费掉了。
合理的并行度的设置,应该是要设置的足够大,大到可以完全合理的利用你的集群资源。比如上面的例子,总共集群有150个cpu core,可以并行运行150个task。那么就应该将你的Application的并行度,至少设置成150,才能完全有效的利用你的集群资源,让150个task,并行执行。而且task增加到150个以后,即可以同时并行运行,还可以让每个task要处理的数据量变少。比如总共150G的数据要处理,如果是100个task,每个task计算1.5G的数据,现在增加到150个task,可以并行运行,而且每个task主要处理1G的数据就可以。
很简单的道理,只要合理设置并行度,就可以完全充分利用你的集群计算资源,并且减少每个task要处理的数据量,最终,就是提升你的整个Spark作业的性能和运行速度。

1.2.3、设置并行度

1)、task数量,至少设置成与Spark application的总cpu core数量相同(最理想情况,比如总共150个cpu core,分配了150个task,一起运行,差不多同一时间运行完毕)。
2)、官方是推荐,task数量,设置成spark application总cpu core数量的2~3倍,比如150个cpu core,基本要设置task数量为300~500。
实际情况,与理想情况不同的,有些task会运行的快一点,比如50s就完了,有些task,可能会慢一点,要1分半才运行完,所以如果你的task数量,刚好设置的跟cpu core数量相同,可能还是会导致资源的浪费。比如150个task,10个先运行完了,剩余140个还在运行,但是这个时候,有10个cpu core就空闲出来了,就导致了浪费。那如果task数量设置成cpu core总数的2~3倍,那么一个task运行完了以后,另一个task马上可以补上来,就尽量让cpu core不要空闲,同时也是尽量提升spark作业运行的效率和速度,提升性能。
3)、如何设置一个Spark Application的并行度?
spark.default.parallelism
SparkConf conf = new SparkConf()
.set(“spark.default.parallelism”, “500”)

1.3、重构RDD架构以及RDD持久化

1.3.1、RDD架构重构与优化

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

1.3.2、公共RDD一定要实现持久化

对于要多次计算和使用的公共RDD,一定要进行持久化。
持久化,就是将RDD的数据缓存到内存中/磁盘中(BlockManager)以后无论对这个RDD做多少次计算,那么都是直接取这个RDD的持久化的数据,比如从内存中或者磁盘中,直接提取一份数据。

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

如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许,会导致OOM内存溢出。
当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑使用序列化的方式在纯内存中存储。将RDD的每个partition的数据,序列化成一个大的字节数组,就一个对象。序列化后,大大减少内存的空间占用。
序列化的方式,唯一的缺点就是,在获取数据的时候,需要反序列化。
如果序列化纯内存方式,还是导致OOM内存溢出,就只能考虑磁盘的方式、内存+磁盘的通方式(无序列化)、内存+磁盘(序列化)。

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

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

1.4 广播变量

1.4.1 概念及需求

Spark Application(我们自己写的Spark作业)最开始在Driver端,在我们提交任务的时候,需要传递到各个Executor的Task上运行。对于一些只读、固定的数据(比如从DB中读出的数据),每次都需要Driver广播到各个Task上,这样效率低下。广播变量允许将变量只广播(提前广播)给各个Executor。该Executor上的各个Task再从所在节点的BlockManager获取变量,如果本地没有,那么就从Driver远程拉取变量副本,并保存在本地的BlockManager中。此后这个executor上的task,都会直接使用本地的BlockManager中的副本。而不是从Driver获取变量,从而提升了效率。
一个Executor只需要在第一个Task启动时,获得一份Broadcast数据,之后的Task都从本节点的BlockManager中获取相关数据。

1.4.2 使用方法

1)调用SparkContext.broadcast方法创建一个Broadcast[T]对象。任何序列化的类型都可以这么实现。
2)通过value方法访问该对象的值。
3)变量只会被发送到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)

1.5 使用Kryo序列化

1.5.1、概念及需求

默认情况下,Spark内部是使用Java的序列化机制,ObjectOutputStream / ObjectInputStream,对象输入输出流机制,来进行序列化。
这种默认序列化机制的好处在于,处理起来比较方便,也不需要我们手动去做什么事情,只是,你在算子里面使用的变量,必须是实现Serializable接口的,可序列化即可。
但是缺点在于,默认的序列化机制的效率不高,序列化的速度比较慢,序列化以后的数据,占用的内存空间相对还是比较大。
Spark支持使用Kryo序列化机制。这种序列化机制,比默认的Java序列化机制速度要快,序列化后的数据更小,大概是Java序列化机制的1/10。
所以Kryo序列化优化以后,可以让网络传输的数据变少,在集群中耗费的内存资源大大减少。

1.5.2、Kryo序列化机制启用以后生效的几个地方

1)、算子函数中使用到的外部变量,使用Kryo以后:优化网络传输的性能,可以优化集群中内存的占用和消耗。
2)、持久化RDD,优化内存的占用和消耗。持久化RDD占用的内存越少,task执行的时候,创建的对象,就不至于频繁的占满内存,频繁发生GC。
3)、shuffle:可以优化网络传输的性能。

1.5.3、使用方法

第一步,在SparkConf中设置一个属性,spark.serializer,org.apache.spark.serializer.KryoSerializer类。
第二步,注册你使用的需要通过Kryo序列化的一些自定义类,SparkConf.registerKryoClasses()。
项目中的使用:
.set(“spark.serializer”, “org.apache.spark.serializer.KryoSerializer”)
.registerKryoClasses(new Class[]{CategorySortKey.class})

1.6、使用fastutil优化数据格式

1.6.1、fastutil介绍

fastutil是扩展了Java标准集合框架(Map、List、Set。HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue。
fastutil能够提供更小的内存占用,更快的存取速度。我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,好处在于fastutil集合类可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度。
fastutil也提供了64位的array、set和list,以及高性能快速的,以及实用的IO类,来处理二进制和文本类型的文件。
fastutil最新版本要求Java 7以及以上版本。
fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中。
fastutil还提供了一些JDK标准类库中没有的额外功能(比如双向迭代器)。
fastutil除了对象和原始类型为元素的集合,fastutil也提供引用类型的支持,但是对引用类型是使用等于号(=)进行比较的,而不是equals()方法。
fastutil尽量提供了在任何场景下都是速度最快的集合类库。

1.6.2、Spark中应用fastutil的场景

1)、如果算子函数使用了外部变量。第一,你可以使用Broadcast广播变量优化。第二,可以使用Kryo序列化类库,提升序列化性能和效率。第三,如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量,首先从源头上就减少内存的占用,通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内存占用。
2)、在你的算子函数里,也就是task要执行的计算逻辑里面,如果有逻辑中,出现,要创建比较大的Map、List等集合,可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历、存取等集合操作,此时,可以考虑将这些集合类型使用fastutil类库重写,使用了fastutil集合类以后,就可以在一定程度上,减少task创建出来的集合类型的内存占用。避免executor内存频繁占满,频繁唤起GC,导致性能下降。

1.6.3、关于fastutil调优的说明

fastutil其实没有你想象中的那么强大,也不会跟官网上说的效果那么一鸣惊人。广播变量、Kryo序列化类库、fastutil,都是之前所说的,对于性能来说,类似于一种调味品,烤鸡,本来就很好吃了,然后加了一点特质的孜然麻辣粉调料,就更加好吃了一点。分配资源、并行度、RDD架构与持久化,这三个就是烤鸡。broadcast、kryo、fastutil,类似于调料。
比如说,你的spark作业,经过之前一些调优以后,大概30分钟运行完,现在加上broadcast、kryo、fastutil,也许就是优化到29分钟运行完、或者更好一点,也许就是28分钟、25分钟。
shuffle调优,15分钟。groupByKey用reduceByKey改写,执行本地聚合,也许10分钟。跟公司申请更多的资源,比如资源更大的YARN队列,1分钟。

1.6.4、fastutil的使用

在pom.xml中引用fastutil的包

fastutil
fastutil
5.0.9

速度比较慢,可能是从国外的网去拉取jar包,可能要等待5分钟,甚至几十分钟,不等
List 相当于 IntList
基本都是类似于IntList的格式,前缀就是集合的元素类型。特殊的就是Map,Int2IntMap,代表了key-value映射的元素类型。除此之外,还支持object、reference。

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

1.7.1、task的locality有五种

1)、PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中。计算数据的task由executor执行,数据在executor的BlockManager中,性能最好。
2)、NODE_LOCAL:节点本地化,代码和数据在同一个节点中。比如说,数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中运行,或者是,数据和task在一个节点上的不同executor中,数据需要在进程间进行传输。
3)、NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分。
4)、RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上,数据需要通过网络在节点之间进行传输。
5)、ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差。

1.7.2、Spark的任务调度

Spark在Driver上,对Application的每一个stage的task进行分配之前都会计算出每个task要计算的是哪个分片数据。Spark的task分配算法优先会希望每个task正好分配到它要计算的数据所在的节点,这样的话,就不用在网络间传输数据。
但是,有时可能task没有机会分配到它的数据所在的节点。为什么呢,可能那个节点的计算资源和计算能力都满了。所以这种时候, Spark会等待一段时间,默认情况下是3s(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后,实在是等待不了了,就会选择一个比较差的本地化级别。比如说,将task分配到靠它要计算的数据所在节点比较近的一个节点,然后进行计算。
但是对于第二种情况,通常来说,肯定是要发生数据传输,task会通过其所在节点的BlockManager来获取数据,BlockManager发现自己本地没有数据,会通过一个getRemote()方法,通过TransferService(网络数据传输组件)从数据所在节点的BlockManager中,获取数据,通过网络传输回task所在节点。
对于我们来说,当然不希望是类似于第二种情况的了。最好的,当然是task和数据在一个节点上,直接从本地executor的BlockManager中获取数据,纯内存,或者带一点磁盘IO。如果要通过网络传输数据的话,性能肯定会下降的。大量网络传输,以及磁盘IO,都是性能的杀手。

1.7.3、我们什么时候要调节这个参数

观察spark作业的运行日志。推荐大家在测试的时候,先用client模式在本地就直接可以看到比较全的日志。日志里面会显示:starting task…,PROCESS LOCAL、NODE LOCAL
观察大部分task的数据本地化级别,如果大多都是PROCESS_LOCAL,那就不用调节了。
如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长。要反复调节,每次调节完以后再运行并观察日志,看看大部分的task的本地化级别有没有提升,看看整个spark作业的运行时间有没有缩短。注意,不要本末倒置,不要本地化级别是提升了,但是因为大量的等待时长,spark作业的运行时间反而增加了,那还是不要调节了。

1.7.4、怎么调节

spark.locality.wait,默认是3s。6s,10s
默认情况下,下面3个的等待时长,都是跟上面那个是一样的,都是3s
spark.locality.wait.process
spark.locality.wait.node
spark.locality.wait.rack
new SparkConf().set(“spark.locality.wait”, “10”)

以上是关于Spark的性能调优,如有问题,欢迎批评指正 !或者你有更多的调优总结,我们可以多多交流 !

猜你喜欢

转载自blog.csdn.net/weixin_38653290/article/details/84170279