spark如何防止内存溢出_spark开发十大原则

v2-e686b6b7f4f816d357945ce2818bc3d3_1440w.jpg?source=172ae18b

前言

本文主要阐述的是在开发spark的时候遵循十大开发原则,这些原则都是我们的前辈辛辛苦苦的总结而来,但是也不是凭空创造的,是有依据可循的,就在官网上面,让我们来认识一下吧。

网址:http://spark.apache.org/docs/2.2.3/tuning.html

通过上面的网址我们可以找到所有优化spark的内容,记下来让我开始阐述一下这十大开发原则吧。

原则一:避免创建重复的RDD

一般而言,我们加载数据在hdfs或者本地或者任何地方
比如下面的语句:
val rdd1 = sc.textFile("hdfs://node1:9000/hello.txt")
上面的语句我们从hdfs加载了一份hello.txt文件,这是正确的,毫无疑问,但是有时候当我们的代码达到一定
数量级别之后,比如超过500行或者一千行,这个时候我们就可能忘记我们已经加载过这个数据源了,很大的程度
上我们还会写下面的代码
val rdd2  = sc.textFile("hdfs://node1:9000/hello.txt")
所以你发现了没?我们对同一份数据进行了两次加载,读者读到这里可能会笑,怎么可能,我不会犯这样的错误,
或者读者根本没有意识到这种有什么问题?
很简单,在代码多的时候这种问题出现的概率很大,而且同一份数据加载两次就意味着我们第二次执行了一次耗时
的操作,而且还没有啥用处。
所以,我们再写代码的时候一定要注意这个问题,当数据量特别大的时候,而且是越大的时候,这个问题越严重,要
仔细检查我们的代码,避免重复加载同一份数据。

原则二:尽可能的复用同一个RDD

比如我们这里创建了一个rdd操作
val rdd1 = xxxxx
总之我们通过一种方式获取了一种rdd
比如rdd1的数据格式现在为(key1,value1),这个时候我们想要做一种操作,我们指向用rdd1中的value1
所以我们很有可能的操作是下面这种
val rdd2 = rdd1.map(x=>(x._2))
于是通过上面的操作我们又获得了一个新的rdd,于是接下来我们继续用rdd2操作
rdd2.map(x=>x*2)
到这里,读者你发现了问题了没?首先rdd2.map(x=>x*2)这个操作的执行步骤当在运行的时候还依旧是先去执行
rdd1,然后创建rdd2,然后把里面的每个元素乘以2,有没有觉得多余呢?是的,很多余。
那么我们正确的做法就是直接用rdd1来操作即可

比如这个样子:
val rdd1 = xxxx
rdd1.map(x=>x._2 * 2))
以上操作,我们并没有创建新的rdd,但是我们做到了相同的效果

所以,我们再写rdd的时候尽可能的用同一个rdd,避免创建更多的rdd,以减少开销,减少算子的执行次数

原则三:对多次使用的RDD进行持久化

比如下面的场景
val rdd1 = sc.textFile("hdfs://node1:9000/hello.txt")
rdd1.map(..)
rdd1.reduce(..)
我们观察一下,发现第二个rdd1.reduce实际上它的执行过程,依旧是从rdd1的加载开始执行,而rdd1.map也是从
rdd1的加载数据开始执行,发现没?无论我们愿意还是不愿意,我们都将rdd1的过程执行了两次
那么遇到这种情况,我们就可以将rdd1进行持久化,这样我们再次执行rdd1.reduce方法的时候实际上我们是从内存
中直接加载的rdd1,并未重新执行rdd1的加载过程。
正确代码如下:
val rdd1 = sc.textFile("hdfs://node1:9000/hello.txt").cache()
rdd1.map(..).foreach()
rdd1.reduce(..)
是的,接下来就要说明了一个问题了,cache()这个操作呢,需要action操作才能进行持久化。
那么都有哪些action 操作呢?
我来列举一下 
collect()	
first()	
take(n)	
takeSample(withReplacement, num, [seed])	
takeOrdered(n, [ordering])	
saveAsTextFile(path)	
saveAsSequenceFile(path)
saveAsObjectFile(path)
countByKey()	
foreach(func)	
以上就是我们常用的action操作。
接下来我们来聊一聊持久化除了cache()这种的其他持久化方法
我们还可以使用persist方法指定其他形式的操作

我们先来一个代码进行展示一下如何设置 
val rdd1 = sc.txttFile("hdfs://node1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
是的,就这样子设置
然后再来让我们认识一下持久化的级别都有哪些?

持久化级别列表:

v2-4268d3777c1f10c76cd19f238ef2c499_b.jpg
接下来让我们聊一聊该如何设置这些级别,也就是如何选择一种最合适的的持久化策略。

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

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

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

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

原则四:尽量避免使用shuffle类算子

如果有可能的情况下,我们尽量避免使用shuffle类的算子,我们都是在spark的运行过程中,shuffle算子会从多个节点上
将key拉到同一个节点上,进行聚合或者join操作,在这个过程中,会产生大量的网络磁盘IO,所以我们要尽量避免,以减少磁盘IO的
开销。
举个例子:

val rdd3= rdd1.join(rdd2)
在这个过程中会把其他节点上的key通过网络拉到一个节点上,这是错误的。

遇到这种问题,我们就可以使用广播变量的方式
比如:
val rdd2Data = rdd2.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)

原则五:使用map-side预聚合的shuffle操作

 当然了,在原则四的基础上,我们可能有时候根本无法避免使用shuffle操作,那么这个时候我们就使用预聚合的shuffle操作算子
比如我们优先使用reduceBykey,而不是使用groupBykey算子

怎么理解呢?我们要从reduceBykey和gropBykey的执行过程来说明这个过程。

我们先说groupBykey操作,它是将其他节点上的数据先传输到reduce端,然后进行聚合的操作
然后是reduceBykey是现在Map端进行先聚合,然后再传输到reduce端
读者发现了没?
groupBykey是将全量的数据进行传输,也就是原始数据进行传输
而reduceBykey呢?他是将聚合后的数据传输,这样子实际上经过了聚合之后,数据量已经缩小了很大,极大的减少了网络的传输IO
通过这样方式,我们在无法避免使用shuffle操作算子的情况下,我们使用了一种更优化的方法来执行。

原则六:使用高性能的算子

 在原则五的基础上,我们总结了一些算子是比较高性能的,我们优先选择这些高性能的算子操作
第一个就是优先使用reduceBykey,而不是使用groupBYkey
第二个使用MapPartitions替代普通的map操作
我们知道map是对数据的每一条进行处理,比如我们有这么一个场景,我们需要把数据写入到mysql中,如果我们使用map操作,我们则需要
一条一条的插入,这会产生大量的数据库链接操作,但是使用MapPartitons的时候,我们可以一个分区一个分区的进行插入mysql,操作
量一下子就小了,但是也需要注意点的就是,因为MapPartions操作是进行分区操作,所以会产生内存溢出的问题,所以,我们在我们内存
够用的时候使用MapPartitions更优于使用Map
第三个使用foreach Partitions代替foreache
这个其实和第二个及其相似,也就是我们能够在批量处理的时候就尽量使用批量处理的函数,而不是使用单个出来的函数

第四个使用filter之后进行coalesce操作
这里要说明一下coalesce和repartition操作都是进行分区操作
我们在使用filter通常是过滤操作,于是数据量就减少了,但是分区这个分区并没有减少,所以,我们这里减少分区,则会优化一定的性能
一般建议,使用coalesce来减少分区,使用repartition来增加分区

第五个使用repartitionAndSortWithinPartitions替代repartition与sort类操作
因为这两个操作都是同样的操作,但是官方建立使用前者更好

原则六:广播大变量

比如我们这里有一个场景

val list1 = ... 
rdd1.map(x=>x.list1)

假设上面的list1的数量级别为100M或者更大比如1个G,那么如果按照上面的代码来看的话,因为list1是在driver端来形成的
当每个Executor中的task要使用的时候,就需要把list1中的数据传输到每一个task中,我们知道,对于一个Executor来说
它的内部可能被分配多个task,于是乎,每一个task需要一份数据,那么这个数据就被传输了和task数量级别的数据
比如一个Exector中有3个task,而list1中的数据为1个G,这个时候,我们就要传输3个G的大小,到Exector中,数量增大了很多
尤其是在内存不是那么大的情况下,这个内存溢出的可能性非常大。

所以针对这种情况,我们使用广播变量就会减少数据传输
比如下面这种写法:
val list1 = ...
val listBradcast = sc.broadcast(list1)
rdd1.map(x=>x.list1.value)

在这种情况下,list1这个数据只会传输到Executor中保留一份,其他的所有task共享这一份数据,于是就跟task的个数无关,
原先传输3个G的情况下, 这个时候只会传输1个G。
数据量的减少就会减少网络传输,就会增加算子的执行时间。

原则八:使用Kryo优化序列性能

上面我们我们可以使用广播变量将数据给广播给Executor,以减少数据量的传输,但是实际上,spark默认就是
将数据进行序列化,那么默认情况下spark使用的是Java的序列化机制,如果这个时候一种更好的序列化方式岂不是更好吗?
是的,在spark的官方文档上,也就是我前言中写到的官网的文档地址中,官方建议是用Kryo这种序列化的算法更好。

那么怎么设置这种呢?

val conf = new SparkConf().setMaster(..).setAppName(..)

zconf.set("spark.serializer", "org.apache.spark.serializer.KryoSerizlizer")  # 这里设置

# 在某种情况下比如我们上面的注册自定义的,则可以这样设置

conf.registerKryoClasses(Array(classOf[MyClass], classOf[Myclass2]))

建议:在写spark代码的时候就直接先设置上,管他用不用

原则九:优化数据结构

 这个规则是官网给出的建议,但是呢,这个比较扯淡,看看就行了

1.能用json字符串的不要用对象表示,因为对象头额外占用16个字节,多个对象就会占用x乘以16个字节,而字符串始终占用40个字节
2.能不用字符串就用不用字符串,因为字符串占用40个字节,比如 能用 数字1 就不用 字符串 “1“
3.尽量用数组代码集合类型
4.上面的嘛看看就行了,你说不用对象,那么面向对象的思想何在,代码的可读性都没有了,所以看一看注意一下即可

原则十:尽可能的数据本地化

 我们先来认识一下进程化级别:
1.PROCESS_LOCAl :进程本地化
代码和数据在同一个进程中,也就是同一个Executor中,计算数据的task和数据在同一个Executor中,这样子就不用从其他节点来
传输数据到task中
2.NODE_LOCAL: 节点本地化
也就是在做不到计算数据和task在同一个进程的情况下,我们考虑计算数据和task在同一个节点上,比如同一个服务器上,这样子也可以
避免数据从其他节点上传输过来,也减少了网络传输
3.NO_PREF:对于task来说,数据从哪里获取都一样,没有好坏之分
4.RACK_LOCAL:机架本地化
在我们做不到进程本地化和节点本地化的时候,我们可以将task和计算数据进行在同一个机架上,这样子也可以减少不同机架中的数据传输
5.ANY
上面的都做不到,那么只能任何地方都可以了,这种性能最差

那么我们知道了上面的级别之后,我们该如何调优呢?

在spark中,上面都是等待3秒不行,就去换到下一个级别
所以我们可以设置的多一些
spark.locality.wait = 3s
 spark.locality.wait.process 30s 我们让他等待30s
spark.locality.wait.node 30s 等待30s

总结

以上就是开发spark的十大开发原则,关注这些会让你的代码更优化,更有优化效率,另外,点击关注,后面我们写

如何spark内存模型的调优,知乎会推送给您的,在您点击关注之后。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值