spark优化
spark之资源分配
1.分配哪些资源
executor,cpu per executor,memory per executor,driver-memory
2.在哪里分配这些资源
/usr/local/spark/bin/spark-submit \
--class cn.spark.sparktest.core.WordCountCluster \
--num-executors 3\ 配置executor的数量
--driver-memory 100m \ 配置driver的内存(影响不大)
--executor-memory 100 \ 配置每个executor的cpu core数量
--executor-cores 3 \
/usr/local/SparkTest-0.0.1-SNAPSHOT-jar-with-dependencies.jar \
3.调节到多大算大呢?
第一种,Spark Standalone,公司集群上,搭建了一套Spark集群,你心里应该清楚每台机器还能给你使用的,大概有多少内存,多少cup core;那么设置的时候,就根据这个实际的情况,去调节每个spark作业的资源分配。比如说你的每台机器能够给你使用4G内存,2个cup core;20台机器;executor 20;4G内存,2个cup core,平均每个executor
第二种,yarn 资源队列,资源调度。应该去查看你的spark作业,要提交到的资源队列,大概有多少资源? 500G内存,100个cpu core;executor,50;10G内存,2个cup core,平均每个executor
4.为什么调节资源后,性能可以提升?
1.增加executor
如果executor数量比较少,那么,能够并行执行的task数量就比较少,意味着,我们的application的并行执行的能力就很弱。比如有3个executor,每个executor有2个cup core,那么同时能够并行执行的task,就是6个。6个执行完以后,再换下一批6个task。增加了executor的数量后,可以并行执行的任务就变多了
2.增加每个executor的cpu core ,也是增加执行的并行能力
3.增加每个executor的内存量,增加了内存量以后,对性能的提升,有两点:
1、如果需要对rdd进行cache,那么更多的内存,就可以缓存更多的数据,将更少的数据写入磁盘,甚至不写入磁盘,减少了磁盘io
2、对于shuffle操作,reduce端,会需要更多的内存存放拉取的数据并进行聚合,如果内存不够,也会写入磁盘,如果给executor分配更多内存以后,就有更少的数据写入磁盘,甚至不需要写入磁盘,减少io,提升性能
3、对于task 的执行,可能会创建很多对象,如果内存比较小,可能会频繁导致JVM堆内存满了,然后频繁GC,垃圾回收,minor GC和full GC(速度很慢)。内存加大后,带来更少的GC,垃圾回收,避免了速度变慢
spark优化之调节并行度
1、task数量,至少设置成与Spark application的总cup 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空闲出来了,就导致了浪费。
3、如何设置一个Spark Application的并行度?
Spark.default.parallelism
SparkConf conf = new SparkConf().set("Spark.default.parallelism","500")
spark优化之重构RDD架构以及RDD持久化
第一,RDD架构重构与优化
尽量去复用RDD,差不多的RDD,可以抽取为一个共同的RDD,供后面的RDD计算时,反复使用
第二,公共RDD一定要实现持久化
对于多次计算和使用的公共RDD,一定要进行持久化,也就是说,将RDD的数据缓存到内存、磁盘中,以后无论对这个RDD做多少次计算,那么都是直接取这个RDD的持久化的数据
第三,持久化是可以进行序列化的
如果正常将数据持久化内存中,那么可能导致内存的占用过大,这样的话,也许,会导致OOM内存溢出。
当纯内存无法支持公共RDD数据完全存放的时候,就有限考虑,使用序列化的方式在纯内存中存储,将RDD的每个partition的数据,序列化成一个大的字节数组,就一个对象,序列化后,大大减少内存的空间占用。
序列化的方式,唯一的缺点就是,在获取数据的时候,需要反序列,如果序列化纯内存方式还是导致OOM,内存溢出,就只能考虑磁盘的方式
RDD持久化
将数据通过持久化(或缓存)在内存中是Spark的重要功能之一,当你缓存了一个RDD,每个节点都缓存了RDD的所有分区,这样就可以在内存中进行计算,这样可以使以后在RDD上的动作更快(通常可以提高10倍)
RDD通过使用persist或cache方法进行标记,他通过动作操作第一次在RDD上进行计算后,他就会被缓存在节点上的内存中,spark的缓存具有容错性,如果RDD的某一分区丢失,它会自动使用最初创建RDD时的转换操作进行重新计算
持久化方法选择:persist(StorageLevel)
1.第一种选择:persist(StorageLevel.Memory_only),纯内存,无序列化,可以使用cache()方法代替
2.第二种选择:StorageLevel.Memory_only_ser 纯内存序列化
3.第三种选择:StorageLevel.Memory_and_desk 纯内存加磁盘
4.第四种选择:StorageLevel.Memory_and_desk_ser 纯内存加磁盘序列化
5.第五种选择:StorageLevel.Memory_desk_ser 纯磁盘
第四,为了数据的高可靠性,而且内存充足,可以使用双副本机制,进行持久化
持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次,持久化的每个数据单元,存储一份副本,放在其他节点上面;从而进行容错;一个副本丢了,不用重新计算,还可以使用另外一个副本。
这种方式仅仅针对你的内存资源极度充足
RDD本身(内存中),只有一个副本,如果丢失需要重新计算
RDD落地到hdfs,可以配置副本数:spark.hadoop.dfs.replication=2
spark优化之广播大变量
问题:没有广播的时候遇到了一个大变量map
比如:task执行的算子中,使用了外部的变量,每个task都会获取一份变量的副本,有什么缺点呢?在什么情况下,会出现性能上的恶劣影响呢?
map,本身是不小,存放数据的一个单位是entry,还有可能会用链表的格式来存放entry链条,所有map是比较消耗内存的数据格式。
加入有1000个task,这些task都用到了占用1m内存的map,那么首先,map会拷贝1000份副本,通过网络传输到各个task中,给task使用,总计有1G的数据,会通过网络传输,网络传输的开销,不容乐观!!网络传输也许就会消耗掉你的spark作业运行的总时间的一小部分。
map副本,传输到了各个task上之后是要占用内存的,1个map的确影响不大,1m,1000个map分布在你的集群中,一下子就消耗了1G。造成不必要的内存消耗和占用,导致你在进行rdd持久化到内存,也就就没法完全在内存中放下,就只能写入磁盘,最后导致后续的操作在磁盘io上消耗性能;你的task在创建对象的时候,也许会发现堆内存放不下所有对象,也许就就会导致频繁的垃圾回收器的回收,GC,GC的时候一定会导致工作线程停止,也就是导致spark暂停工作的时间,频繁GC的话对spark作业的速度影响很大
使用了广播变量后呢?
广播变量初始的时候,就在driver上有一份副本。
task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的executor对应的blockmanager中,尝试获取变量副本,如果本地没有,那么就从driver远程拉取变量副本,并保存在本地的blockmanager中;此后这个executor上的task,都会直接使用本地的blockmanager上拉取变量副本。
好处:不是每个task一份变量副本,而是变成每个节点的executor才一份副本。
spark优化之使用kryo序列化
1.为什么要进行序列化
在进行stage间的task的shuffle操作时,节点与节点之间的task会互相大量通过网络拉取和传输文件,此时,这些数据既然通过网络传输,也是可能要序列化的,就会使用kryo
2.为什么要使用kryo序列化
默认情况下,spark内部都是使用java的系列化机制,ObjectOutPutStream、ObjectInPutStream,对象输入输出流机制,来进行序列化。这种默认的序列化机制的好处在于,处理起来比较方便,也不需要我们手动去做什么事情,只是,你在算子里面使用的变量,必须是实现serializable接口的,可序列化即可。但是缺点在于,默认的序列化机制的效率不高,序列化的速度比较慢,系列化以后的数据,占用的内存空间相对还是比较大。可以手动进行序列化格式的优化
spark支持使用kryo序列化机制,kryo序列化机制,比默认的java序列化机制,速度要快,序列化后的数据更小,大概是java序列化机制的1/10.所以kryo序列化优化以后,可以让网络传输的数据变少,在集群中耗费的内存资源大大减少。
3.kryo序列化作用在什么地方?
1.算子函数中使用到的外部变量 优化网络传输的性能,可以优化集群中内存的占用和消耗
2.持久化rdd时进行序列化 优化内存中的占用和消耗,持久化rdd占用的内存越少,task执行的时候,创建的对象,就不至于频繁的占满内存,频繁发生GC
3.shuffle 优化网络传输的性能
4.如何实现序列化?
sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
首先第一步,在sparkConf中设置序列化属性,之所以没有作为默认的序列化类库的原因:因为kryo要求,如果要达到它的最佳性能的话,一定要注册你自定义的类
第二步,注册你使用到的,需要通过kryo序列化的,一些自定义类,sparkconf。registerKryoClasses()
项目中的使用:
sparkConf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer").registerKryoClasses(new Class[]{CategorySortKey.class})
如:
SparkSession.builder().master("local").config("spark.serializer","org.apache.spark.serializer.KryoSerializer")
.config("spark.kryo.registrator","MyRegistor")
.appName(Constants.SPARK_APP_NAME_SESSION).getOrCreate()
public class MyRegistor implements KryoRegistrator{
public void registerClasses(Kryoryo){
kryo.register(CategorySortKey.class)
}
}
sprak优化之使用fastUtil?
1.什么是fastutil
fastutil是扩展了java标准集合框架的类库,提供了特殊类型的map,set,list,queue;
fastutil能够提供更小的内存占用,更快的存取速度,我们使用fastutil提供的集合类来替代自己平时使用的jdk原生的map,list,set,好处在于,可以减少内存的占用,并且在进行集合的遍历、根据索引获取元素的值和设置元素的值的时候,提供更快的存取速度;
fastutil也提供了64位的array,set,list,以及高性能快速的,以及实用的io类,来处理二进制和文本类型的文件;
2.spark中应用fastutil的场景
1.如果算子函数使用了外部变量,那么第一,你可以实用broadcast广播变量优化;第二,可以使用kryo序列化类库,提升序列化性能和效率;第三,如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量,首先从源头上就减少内存的占用,通过广播变量进一步减少内存占用,再通过kryo序列化类库进一步减少内存占用
2.在你的算子函数里,也就是task要执行的计算逻辑里面,如果有逻辑中出现,要创建比较大的map,list集合,可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历,存取等集合操作;那么此时,可以考虑将这些集合类型使用fastutil类库重写。
3.fastutil的使用
<dependency>
<groupId>fastutil</groupId>
<artifactId>fastutil</artifactId>
<version>5.0.9</version>
</denpendency>
spark优化之调节数据本地化等待时长
1.task数据获取级别
process_local:进程本地化,代码和数据在同一个进程中,也就是在同一个executor中,计算数据的task由executor执行,数据在exector的blockManager;性能最好
node_local:节点本地化,代码和数据在同一个节点中,比如说,数据作为一个hdfsblock块,就在节点上,而task在节点上某个executor中运行,或者是,数据和task在一个节点上的不同executor中,数据需要在进程间进行传输
no_per:对于task来说,数据从哪里获取都一样,没有好坏之分
rack_local:机架本地化,数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输
any:数据和task可能在集群中的任何地方,而且不在一个机架中,性能最差
spark.locality.wait 默认是3s
2.原理
spark在driver上,对于application的每一个stage的task,进行分配之前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;spark的task分配算法,优先会希望每个task正好分配到它要计算的数据所在的节点,这样的话,就不用在网络间传输数据;
但是通常来说,事与愿违,可能task没有机会分配到它的数据所在的节点,为什么呢?可能那个节点扥计算资源和计算能力都满了,所有这种时候,通常来说,spark会等待一段时间,默认情况是3s(不是绝对的,还有很多种情况,对不同的本地化级别,都会去等待),到最后实在是等待不了了,就会选择一个比较差的本地化级别,比如说,将task分配到靠它要计算的数据所在节点,比较近的一个节点,然后进行计算。
对于第二种情况,通常来说,肯定要发生数据传输,task会通过其所在节点blockmanager来获取数据,blockmanager发现自己本地没有数据,会通过一个个体Remote()方法,通过TransferService从数据所在节点的blockManager中获取数据,通过网络传输回task所在节点。
3.怎么调节
观察日志,spark作业的运行日志,建议测试的时候先用client模式,在本地就直接可以看到比较全的日志
日志里面会显示,starting task,,,,,procss local ,node local
观察大部分task的数据本地化级别
如果大多数都是process_local ,那就不用调节了。如果好多级别都是node_local,any,那么最好就去调节一下数据本地化的等待时长
调节完,反复观察,看看整个spark作业多的运行时间有没有缩短,如果本地化级别提升了,但是因为等待时长了,运行作业的时间反而增加了,那就不要调节了
怎么调节?
spark.locality.wait,默认是3s,6s,10s
默认情况下,下面3个的等待时长是以spark.localoity.wait为默认值的,都是3s
spark.locality.wait.process
spark.locality.wait.node
spark.locality.wait.rack
new SparkConf().set("spark.locality.wait","10")
spark优化之JVM调优
一、降低cache操作的内存占比
1.性能调优分为哪几块?
常规性能调优:分配资源,并行度。。。。
JVM(java虚拟机)调优:jvm相关的参数,通常情况下,如果你的硬件配置、基础的jvm配置都OK的话,JVM通常不会造成太严重的性能问题;反而更多的是,在troubleshooting中,jvm占了很重要的地位;JVM造成线上的spark作业运行报错,甚至失败,比如OOM等
shuffle调优(相当重要):spark在执行groupByKey,reduceByKey等操作时,shuffle环节的调优,这个很重要,在spark作业的运行过程中,只要一牵扯到有shuffle的操作,基本上shuffle的性能消耗,要占到整个spark作业的50%-90%,10%来运行map等操作,90%耗费在这两个shuffle操作。groupByKey,countByKey
spark操作调优(spark算子调优,比较重要):groupByKey,countByKey或aggregateByKey来重构实现,有些算子的性能,是比其他一些算子的性能要高的。foreachPartition替代foreach。如果一旦遇到合适的情况,效果还是不错的
2.JVM主要会存在哪些问题呢?
内存不足,我们rdd的缓存,task运行定义的算子函数,可能会创建很多对象,都可能会占用大量内存,没搞好的话,可能导致JVM出问题
3.原理
堆内存:堆是java虚拟机所管理的内存中最大的一块,被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都是在这里分配内存,也是是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆。
堆的大小可以通过-Xms(最小值)和-Xmx(最大值)参数设置
如果从内存回收的角度看,堆中还可以细分为:新生代和老年代
新生代:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space构成,可以通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivorSpace的大小
老年代:用于存放经过多次新生代GC仍然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,理想情况下,老年代都是放一些声明周期很长的对象,数量应该是很少的,比如数据库连接池。
每一次放对象的时候,都是放入Eden区域,和其中一个survivor区域;另外一个survivor是空闲的。
当eden区域和一个survivor区域放满了以后(spark运行过程中,产生的对象实在太多了),就会触发minor gc,小型垃圾回收,把不再使用的对象,从内存中情况,给后面创建的对象腾出来点地方。
清理掉了不再使用的对象之后,那么也将存活下来的对象(还要继续使用的),放入之前空闲的那一个survivor区域中,这里可能出现一个问题,默认eden,survivorvor1和survivor2的内存占比是8:1:1.问题是,如果存活下来的对象是1.5,一个survivor区域放不下,此时就可能通过JVM的担保机制(不同JVM版本可能对应的行为),将对于的对象直接放入老年代了。
如果你的内存不够大,可能导致频繁的年轻代内存满溢,频繁的进行minor gc,导致短时间内,有些存活对象,多次垃圾回收都没有回收掉,进而导致这种短声明周期(其实不一定要长期使用的)对象,年龄过大,垃圾回收次数太多还没有回收到,跑到老年代。
老年代中可能会因为你内存不足,囤积一大堆,短生命周期的,本来应该在年轻代中的,可能马上要被回收掉的对象,此时,可能导致老年代频繁满溢,频繁进行full gc(全局、全面垃圾回收)。full gc就会去回收老年代中的对象,full gc由于这个算法的设计,是针对的老年代中的对象数量很少,满溢进行full gc的频率应该很少,因此采取了不太复杂的,但是耗费性能和时间的垃圾回收算法,full gc很慢。
minor gc 、full gc 无论是块,还是慢,都导致JVM的工作线程停止工作,也就是说gc的时候,spark停止工作,等待垃圾回收结束
内存不足的时候产生的问题:
1、频繁minor gc,也会导致spark停止工作
2、老年代囤积大量活跃对象(短生命周期的对象),导致频繁full gc,full gc时间很长,短则数十秒,长则数分钟,甚至数小时,可能导致spark长时间停止工作
3、严重影响spark的性能和运行的速度
4.怎么设置
JVM调优的第一个点:降低cache操作的内存占比
spark中,堆内存又被划分成了两块,一块是专门用来给rdd的cache,persist操作进行RDD数据缓存用的,另一块就是我们刚才说的,用来给算子函数的运行使用的,存放函数中自己创建的对象。
默认情况下,给RDD cache操作的内存占比,是0.6.60%的内存都给了cache操作了,但问题是,某些情况下,cache不是那么的紧张,而task算子函数中创建的对象过多,内存又不太大,导致了频繁minor gc,进一步导致full gc,导致spark频繁的停止工作,性能影响很大
针对上述情况,yarn去运行的话,通过yarn界面,去查看spark作业的运行统计,一层一层点击进去,可能看到每个stage的运行情况,包括每个task的运行时间,gc时间等等,如果发现gc太频繁,时间太长,就可以适当调整这个比例。
降低cache操作的内存咱比,大不了用persist操作,将一部分缓存的rdd数据写入磁盘,或者序列化的方式,配合kryo序列化类,减少rdd缓存的内存占用,降低cache操作内存咱比,对应的算子函数的内存占比就提升了,有更多的内存可以使用
spark.storage.memoryFraction 0.6->0.5->0.4
二 调节executor堆外内存与连接等待时长
spark.yarn.executor.memoryOverhead = 2048
spark.core.connection.ack.timeout=300
1.调节executor堆外内存
如果你的spark作业处理的数据量特别特别大,几亿数据量,然后spark一运行,时不时的报错,shuffle file can not find,executor,task lost,out of memory(内存溢出),
可能是说executor的堆外内存不太够用,导致executor在运行的过程中,可能会内存溢出,然后导致后续的stage的task在运行的时候,可能要从一些executor中拉取 shuffle map output 文件,但是executor已经挂掉了,关联的blockManager也没有了,spark作业彻底崩溃
上述情况下,就可以考虑调节一些堆外内存
注意:一定要在spark-submit脚本中去设置
默认情况下,堆外内存上限大概是300多m,调节这个参数,到1G,或者2G,4G,通常这个参数调节上以后,就会避免掉某些jvm oom的异常问题,同时spark作业的性能得到较大的提升
2.连接等待时长
一个executor跑着跑着,突然堆内内存不足了,堆外内存也不足了,可能会oom,挂掉,blockmanager也没有了,数据也丢失掉了。
另一个executro的task虽然通过driver的MapOutputTracker获取到了自己数据的地址,但是实际上去找对方的block manager获取数据的时候,是获取不到的,此时就会在spark-submit运行作业(jar),client(standalone client,yarn client),在本机就会打印log shuffle output file not found。。。DAGSchedular,resubmit task 一直会挂掉,反复挂掉,反复报错几次,spark作业就崩溃了。
但是偶尔有一种情况,输出一串fileid,uuid not found,file lost。很有可能是那份数据的executor在jvm gc,所以拉取数据的时候建立不了连接,然后超过默认的60s以后,直接宣告失败。这个时候可以考虑调节连接的超时时长
spark优化之Shuffle调优
一、shuffle原理
1.1 什么情况下会有shuffle
在spark中,主要是以下几个算子:groupByKey,reduceByKey,countByKey,join等等
1.2 什么是shuffle
groupByKey,要把分布在集群各个节点上的数据中的同一个key,对应的values,都给集中到一块儿,集中到集群中同一个节点上,更严密一点说,就是集中到一个节点的一个executor的一个task中,然后呢,集中一个key对应的values之后,才能交给我们来进行处理<key,Iterable<value>>;
reduceByKey,算子函数区队values集合进行reduce操作,最后变成一个value
countByKey,需要在一个task中,获取到一个key对应的所有的value,然后进行计数,统计总共有多少个value;
join,RDD<key,value>,RDD<key,value>,只要是两个RDD中,key相同对应的2个value,都能到一个节点的executor的task中,给我们进行处理
以reduceByKey为例:
每一个shuffle的前半部分stage的task,每个task都会创建下一个stage的task数量相同的文件,比如下一个stage会有100个task,那么当前stage每个task都会穿件100份文件,会将同一个value对应的values,一定是写入同一个文件中的,不同节点上的task,也一定会将同一个key对应的value,写入下一个stage,同一个task对应的文件中,shuffle的后半部分stage的task,每个task都会从各个节点上的task写的属于自己的那一份文件中,拉取key,value对,然后task会有一个内存缓冲区,然后会用hashmap进行key,values的汇聚;
task会用我们自己定义的聚合函数,比如reduceByKey(_+_),把所有values进行一对一的累加,聚合出来最终的值,就完成了shuffle
shuffle,一定是分成两个stage来完成的
reduceByKey(_+_),在某个action触发job的时候,DAGScheduler会负责划分job为多个stage,划分的依据,就是,如果发现有会触发shuffle操作的算子,比如reduceByKey,就将这个操作的前半部分记忆之前所有的rdd和transformation操作,划分为一个stage,shuffle操作的后半部分,以及后面的,直到action为止的rdd和transformation操作划分为另一个stage。
shuffle前半部分的task在写入数据到磁盘文件之前,都会先写入一个一个的内存缓冲,内存缓冲满溢之后,再spill溢写到磁盘文件中
二、合并Map端输出文件
new.SparkConf().set("spark.shuffle.comsolidateFiles","true")
开启shuffle map端输出文件合并的机制,默认情况下,是不开启的
三、Map端内存缓冲与Reduce端内存占比
默认情况下,shuffle的map task,输出到磁盘文件的时候,统一都会先写入每个task自己关联的一个内存缓冲区你,这个缓冲区大小,默认是32kb,每一次,当内存缓冲区满溢之后,才会进行spilt操作,溢写操作,溢写到磁盘文件中去。
reduce端task,在拉取到数据之后,会用hashmap的数据格式,来对各个key对应的values进行汇聚。针对每个key对应的values,执行我们自定义的聚合函数的代码,reduce task,在进行汇聚,聚合等操作的时候,实际上,使用的就是自己对应的executor的内存,默认executor内存中划分给reduce task进行汇聚的比例是0.2.理论上,很有可能会出现,拉取过来的数据很多,,那么在内存中放不下,这个时候,默认的行为,就是将内存中放不下的数据都spill(溢写)到磁盘文件中去。
如何调优?
调节map task内存缓冲:spark.shuffle.file.buffer,默认32k
调节reduce端聚合内存占比:spark.shuffle.memoryPraction 0.2
在实际生产环境中,什么时候来调节两个参数?
进入spark ui,查看详情,如果发现shuffle磁盘的write和read很大,这个时候意味着最好调节一些shuffle的参数,进行调优,首先是考虑开启map端输出文件合并机制,调节上面那两个参数,调节的时候的原则,spark.shuffle.file.buffer 每次扩大一倍,spark.shuffle.memoryPraction每次增加0.1 ,看看效果
4.1 shuffle 原理
HashShuffleManager(spark1.2)->优化后的HashShuffleManager->SortShuffleManager(包括两种机制:1.普通运行机制 2.byPass机制)
spark1.2以后默认SortShuffleManager
1.HashShuffleManager
上游的每一个task都会成为下游的每一个task创建一份文件,这样的话会产生大量的小文件
2.优化后的HashShuffleManager(map端合并)
两个task共用一个buffer,最后小文件的个数为core_number*下游task的数量
3.SortShuffleManager
会对每个reduce task要处理的数据,进行排序(默认的)
4.Bypass
触发的条件:shuffle mao task 数量小于spark.shuffle.sort.bypassMergeThreshold参数的值(默认为200)+不是排序类的shuffle算子(比如reduceByKey)
优点:避免了sort排序,节省了性能开销,而且还能将多个reduce的文件合并成为一份文件,节省了reduce task拉取数据的时候磁盘io的开销
4.2 怎样调优?
1.如果不需要按照key排序,建议使用优化后的HashShuffleManager
2.如果需要数据按key排序,可以选择SortShuffleManager,reduce task 的数量应该超过200,这样sort、merge的机制才能生效
3.如果不需要排序,并且希望每个task输出的文件最终是会合并成一份的,调节spark.shuffle.sort.bypassMergeThreshold > 200
五、参数总结
spark.shuffle.file.buffer 32k buffer大小默认32k maptask端的shuffle 降低磁盘IO
spark.reduce.MaxSizeFlight 48M shuffle read 拉取数据量的大小
spark.shuffle.memoryFraction 0.2 shuffle聚合内存的比率
spark.shuffle.io.maxRetries 拉取数据重试次数
spark.shuffle.io.retryWait 调整到重试间隔时间60s
spark.shuffle.memory hash|sort Spark shuffle的种类
spark.shuffle.consolidateFile false 针对hashShuffle 合并机制
spark.shuffle.sort.bypassMergeThreshold 针对SortShuffle Sortshufffle bypass机制 200次
spark优化之算子调优
一、MapPartitions提升Map类操作性能
spark中最基本的原则,就是每个task处理一个rdd的partition
缺点:可能oom
怎么调优?
当数据量不是特别大的时候,都可以使用这种MapPartition系列操作,性能提升比较明显
二、filter过后使用coalesce算子减少分区
默认情况下,经过filter之后,rdd的每个partition的数据量,可能都不太一样了
问题:
1.每个partition数量变少了,但是在后面进行处理的时候,还是要跟partition数量一样数量的task,来进行处理,有点浪费task计算资源
2.每个partition的数据量不一样,会导致后面的每个task处理每个partition的时候,每个task要处理的数据量就不同,这个时候容易发生数据倾斜。。。比如第一个partition的数据量才100,第二个partition的数据量是900,那么在后面的task处理逻辑一样的情况下,不同的task处理的数据量可以差别达到9倍,导致有些task运行快,有些运行慢。
如何优化?
coalesce算子
主要就是用于在filter算子操作后,针对每个partition的数据量各不相同的情况下,来压缩partition的数量,减少partition的数量,而且让每个partition的数据量都尽量均匀紧凑,从而便于后面的task进行计算操作,在某种程度上,能够一定程度的提升性能
三、使用foreachPartition优化写数据库的性能
默认的foreach的性能缺陷在哪里?
首先,对于每条数据,都要单独去调用一次function,task 为每个数据,都要去执行一次function函数。如果100w条数据(一个partition),调用100w次,性能比较差,另外如果每个数据都去创建一个数据库连接的话,那么就得创建100w次数据库连接,而且数据库连接池的创建和销毁都非常的消耗性能,而且多次通过数据库连接,往申诉局库发送一条sql语句,如果有100w条,就要发送100w次
用了foreachpartition算子之后,好处在哪?
1.对于我们写的function函数,就调用一次,一次传入一个partition所有的数据
2.主要创建或获取一个数据库连接就可以了
3.只要向数据库发送一次sql语句和多组参数即可
缺点:可能OOM
四、使用repartition算子解决sparksql并行度低的问题
并行度设置
1.spark.default.parallelism
2.textFile(),传入第二个参数,指定partition数量(少用)
设置的并行度什么时候才会生效?
Sparksql的那个stage的并行度,没法自己指定,sparksql自己会默认根据hive表对应的hdfs文件的block,自动设置spark sql查询所在的那个stage的并行度,自己通过参数指定的并行度,只会在没有spark sql的stage中生效
解决上述spark sql无法设置并行度和task数量的办法是什么呢?
repartition算子重新进行分区
五、使用reduceByKey本地聚合
1.在本地聚合以后,在map端的数据量就变少了,减少磁盘IO,而且可以减少磁盘空间的占用
2.下一个stage,拉取数据的量也就变少了,减少网络的数据传输的性能消耗
3.在reduce端进行数据缓存的内存占用变少了
4.reduce端,要进行聚合的数据量也变少了
spark调优之troubleShooting
一、控制shuffle reduce端缓冲大小以避免OOM
map端的task是不断的输出数据的,数据量可能是很大的,但是,其实reduce端的task,并不是等到map端的task将属于自己的那份数据全部写入磁盘文件之后,再去拉取的。map端写一点数据,reduce端task就会拉取一小部分数据,立即进行后面的集合,算子的应用。
每次reduce能够拉取多少数据,由buffer来决定,因此拉取过来的数据都先放在buffer中,然后采用后面的executor分配的堆内存占比(0.2)hashmap,去进行后续的集合,函数的执行
可能出现什么问题?
默认大小是48M,大多数是,reduce端task一边拉取,一边计算,不一定一直会拉满48m的数据,可能拉个10m就计算掉了。
但有时候,map端的数据量特别大,然后写出的速度特别快,reduce端所有task,拉取的时候,全部达到自己的缓冲的最大极限值,缓冲 48m 全部填满,这个时候加上执行的聚合函数的代码,可能会创建大量的对象,也许内存一下子就撑不住了。
这个时候应该减少reduce端task缓冲的大小,多拉几次,也可以尝试去增加这个reduce端缓冲大小
二、解决JVM GC导致的shuffle拉取文件失败
比如:executor的jvm进程,可能内存不是很够用了,可能就会执行GC,导致executor内,所有的工作线程全部停止,比如blockmanager,基于netty的网络通信,下一个stage的executor,可能是还没有停止掉的。task想要去上一个stage的task所在的executor,去拉取属于自己的数据,结果对方正在GC,就导致拉取半天没有拉到
解决方法:
spark.shuffle.io.maxRetries 拉取数据重试次数
spark.shuffle.io.retryWait 调整到重试间隔时间60s