1.性能调优
1.1 分配更多资源
- 分配哪些资源?
Executor的数量
每个Executor所能分配的CPU数量
每个Executor所能分配的内存量
Driver端分配的内存数量
- 在哪里分配这些资源?
# 在生产环境中,提交spark作业时,用的spark-submit shell脚本,里面调整对应的参数:
/usr/local/spark/bin/spark-submit\
--classcn.spark.sparktest.core.WordCountCluster \
--num-executors3 \ 配置executor的数量
--driver-memory100m \ 配置driver的内存(影响不大)
--executor-memory100m \ 配置每个executor的内存大小
--total-executor-cores3 \ 配置所有executor的cpu core数量
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar\
- 调节到多大,算是最大呢?
常用的资源调度模式有Spark Standalone和Spark On Yarn。比如说你的每台机器能够给你使用60G内存,10个cpu core,20台机器。那么executor的数量是20。平均每个executor所能分配60G内存和10个cpu core。
- 为什么多分配了这些资源以后,性能会得到提升?
增加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数量,就是100个task。就物理性能来看,执行的速度,提升了2倍。
增加每个executor的内存量。增加了内存量以后,对性能的提升,有三点:
1、如果需要对RDD进行cache,那么更多的内存,就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘。减少了磁盘IO。
2、对于shuffle操作,reduce端,会需要内存来存放拉取的数据并进行聚合。如果内存不够,也会写入磁盘。如果给executor分配更多内存以后,就有更少的数据,需要写入磁盘,甚至不需要写入磁盘。减少了磁盘IO,提升了性能。
3、对于task的执行,可能会创建很多对象。如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC,垃圾回收,minor GC和full GC。(速度很慢)。内存加大以后,带来更少的GC,垃圾回收,避免了速度变慢,速度变快了。
1.2 调节并行度
- 并行度的概念
就是指的是Spark作业中,各个stage的task数量,代表了Spark作业的在各个阶段(stage)的并行度。
- 如果不调节并行度,导致并行度过低,会怎么样?
比如现在spark-submit脚本里面,给我们的spark作业分配了足够多的资源,比如50个executor,每个executor有10G内存,每个executor有3个cpu core。基本已经达到了集群或者yarn队列的资源上限。task没有设置,或者设置的很少,比如就设置了100个task,50个executor,每个executor有3个cpu core,也就是说,你的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作业的性能和运行速度。
- 设置并行度
-
task数量,至少设置成与Spark application的总cpu core数量相同(最理想情况,比如总共150个cpu core,分配了150个task,一起运行,差不多同一时间运行完毕)。
-
官方是推荐,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作业运行的效率和速度,提升性能。
-
如何设置一个Spark并行度?
#参数 spark.default.parallelism
SparkConf conf = newSparkConf().set("spark.default.parallelism","500")
1.3、 重构RDD架构以及RDD持久化
- RDD架构重构与优化
尽量去复用RDD,差不多的RDD,可以抽取成为一个共同的RDD,供后面的RDD计算时,反复使用。
- 公共RDD一定要实现持久化
对于要多次计算和使用的公共RDD,一定要进行持久化。
持久化,就是将RDD的数据缓存到内存中/磁盘中(BlockManager)以后无论对这个RDD做多少次计算,那么都是直接取这个RDD的持久化的数据,比如从内存中或者磁盘中,直接提取一份数据。
- 持久化,是可以进行序列化的
如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许,会导致OOM内存溢出。
当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑使用序列化的方式在纯内存中存储。将RDD的每个partition的数据,序列化成一个大的字节数组,就一个对象。序列化后,大大减少内存的空间占用。
序列化的方式,唯一的缺点就是,在获取数据的时候,需要反序列化。
如果序列化纯内存方式,还是导致OOM内存溢出,就只能考虑磁盘的方式、内存+磁盘的普通方式(无序列化)、内存+磁盘(序列化)。
- 为了数据的高可靠性,而且内存充足,可以使用双副本机制,进行持久化。
持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次。持久化的每个数据单元,存储一份副本,放在其他节点上面。从而进行容错。一个副本丢了,不用重新计算,还可以使用另外一份副本。这种方式,仅仅针对你的内存资源极度充足的情况。
1.4、 广播变量
-
概念及需求
Spark Application(我们自己写的Spark作业)最开始在Driver端,在我们提交任务的时候,需要传递到各个Executor的Task上运行。对于一些只读、固定的数据(比如从DB中读出的数据),每次都需要Driver广播到各个Task上,这样效率低下。广播变量允许将变量只广播(提前广播)给各个Executor。该Executor上的各个Task再从所在节点的BlockManager获取变量,如果本地没有,那么就从Driver远程拉取变量副本,并保存在本地的BlockManager中。此后这个executor上的task,都会直接使用本地的BlockManager中的副本。而不是从Driver获取变量,从而提升了效率。一个Executor只需要在第一个Task启动时,获得一份Broadcast数据,之后的Task都从本节点的BlockManager中获取相关数据。 -
使用方法
-
调用SparkContext.broadcast方法创建一个Broadcast[T]对象。任何序列化的类型都可以这么实现。
-
通过value属性访问改对象的值(Java之中为value()方法)
-
变量只会被发送到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)
1.5、使用Kryo序列化
- 概念及需求
默认情况下,Spark内部是使用Java的序列化机制,ObjectOutputStream / ObjectInputStream,对象输入输出流机制,来进行序列化。
这种默认序列化机制的好处在于,处理起来比较方便,也不需要我们手动去做什么事情,只是,你在算子里面使用的变量,必须是实现Serializable接口的,可序列化即可。
但是缺点在于,默认的序列化机制的效率不高,序列化的速度比较慢,序列化以后的数据,占用的内存空间相对还是比较大。
Spark支持使用Kryo序列化机制。这种序列化机制,比默认的Java序列化机制速度要快,序列化后的数据更小,大概是Java序列化机制的1/10。
所以Kryo序列化优化以后,可以让网络传输的数据变少,在集群中耗费的内存资源大大减少。
- Kryo序列化机制启用以后生效的几个地方
- 算子函数中使用到的外部变量,使用Kryo以后:优化网络传输的性能,可以优化集群中内存的占用和消耗
- 持久化RDD,优化内存的占用和消耗。持久化RDD占用的内存越少,task执行的时候,创建的对象,就不至于频繁的占满内存,频繁发生GC。
- shuffle:可以优化网络传输的性能
- 使用方法
#第一步,在SparkConf中设置一个属性,spark.serializer,org.apache.spark.serializer.KryoSerializer类。
.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
#第二步,注册你使用的需要通过Kryo序列化的一些自定义类,SparkConf.registerKryoClasses()
.registerKryoClasses(newClass[]{CategorySortKey.class})
1.6、使用fastutil优化数据格式
-
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尽量提供了在任何场景下都是速度最快的集合类库。 -
Spark中应用fastutil的场景
- 如果算子函数使用了外部变量。第一,你可以使用Broadcast广播变量优化。第二,可以使用Kryo序列化类库,提升序列化性能和效率。第三,如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量,首先从源头上就减少内存的占用,通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内存占用。
- 在你的算子函数里,也就是task要执行的计算逻辑里面,如果有逻辑中,出现,要创建比较大的Map、List等集合,可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历、存取等集合操作,此时,可以考虑将这些集合类型使用fastutil类库重写,使用了fastutil集合类以后,就可以在一定程度上,减少task创建出来的集合类型的内存占用。避免executor内存频繁占满,频繁唤起GC,导致性能下降。
- 关于fastutil调优的说明
fastutil其实没有你想象中的那么强大,也不会跟官网上说的效果那么一鸣惊人。广播变量、Kryo序列化类库、fastutil,都是之前所说的,对于性能来说,类似于一种调味品,烤鸡,本来就很好吃了,然后加了一点特质的孜然麻辣粉调料,就更加好吃了一点。分配资源、并行度、RDD架构与持久化,这三个就是烤鸡。broadcast、kryo、fastutil,类似于调料。
比如说,你的spark作业,经过之前一些调优以后,大概30分钟运行完,现在加上broadcast、kryo、fastutil,也许就是优化到29分钟运行完、或者更好一点,也许就是28分钟、25分钟。
shuffle调优,15分钟。groupByKey用reduceByKey改写,执行本地聚合,也许10分钟。跟公司申请更多的资源,比如资源更大的YARN队列,1分钟。
1.7、调节数据本地化等待时长
- task的locality有五种
- PROCESS_LOCAL:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中。计算数据的task由executor执行,数据在executor的BlockManager中,性能最好。
- NODE_LOCAL:节点本地化,代码和数据在同一个节点中。比如说,数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中运行,或者是,数据和task在一个节点上的不同executor中,数据需要在进程间进行传输。
- NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分。
- RACK_LOCAL:机架本地化,数据和task在一个机架的两个节点上,数据需要通过网络在节点之间进行传输。
- ANY:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差。
- 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,都是性能的杀手。
- 我们什么时候要调节这个参数
观察spark作业的运行日志。推荐大家在测试的时候,先用client模式在本地就直接可以看到比较全的日志。日志里面会显示:starting task…,PROCESS LOCAL、NODE LOCAL
观察大部分task的数据本地化级别,如果大多都是PROCESS_LOCAL,那就不用调节了。
如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节一下数据本地化的等待时长。要反复调节,每次调节完以后再运行并观察日志,看看大部分的task的本地化级别有没有提升,看看整个spark作业的运行时间有没有缩短。注意,不要本末倒置,不要本地化级别是提升了,但是因为大量的等待时长,spark作业的运行时间反而增加了,那还是不要调节了。
- 怎么调节
#spark.locality.wait,默认是3s
#默认情况下,下面3个的等待时长,都是跟上面那个是一样的,都是3s
spark.locality.wait.process
spark.locality.wait.node
spark.locality.wait.rack
newSparkConf().set("spark.locality.wait", "10")
2、JVM调优
堆内存存放我们创建的一些对象,有老年代和年轻代。理想情况下,老年代都是放一些生命周期很长的对象,数量应该是很少的,比如数据库连接池。我们在spark task执行算子函数(我们自己写的),可能会创建很多对象,这些对象都是要放入JVM年轻代中的。每一次放对象的时候,都是放入eden区域,和其中一个survivor区域。另外一个survivor区域是空闲的。当eden区域和一个survivor区域放满了以后(spark运行过程中,产生的对象实在太多了),就会触发minor gc,小型垃圾回收。把不再使用的对象,从内存中清空,给后面新创建的对象腾出来点儿地方。清理掉了不再使用的对象之后,那么也会将存活下来的对象(还要继续使用的),放入之前空闲的那一个survivor区域中。这里可能会出现一个问题。默认eden、survior1和survivor2的内存占比是8:1:1。问题是,如果存活下来的对象是1.5,一个survivor区域放不下。此时就可能通过JVM的担保机制(不同JVM版本可能对应的行为),将多余的对象,直接放入老年代了。
如果你的JVM内存不够大的话,可能导致频繁的年轻代内存满溢,频繁的进行minor gc。频繁的minor gc会导致短时间内,有些存活的对象,多次垃圾回收都没有回收掉。会导致这种短生命周期(其实不一定是要长期使用的)对象,年龄过大,垃圾回收次数太多还没有回收到,跑到老年代。
老年代中,可能会因为内存不足,囤积一大堆,短生命周期的,本来应该在年轻代中的,可能马上就要被回收掉的对象。此时,可能导致老年代频繁满溢。频繁进行full gc(全局/全面垃圾回收)。full gc就会去回收老年代中的对象。full gc由于这个算法的设计,是针对的是,老年代中的对象数量很少,满溢进行full gc的频率应该很少,因此采取了不太复杂,但是耗费性能和时间的垃圾回收算法。full gc很慢。full gc / minor gc,无论是快,还是慢,都会导致jvm的工作线程停止工作,stop the world。简而言之,就是说,gc的时候,spark停止工作了。等着垃圾回收结束。
内存不充足的时候,出现的问题:
1、频繁minor gc,也会导致频繁spark停止工作
2、老年代囤积大量活跃对象(短生命周期的对象),导致频繁full gc,full gc时间很长,短则数十秒,长则数分钟,甚至数小时。可能导致spark长时间停止工作。
3、严重影响咱们的spark的性能和运行的速度。
2.1、降低cache操作的内存占比
spark中,堆内存又被划分成了两块,一块是专门用来给RDD的cache、persist操作进行RDD数据缓存用的。另外一块用来给spark算子函数的运行使用的,存放函数中自己创建的对象。
默认情况下,给RDD cache操作的内存占比,是0.6,60%的内存都给了cache操作了。但是问题是,如果某些情况下cache不是那么的紧张,问题在于task算子函数中创建的对象过多,然后内存又不太大,导致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作。性能影响会很大。
针对上述这种情况,可以在任务运行界面,去查看你的spark作业的运行统计,可以看到每个stage的运行情况,包括每个task的运行时间、gc时间等等。如果发现gc太频繁,时间太长。此时就可以适当调价这个比例。
降低cache操作的内存占比,大不了用persist操作,选择将一部分缓存的RDD数据写入磁盘,或者序列化方式,配合Kryo序列化类,减少RDD缓存的内存占用。降低cache操作内存占比,对应的,算子函数的内存占比就提升了。这个时候,可能就可以减少minor gc的频率,同时减少full gc的频率。对性能的提升是有一定的帮助的。
一句话,让task执行算子函数时,有更多的内存可以使用。
spark.storage.memoryFraction,0.6 ->0.5 -> 0.4 -> 0.2
2.2、调节executor堆外内存与连接等待时长
调节executor堆外内存
有时候,如果你的spark作业处理的数据量特别大,几亿数据量。然后spark作业一运行,时不时的报错,shuffle file cannot find,executor、task lost,out of
memory(内存溢出)。
可能是executor的堆外内存不太够用,导致executor在运行的过程中,可能会内存溢出,可能导致后续的stage的task在运行的时候,要从一些executor中去拉取shuffle map output文件,但是executor可能已经挂掉了,关联的block
manager也没有了。所以会报shuffle output file not found,resubmitting task,executor lost。spark作业彻底崩溃。
上述情况下,就可以去考虑调节一下executor的堆外内存。也许就可以避免报错。此外,有时堆外内存调节的比较大的时候,对于性能来说,也会带来一定的提升。
#可以调节堆外内存的上限:
#park-submit脚本里面,去用--conf的方式,去添加配置。
--conf spark.yarn.executor.memoryOverhead=2048
# 用new SparkConf().set()这种方式去设置是没有用的!
默认情况下,这个堆外内存上限大概是300M。通常在项目中,真正处理大数据的时候,这里都会出现问题,导致spark作业反复崩溃,无法运行。此时就会去调节这个参数,到至1G(1024M),甚至说2G、4G。
通常这个参数调节上去以后,就会避免掉某些JVM OOM的异常问题,同时呢,会让整体spark作业的性能,得到较大的提升。
调节连接等待时长
我们知道,executor会优先从自己本地关联的BlockManager中获取某份数据。如果本地block manager没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取。
而此时上面executor去远程连接的那个executor,因为task创建的对象特别大,特别多,
频繁的让JVM堆内存满溢,正在进行垃圾回收。而处于垃圾回收过程中,所有的工作线程全部停止,相当于只要一旦进行垃圾回收,spark / executor停止工作,无法提供响应。
此时呢,就会没有响应,无法建立网络连接,会卡住。spark默认的网络连接的超时时长,是60s,如果卡住60s都无法建立连接的话,那么就宣告失败了。
报错几次,几次都拉取不到数据的话,可能会导致spark作业的崩溃。也可能会导致DAGScheduler,反复提交几次stage。TaskScheduler反复提交几次task。大大延长我们的spark作业的运行时间。
#可以考虑调节连接的超时时长
# spark-submit脚本,切记,不是在new SparkConf().set()这种方式来设置的
--conf spark.core.connection.ack.wait.timeout=300
spark.core.connection.ack.wait.timeout(spark core,connection,连接,ack,wait timeout,建立不上连接的时候,超时等待时长)
调节这个值比较大以后,通常来说,可以避免部分的偶尔出现的某某文件拉取失败,某某文件lost掉了。
3、Shuffle调优
原理概述:
什么样的情况下,会发生shuffle?
在spark中,主要是以下几个算子:groupByKey、reduceByKey、countByKey、join,等等。
什么是shuffle?
groupByKey,要把分布在集群各个节点上的数据中的同一个key,对应的values,都要集中到一块儿,集中到集群中同一个节点上,更严密一点说,就是集中到一个节点的一个executor的一个task中。
然后呢,集中一个key对应的values之后,才能交给我们来进行处理,>。reduceByKey,算子函数去对values集合进行reduce操作,最后变成一个value。countByKey需要在一个task中,获取到一个key对应的所有的value,然后进行计数,统计一共有多少个value。join,RDD,RDD
value>,只要是两个RDD中,key相同对应的2个value,都能到一个节点的executor的task中,给我们进行处理。
shuffle,一定是分为两个stage来完成的。因为这其实是个逆向的过程,不是stage决定shuffle,是shuffle决定stage。
reduceByKey(+),在某个action触发job的时候,DAGScheduler,会负责划分job为多个stage。划分的依据,就是,如果发现有会触发shuffle操作的算子,比如reduceByKey,就将这个操作的前半部分,以及之前所有的RDD和transformation操作,划分为一个stage。shuffle操作的后半部分,以及后面的,直到action为止的RDD和transformation操作,划分为另外一个stage。
3.1、合并map端输出文件
- 如果不合并map端输出文件的话,会怎么样?
举例实际生产环境的条件:
100个节点(每个节点一个executor):100个executor
每个executor:2个cpu core
总共1000个task:每个executor平均10个task
每个节点,10个task,每个节点会输出多少份map端文件?10 * 1000=1万个文件
总共有多少份map端输出文件?100 * 10000 = 100万。
第一个stage,每个task,都会给第二个stage的每个task创建一份map端的输出文件
第二个stage,每个task,会到各个节点上面去,拉取第一个stage每个task输出的,属于自己的那一份文件。
shuffle中的写磁盘的操作,基本上就是shuffle中性能消耗最为严重的部分。
通过上面的分析,一个普通的生产环境的spark job的一个shuffle环节,会写入磁盘100万个文件。
磁盘IO对性能和spark作业执行速度的影响,是极其惊人和吓人的。
基本上,spark作业的性能,都消耗在shuffle中了,虽然不只是shuffle的map端输出文件这一个部分,但是这里也是非常大的一个性能消耗点。
- 开启shuffle map端输出文件合并的机制
new SparkConf().set("spark.shuffle.consolidateFiles","true")
默认情况下,是不开启的,就是会发生如上所述的大量map端输出文件的操作,严重影响性能。
- 合并map端输出文件,对咱们的spark的性能有哪些方面的影响呢?
- map task写入磁盘文件的IO,减少:100万文件-> 20万文件
- 第二个stage,原本要拉取第一个stage的task数量份文件,1000个task,第二个stage的每个task,都要拉取1000份文件,走网络传输。合并以后,100个节点,每个节点2个cpu core,第二个stage的每个task,主要拉取100 * 2 = 200个文件即可。此时网络传输的性能消耗也大大减少。
分享一下,实际在生产环境中,使用了spark.shuffle.consolidateFiles机制以后,实际的性能调优的效果:对于上述的这种生产环境的配置,性能的提升,还是相当的可观的。spark作业,5个小时-> 2~3个小时。
大家不要小看这个map端输出文件合并机制。实际上,在数据量比较大,你自己本身做了前面的性能调优,executor上去->cpu core上去->并行度(task数量)上去,shuffle没调优,shuffle就很糟糕了。大量的map端输出文件的产生,对性能有比较恶劣的影响。
这个时候,去开启这个机制,可以很有效的提升性能。
3.2、调节map端内存缓冲与reduce端内存占比
3.2.1、默认情况下可能出现的问题
默认情况下,shuffle的map task,输出到磁盘文件的时候,统一都会先写入每个task自己关联的一个内存缓冲区。
这个缓冲区大小,默认是32kb。
每一次,当内存缓冲区满溢之后,才会进行spill溢写操作,溢写到磁盘文件中去。
reduce端task,在拉取到数据之后,会用hashmap的数据格式,来对各个key对应的values进行汇聚。
针对每个key对应的values,执行我们自定义的聚合函数的代码,比如_ + _(把所有values累加起来)。
reduce task,在进行汇聚、聚合等操作的时候,实际上,使用的就是自己对应的executor的内存,executor(jvm进程,堆),默认executor内存中划分给reduce task进行聚合的比例是0.2。
问题来了,因为比例是0.2,所以,理论上,很有可能会出现,拉取过来的数据很多,那么在内存中,放不下。这个时候,默认的行为就是将在内存放不下的数据都spill(溢写)到磁盘文件中去。
在数据量比较大的情况下,可能频繁地发生reduce端的磁盘文件的读写。
- 调优方式
调节map task内存缓冲:spark.shuffle.file.buffer,默认32k(spark 1.3.x不是这个参数,后面还有一个后缀,kb。spark 1.5.x以后,变了,就是现在这个参数)
调节reduce端聚合内存占比:spark.shuffle.memoryFraction,0.2
- 在实际生产环境中,我们在什么时候来调节两个参数?
看Spark UI,如果你的公司是决定采用standalone模式,那么狠简单,你的spark跑起来,会显示一个Spark UI的地址,4040的端口。进去观察每个stage的详情,有哪些executor,有哪些task,每个task的shuffle write和shuffle read的量,shuffle的磁盘和内存读写的数据量。如果是用的yarn模式来提交,从yarn的界面进去,点击对应的application,进入Spark UI,查看详情。
如果发现shuffle 磁盘的write和read,很大。这个时候,就意味着最好调节一些shuffle的参数。首先当然是考虑开启map端输出文件合并机制。其次调节上面说的那两个参数。调节的时候的原则:spark.shuffle.file.buffer每次扩大一倍,然后看看效果,64,128。spark.shuffle.memoryFraction,每次提高0.1,看看效果。
不能调节的太大,太大了以后过犹不及,因为内存资源是有限的,你这里调节的太大了,其他环节的内存使用就会有问题了。
- 调节以后的效果
map task内存缓冲变大了,减少spill到磁盘文件的次数。reduce端聚合内存变大了,减少spill到磁盘的次数,而且减少了后面聚合读取磁盘文件的数量。
3.3、HashShuffleManager与SortShuffleManager
- shuffle调优概述
大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO、序列化、网络数据传输等操作。因此,如果要让作业的性能更上一层楼,就有必要对shuffle过程进行调优。但是也必须提醒大家的是,影响一个Spark作业性能的因素,主要还是代码开发、资源参数以及数据倾斜,shuffle调优只能在整个Spark的性能调优中占到一小部分而已。因此大家务必把握住调优的基本原则,千万不要舍本逐末。下面我们就给大家详细讲解shuffle的原理,以及相关参数的说明,同时给出各个参数的调优建议。
- ShuffleManager发展概述
在Spark的源码中,负责shuffle过程的执行、计算和处理的组件主要就是ShuffleManager,也即shuffle管理器。
在Spark 1.2以前,默认的shuffle计算引擎是HashShuffleManager。该ShuffleManager而HashShuffleManager有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而由大量的磁盘IO操作影响了性能。
因此在Spark 1.2以后的版本中,默认的ShuffleManager改成了SortShuffleManager。SortShuffleManager相较于HashShuffleManager来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个Task就只有一个磁盘文件。在下一个stage的shuffle read task拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。
在spark 1.5.x以后,对于shuffle manager又出来了一种新的manager,tungsten-sort(钨丝),钨丝sort shuffle manager。官网上一般说,钨丝sort
shuffle manager,效果跟sort shuffle manager是差不多的。
但是,唯一的不同之处在于,钨丝manager,是使用了自己实现的一套内存管理机制,性能上有很大的提升,而且可以避免shuffle过程中产生的大量的OOM,GC,等等内存相关的异常。
- hash、sort、tungsten-sort。如何来选择?
-
需不需要数据默认就让spark给你进行排序?就好像mapreduce,默认就是有按照key的排序。如果不需要的话,其实还是建议搭建就使用最基本的HashShuffleManager,因为最开始就是考虑的是不排序,换取高性能。
-
什么时候需要用sort shuffle
manager?如果你需要你的那些数据按key排序了,那么就选择这种吧,而且要注意,reduce task的数量应该是超过200的,这样sort、merge(多个文件合并成一个)的机制,才能生效把。但是这里要注意,你一定要自己考量一下,有没有必要在shuffle的过程中,就做这个事情,毕竟对性能是有影响的。 -
如果你不需要排序,而且你希望你的每个task输出的文件最终是会合并成一份的,你自己认为可以减少性能开销。可以去调节bypassMergeThreshold这个阈值,比如你的reduce task数量是500,默认阈值是200,所以默认还是会进行sort和直接merge的。可以将阈值调节成550,不会进行sort,按照hash的做法,每个reduce
task创建一份输出文件,最后合并成一份文件。(一定要提醒大家,这个参数,其实我们通常不会在生产环境里去使用,也没有经过验证说,这样的方式,到底有多少性能的提升)
总结:
1、在生产环境中,不建议大家贸然使用第三点:
2、如果你不想要你的数据在shuffle时排序,那么就自己设置一下,用hash shuffle manager。
3、如果你的确是需要你的数据在shuffle时进行排序的,那么就默认不用动,默认就是sort shuffle manager。或者是什么?如果你压根儿不care是否排序这个事儿,那么就默认让他就是sort的。调节一些其他的参数(consolidation机制)。(80%,都是用这种)
4、spark.shuffle.manager:hash、sort、tungsten-sort
spark.shuffle.sort.bypassMergeThreshold:200。自己可以设定一个阈值,默认是200,当reduce task数量少于等于200,map task创建的输出文件小于等于200的,最后会将所有的输出文件合并为一份文件。这样做的好处,就是避免了sort排序,节省了性能开销,而且还能将多个reduce task的文件合并成一份文件,节省了reduce task拉取数据的时候的磁盘IO的开销。
作者:Frank_8942
链接:https://www.jianshu.com/p/1d4521db8f57
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。