笨鸟的平凡之路-Spark之开发调优

1.避免创建重复的RDD.
一个RDD生成后,之后若再对该RDD重新操作时,推荐不要再重新生成了.

2.尽可能复用同一个RDD
如果RDD1是<key,value>类型,RDD2是类型,并且RDD1中的value是由RDD2中的value而来,那么建议只使用RDD1,不建议利用RDD1再生成RDD2.但是这样RDD1还是被计算了两次,所以一般结合3-持久化的方式进行进一步调优.

3.对多次使用的RDD进行持久化
持久化的好处是一个RDD重复使用时,不会将这个RDD从源头开始多次计算,而是只计算一次之后,把这个结果根据自定义的存储策略存储起来,供下次重复使用,而不是重复计算.
持久化的方法一般有两个cache() 和 persist()
cache是基于内存而来,persist是可以自定义存储级别而来.

在这里插入图片描述
如何选择一种最合适的持久化策略

默认情况下,性能最高的当然是MEMORY_ONLY,但前提是你的内存必须足够足够大,可以绰绰有余地存放下整个RDD的所有数据。因为不进行序列化与反序列化操作,就避免了这部分的性能开销;对这个RDD的后续算子操作,都是基于纯内存中的数据的操作,不需要从磁盘文件中读取数据,性能也很高;而且不需要复制一份数据副本,并远程传送到其他节点上。但是这里必须要注意的是,在实际的生产环境中,恐怕能够直接用这种策略的场景还是有限的,如果RDD中数据比较多时(比如几十亿),直接用这种持久化级别,会导致JVM的OOM内存溢出异常。

如果使用MEMORY_ONLY级别时发生了内存溢出,那么建议尝试使用MEMORY_ONLY_SER级别。该级别会将RDD数据序列化后再保存在内存中,此时每个partition仅仅是一个字节数组而已,大大减少了对象数量,并降低了内存占用。这种级别比MEMORY_ONLY多出来的性能开销,主要就是序列化与反序列化的开销。但是后续算子可以基于纯内存进行操作,因此性能总体还是比较高的。此外,可能发生的问题同上,如果RDD中的数据量过多的话,还是可能会导致OOM内存溢出的异常。

如果纯内存的级别都无法使用,那么建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK策略。因为既然到了这一步,就说明RDD的数据量很大,内存无法完全放下。序列化后的数据比较少,可以节省内存和磁盘的空间开销。同时该策略会优先尽量尝试将数据缓存在内存中,内存缓存不下才会写入磁盘。

通常不建议使用DISK_ONLY和后缀为_2的级别:因为完全基于磁盘文件进行数据的读写,会导致性能急剧降低,有时还不如重新计算一次所有RDD。后缀为_2的级别,必须将所有数据都复制一份副本,并发送到其他节点上,数据复制以及网络传输会导致较大的性能开销,除非是要求作业的高可用性,否则不建议使用。

4.尽量避免使用shuffle类的算子.
使用shuffle的算子,其过程大致意思就是将不同节点的数据对于相同的key拉取到同一个节点上进行计算等操作,有可能一个节点上相同的key数量过大,内存不够存放,然后溢写到了磁盘中去了,并且可能发生大量的网络数据传输读写等IO操作,这样效率就会低下.故不推荐多次使用shuffle类算子.(一般使用map类的算子代替使用)

5.使用map-side预聚合shuffle操作
map-side的预聚合如同MapReduce在map端进行combiner.典型的例子就是reduceByKey和groupBykey.reduceByKey是会现在节点内进行一次预聚合,然后将多个节点聚合后的数据再拉到某一台节点上执行计算操作.groupBykey是不经过预聚合,而是将多个节点上所有的数据拉到某一台节点上计算操作,这样全量数据的传输,导致了效率的低下.

6.使用高性能的算子

6.1 使用reduceByKey/aggregateByKey替代groupByKey

因为reduceByKey/aggregateByKey会在map-side预聚合

6.2 使用mapPartitions 替代普通map

mapPartitions与map的区别是:map处理RDD中每一个元素,而mapPartitions是处理RDD的每个分区迭代器里的操作.比如,如果通过jdbc向数据库中写入数据,使用map的话,会为RDD中每一个元素都创建一个链接去写数据,而mapPartitions的方法的话只会为RDD的每一个Partition去创建一个链接去写数据,这样效率会很高.
但也不是说所有的map都换成mapPartitions都好,因为mapPartitions会因为内存的欠缺导致OOM,而map不会有这样的情况发生.

6.3 使用foreachPartitions替代foreach

和mapPartitions一样,foreachPartitions也是基于Partition而来的.在写入数据库的操作时,使用foreach是一条一条的写入,而foreachPartitions方式是批量插入的.实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。

6.4 使用filter之后进行coalesce操作

在filter之前,每个分区的数据量分布的是比较均匀的,task处理的数据量就大致相等的,但是在filter之后,每个分区里的数据发生了改变,每个分区Partition的数据量可能是不一致的了,name相同的task处理不同数据量的分区时,是很有可能出现数据倾斜的,并且相同的task数量处理不同的数据量,会造成少数据量的分区是在浪费task的资源.因此,在用filter算子之后再用一次coalesce重新分区,减少partitions的数量,将filter之后的少数据量重新分区到少的分区里,这样使用更少的task去处理所有的Partition的数据,效率是有所提高的.

6.5 使用repartitionAndSortWithinPartitions替代repartition与sort类操作

Spark官方推荐如果使用reparation之后还有再进行Sort操作时,建议使用reparationAndSortWithinPartitions算子.因为该算子可以重分区的shuffle时同时进行sort操作,这样比先reparation再sort的效率要高.

PS:
reparation和coalesce的区别,reparation底层是调用coalesce方法的,只不过reparation调用的是coalesce shuffle为true的情况,默认情况下,coalesce的shuffle为false的.我们在将少分区重分区多分区时,是父RDD对子RDD发生的是一对多的情况,发生的是宽依赖,所以要发生shuffle,所以可以使用reparation或者coalesce(nums,true)算子.

reparation和coalesce的区别可以参考: https://blog.csdn.net/xianpanjia4616/article/details/82053196

7.使用广播变量-broadcast
有时候我们在使用外部变量时,运行流程一般是将外部变量的副本发送到Executor中需要执行的task中(一个Executor中有很多个task).试想,如果这个外部变量超过100MB,甚至更大时,那么该变量的副本在传输过程中占用了大量的性能,并且各节点中的Executor包含了很多份外部变量,占用了很大的内存,导致了频繁的GC,极大的影响了性能.
broadcast的作用是保证每个Executor中只保存一份外部变量的副本,我们称之为共享变量.使用广播变量之后,一个Executor中的所有task都可以使用这一份共享变量.这样减少了外部变量的多次传输,减少了Executor中外部变量的份数,减少了占用的内存.

8.使用Kryo优化序列化性能
在Spark中,主要有三个地方涉及到了序列化:

8.1 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输(见“原则七:广播大变量”中的讲解)。
8.1 将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。
8.3 使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

对于这三种出现序列化的地方,我们都可以通过使用Kryo序列化类库,来优化序列化和反序列化的性能。Spark默认使用的是Java的序列化机制,也就是ObjectOutputStream/ObjectInputStream API来进行序列化和反序列化。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。

9.优化数据类型
Java中,有三种类型比较耗费内存:

9.1 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
9.2 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
9.3 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。

因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。

但是在笔者的编码实践中发现,要做到该原则其实并不容易。因为我们同时要考虑到代码的可维护性,如果一个代码中,完全没有任何对象抽象,全部是字符串拼接的方式,那么对于后续的代码维护和修改,无疑是一场巨大的灾难。同理,如果所有操作都基于数组实现,而不使用HashMap、LinkedList等集合类型,那么对于我们的编码难度以及代码可维护性,也是一个极大的挑战。因此笔者建议,在可能以及合适的情况下,使用占用内存较少的数据结构,但是前提是要保证代码的可维护性。

10.Data 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.locality.wait,默认是3s

Spark在Driver上,对Application的每一个stage的task,进行分配之前,都会计算出每个task要计算的是哪个分片数据,RDD的某个partition;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;可以改成6s,10s

默认情况下,下面3个的等待时长,都是跟上面那个是一样的,都是3s

spark.locality.wait.process//建议60s
spark.locality.wait.node//建议30s
spark.locality.wait.rack//建议20s
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值