spark性能优化

spark性能优化点

1. 分配更多的资源

分配更多的资源:
它是性能优化调优的王道,就是增加和分配更多的资源,这对于性能和速度上的提升是显而易见的,
基本上,在一定范围之内,增加资源与性能的提升,是成正比的;写完了一个复杂的spark作业之后,进行性能调
优的时候,首先第一步,就是要来调节最优的资源配置;在这个基础之上,如果说你的spark作业,能够分配的资源达到
了你的能力范围的顶端之后,无法再分配更多的资源了,公司资源有限;那么才是考虑去做后面的这些性能调优的点。
相关问题:
(1)分配哪些资源?
(2)在哪里可以设置这些资源?
(3)剖析为什么分配这些资源之后,性能可以得到提升?

1.1 分配哪些资源

executor‐memory、executor‐cores、driver‐memory

1.2 在哪里可以设置这些资源

在实际的生产环境中,提交spark任务时,使用spark‐submit shell脚本,在里面调整对应的参数。
提交任务的脚本:
spark‐submit \
‐‐master spark://node1:7077 \
‐‐class cn.itcast.WordCount \
‐‐num‐executors 3 \ 配置executor的数量
‐‐driver‐memory 1g \ 配置driver的内存(影响不大)
‐‐executor‐memory 1g \ 配置每一个executor的内存大小
‐‐executor‐cores 3 \ 配置每一个executor的cpu个数
/export/servers/wordcount.jar

1.3 参数调节到多大,算是最大

第一种情况:standalone模式
先计算出公司spark集群上的所有资源 每台节点的内存大小和cpu核数,
比如:一共有20台worker节点,每台节点8g内存,10个cpu。
实际任务在给定资源的时候,可以给20个executor、每个executor的内存8g、每个executor的使用的cpu个数
10。
第二种情况:Yarn
先计算出yarn集群的所有大小,比如一共500g内存,100个cpu;
这个时候可以分配的最大资源,比如给定50个executor、每个executor的内存大小10g,每个executor使用的cpu
个数为2。
使用原则:你能使用的资源有多大,就尽量去调节到最大的大小(executor的数量:几十个到上百个不等;executor的
内存;exector的cpu个数)

1.4 为什么调大资源以后性能可以提升

2. 提高并行度

2.1 Spark的并行度指的是什么

spark作业中,各个stage的task的数量,也就代表了spark作业在各个阶段stage的并行度!
        当分配完所能分配的最大资源了,然后对应资源去调节程序的并行度,如果并行度没有与资源相匹配,那么导致你
分配下去的资源都浪费掉了。同时并行运行,还可以让每个task要处理的数量变少(很简单的原理。合理设置并行度,
可以充分利用集群资源,减少每个task处理数据量,而增加性能加快运行速度。)
      举例说明:
         假如, 现在已经在spark‐submit 脚本里面,给我们的spark作业分配了足够多的资源,比如50个executor ,每
个executor 有10G内存,每个executor有3个cpu core 。 基本已经达到了spark集群或者yarn集群上限。task没有
设置,或者设置的很少,比如就设置了100个task、50个executor、每个executor有3个core ,也就是说
Application 任何一个stage运行的时候,都有总数150个cpu core ,可以并行运行。
         但是你现在只有100个task,平均分配一下,每个executor 分配到2个task,那么同时在运行的task,只有100个
task,每个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数据。 

2.2 如何提高并行度

2.2.1 可以设置task的数量
       至少设置成与spark Application 的总cpu core 数量相同(最理想情况,150个core,分配150task,一起运
行,差不多同一时间运行完毕)官方推荐,task数量,设置成spark Application 总cpu core数量的2~3倍 。
        比如150个cpu core ,基本设置task数量为300~500. 与理想情况不同的,有些task会运行快一点,比如50s就完
了,有些task 可能会慢一点,要一分半才运行完,所以如果你的task数量,刚好设置的跟cpu core 数量相同,可能会
导致资源的浪费。
         因为比如150个task中10个先运行完了,剩余140个还在运行,但是这个时候,就有10个cpu core空闲出来了,导
致浪费。如果设置2~3倍,那么一个task运行完以后,另外一个task马上补上来,尽量让cpu core不要空闲。同时尽量
提升spark运行效率和速度。提升性能。
2.2.2 如何设置task数量来提高并行度 
设置参数spark.defalut.parallelism
默认是没有值的,如果设置了值为10,它会在shuffle的过程才会起作用。
比如 val rdd2 = rdd1.reduceByKey(_+_)
此时rdd2的分区数就是10,rdd1的分区数不受这个参数的影响。
可以通过在构建SparkConf对象的时候设置,例如:
new SparkConf().set("spark.defalut.parallelism","500") 
2.2.3 给RDD重新设置partition的数量 
使用rdd.repartition 来重新分区,该方法会生成一个新的rdd,使其分区数变大。
此时由于一个partition对应一个task,那么对应的task个数越多,通过这种方式也可以提高并行度。
2.2.4 提高sparksql运行的task数量 
通过设置参数 spark.sql.shuffle.partitions=500 默认为200;
可以适当增大,来提高并行度。 比如设置为 spark.sql.shuffle.partitions=500 

3. RDD的重用和持久化

3.1 实际开发遇到的情况说明

 
如上图所示的计算逻辑:
(1)当第一次使用rdd2做相应的算子操作得到rdd3的时候,就会从rdd1开始计算,先读取HDFS上的文件,然后对rdd1
做对应的算子操作得到rdd2,再由rdd2计算之后得到rdd3。同样为了计算得到rdd4,前面的逻辑会被重新计算。
(3)默认情况下多次对一个rdd执行算子操作,去获取不同的rdd,都会对这个rdd及之前的父rdd全部重新计算一次。
这种情况在实际开发代码的时候会经常遇到,但是我们一定要避免一个rdd重复计算多次,否则会导致性能急剧降低。
 
总结:可以把多次使用到的rdd,也就是公共rdd进行持久化,避免后续需要,再次重新计算,提升效率。 
 

3.2 如何对rdd进行持久化 

可以调用rdd的cache或者persist方法。
(1)cache方法默认是把数据持久化到内存中 ,例如:rdd.cache ,其本质还是调用了persist方法
(2)persist方法中有丰富的缓存级别,这些缓存级别都定义在StorageLevel这个object中,可以结合实际的应用场
景合理的设置缓存级别。例如: rdd.persist(StorageLevel.MEMORY_ONLY),这是cache方法的实现。

3.3 rdd持久化的时可以采用序列化 

(1)如果正常将数据持久化在内存中,那么可能会导致内存的占用过大,这样的话,也许会导致OOM内存溢出。
(2)当纯内存无法支撑公共RDD数据完全存放的时候,就优先考虑使用序列化的方式在纯内存中存储。将RDD的每个
partition的数据,序列化成一个字节数组;序列化后,大大减少内存的空间占用。
(3)序列化的方式,唯一的缺点就是,在获取数据的时候,需要反序列化。但是可以减少占用的空间和便于网络传输
(4)如果序列化纯内存方式,还是导致OOM,内存溢出;就只能考虑磁盘的方式,内存+磁盘的普通方式(无序列化)。
(5)为了数据的高可靠性,而且内存充足,可以使用双副本机制,进行持久化
持久化的双副本机制,持久化后的一个副本,因为机器宕机了,副本丢了,就还是得重新计算一次;
持久化的每个数据单元,存储一份副本,放在其他节点上面,从而进行容错;
一个副本丢了,不用重新计算,还可以使用另外一份副本。这种方式,仅仅针对你的内存资源极度充足。
比如: StorageLevel.MEMORY_ONLY_2

3.4 广播变量的使用

3.4.1 场景描述
在实际工作中可能会遇到这样的情况,由于要处理的数据量非常大,这个时候可能会在一个stage中出现大量的
task,比如有1000个task,这些task都需要一份相同的数据来处理业务,这份数据的大小为100M,该数据会拷贝
1000份副本,通过网络传输到各个task中去,给task使用。这里会涉及大量的网络传输开销,同时至少需要的内存
为1000*100M=100G,这个内存开销是非常大的。不必要的内存的消耗和占用,就导致了,你在进行RDD持久化
到内存,也许就没法完全在内存中放下;就只能写入磁盘,最后导致后续的操作在磁盘IO上消耗性能;这对于
spark任务处理来说就是一场灾难。
由于内存开销比较大,task在创建对象的时候,可能会出现堆内存放不下所有对象,就会导致频繁的垃圾回收器的
回收GC。GC的时候一定是会导致工作线程停止,也就是导致Spark暂停工作那么一点时间。频繁GC的话,对
Spark作业的运行的速度会有相当可观的影响。 
3.4.2 广播变量引入 
Spark中分布式执行的代码需要传递到各个executor的task上运行。对于一些只读、固定的数据,每次都需要Driver
广播到各个Task上,这样效率低下。广播变量允许将变量只广播(提前广播)给各个executor。该executor上的各
个task再从所在节点的BlockManager(负责管理某个executor对应的内存和磁盘上的数据)获取变量,而不是从
Driver获取变量,从而提升了效率。 

广播变量,初始的时候,就在Drvier上有一份副本。通过在Driver把共享数据转换成广播变量。
task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,尝
试获取变量副本;如果本地没有,那么就从Driver远程拉取广播变量副本,并保存在本地的BlockManager中;
此后这个executor上的task,都会直接使用本地的BlockManager中的副本。那么这个时候所有该executor中的
task都会使用这个广播变量的副本。也就是说一个executor只需要在第一个task启动时,获得一份广播变量数据,之后
的task都从本节点的BlockManager中获取相关数据。
executor的BlockManager除了从driver上拉取,也可能从其他节点的BlockManager上拉取变量副本,网络距离
越近越好。 
 
3.4.3 使用广播变量后的性能分析
比如一个任务需要50个executor,1000个task,共享数据为100M。
(1)在不使用广播变量的情况下,1000个task,就需要该共享数据的1000个副本,也就是说有1000份数需要大量的网络
传输和内存开销存储。耗费的内存大小1000*100=100G.
(2)使用了广播变量后,50个executor就只需要50个副本数据,而且不一定都是从Driver传输到每个节点,还可能是就
近从最近的节点的executor的blockmanager上拉取广播变量副本,网络传输速度大大增加;内存开销 50*100M=5G
总结:
不使用广播变量的内存开销为100G,使用后的内存开销5G,这里就相差了20倍左右的网络传输性能损耗和内存开
销,使用广播变量后对于性能的提升和影响,还是很可观的。
广播变量的使用不一定会对性能产生决定性的作用。比如运行30分钟的spark作业,可能做了广播变量以后,速度
快了2分钟,或者5分钟。但是一点一滴的调优,积少成多。最后还是会有效果的。
3.4.4 如何使用广播变量 
例如:
(1) 通过sparkContext的broadcast方法把数据转换成广播变量,类型为Broadcast,
val broadcastArray: Broadcast[Array[Int]] = sc.broadcast(Array(1,2,3,4,5,6))
(2) 然后executor上的BlockManager就可以拉取该广播变量的副本获取具体的数据。
获取广播变量中的值可以通过调用其value方法
val array: Array[Int] = broadcastArray.value 

4. 使用Kryo序列化

4.1 spark序列化介绍 

Spark在进行任务计算的时候,会涉及到数据跨进程的网络传输、数据的持久化,这个时候就需要对数据进行序列
化。Spark默认采用Java的序列化器。默认java序列化的优缺点如下:
其好处:
处理起来方便,不需要我们手动做其他操作,只是在使用一个对象和变量的时候,需要实现Serializble接口。
其缺点:
默认的序列化机制的效率不高,序列化的速度比较慢;序列化以后的数据,占用的内存空间相对还是比较大。
Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大
概是Java序列化机制的1/10。所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减
少。

4.2 Kryo序列化启用后生效的地方 

Kryo序列化机制,一旦启用以后,会生效的几个地方:
(1)算子函数中使用到的外部变量
算子中的外部变量可能来着与driver需要涉及到网络传输,就需要用到序列化。
最终可以优化网络传输的性能,优化集群中内存的占用和消耗
(2)持久化RDD时进行序列化,StorageLevel.MEMORY_ONLY_SER
将rdd持久化时,对应的存储级别里,需要用到序列化。
最终可以优化内存的占用和消耗;持久化RDD占用的内存越少,task执行的时候,创建的对象,就不至于频繁
的占满内存,频繁发生GC。
(3) 产生shuffle的地方,也就是宽依赖
下游的stage中的task,拉取上游stage中的task产生的结果数据,跨网络传输,需要用到序列化。
最终可以优化网络传输的性能

4.3 如何开启Kryo序列化机制 

(1)在构建sparkConf的时候设置相关参数
new SparkConf().set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
 
Kryo之所以没有被作为默认的序列化类库的原因,主要是因为Kryo要求如果要达到它的最佳性能的话,那么就一定
要注册你自定义的类(比如,你的算子函数中使用到了外部自定义类型的对象变量,这时,就要求必须注册你的类,否
则Kryo达不到最佳性能)。
Kryo也不支持所有实现了 java.io.Serializable 接口的类型,它需要你在程序中 register 需要序列化的类
型,以得到最佳性能。
 
(2)注册需要通过Kryo序列化的一些自定义类
new SparkConf().registerKryoClasses(Array(classOf[Student]))
该方法需要一个Class类型的数组,表示可以一下子注册多个需要实现Kryo序列化的类。 
 

5. 使用fastutil优化数据格式

5.1 fastutil介绍

fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型
的map、set、list和queue;
fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来替代自己平时使用的JDK的
原生的Map、List、Set。

5.2 fastutil好处

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

5.3 Spark中应用fastutil的场景

5.3.1 算子函数使用了外部变量
(1)你可以使用Broadcast广播变量优化;
(2)可以使用Kryo序列化类库,提升序列化性能和效率;
(3)如果外部变量是某种比较大的集合,那么可以考虑使用fastutil改写外部变量;
首先从源头上就减少内存的占用(fastutil),通过广播变量进一步减少内存占用,再通过Kryo序列化类库进一步减少内
存占用。
5.3.2 算子函数里使用了比较大的集合Map/List
在你的算子函数里,也就是task要执行的计算逻辑里面,如果有逻辑中,出现,要创建比较大的Map、List等集合,
可能会占用较大的内存空间,而且可能涉及到消耗性能的遍历、存取等集合操作;
那么此时,可以考虑将这些集合类型使用fastutil类库重写,
使用了fastutil集合类以后,就可以在一定程度上,减少task创建出来的集合类型的内存占用。
避免executor内存频繁占满,频繁唤起GC,导致性能下降。
5.3.3 关于fastutil调优的说明
fastutil其实没有你想象中的那么强大,也不会跟官网上说的效果那么一鸣惊人。广播变量、Kryo序列化类库、
fastutil,都是之前所说的,对于性能来说,类似于一种调味品,烤鸡,本来就很好吃了,然后加了一点特质的孜然麻辣
粉调料,就更加好吃了一点。
分配资源、并行度、RDD架构与持久化,这三个就是烤鸡;
broadcast、kryo、fastutil,类似于调料。
比如说,你的spark作业,经过之前一些调优以后,大概30分钟运行完,
现在加上broadcast、kryo、fastutil,也许就是优化到29分钟运行完、或者更好一点,也许就是28分钟、25分钟。
真正有意义的就是后面要学习的shuffle调优,可能优化之后只需要15分钟;
还有把groupByKey用reduceByKey改写,执行本地聚合,也许10分钟;
甚至可以向公司申请更多的资源,扩大整个集群的计算能力,最后可能到达5分钟就完成任务了。
5.3.4 fastutil的使用
第一步:在pom.xml中引用fastutil的包
<dependency>
<groupId>fastutil</groupId>
<artifactId>fastutil</artifactId>
<version>5.0.9</version>
</dependency>
第二步:平时使用List (Integer)的替换成IntList即可。
List<Integer>的list对应的到fastutil就是IntList类型
使用说明:
基本都是类似于IntList的格式,前缀就是集合的元素类型;
特殊的就是Map,Int2IntMap,代表了key‐value映射的元素类型。

6. 调节数据本地化等待时长

Spark在Driver上对Application的每一个stage的task进行分配之前,都会计算出每个task要计算的是哪个分片数
据,RDD的某个partition;Spark的task分配算法,优先会希望每个task正好分配到它要计算的数据所在的节点,
这样的话就不用在网络间传输数据;
但是通常来说,有时事与愿违,可能task没有机会分配到它的数据所在的节点,为什么呢,可能那个节点的计算资
源和计算能力都满了;所以这种时候,通常来说,Spark会等待一段时间,默认情况下是3秒(不是绝对的,还有很
多种情况,对不同的本地化级别,都会去等待),到最后实在是等待不了了,就会选择一个比较差的本地化级别,
比如说将task分配到距离要计算的数据所在节点比较近的一个节点,然后进行计算。

6.1 本地化级别

(1)PROCESS_LOCAL:进程本地化
         代码和数据在同一个进程中,也就是在同一个executor中;计算数据的task由executor执行,数据在executor的
BlockManager中;性能最好
(2)NODE_LOCAL:节点本地化
        代码和数据在同一个节点中;比如说数据作为一个HDFS block块,就在节点上,而task在节点上某个executor中
运行;或者是数据和task在一个节点上的不同executor中;数据需要在进程间进行传输;性能其次
(3)RACK_LOCAL:机架本地化
        数据和task在一个机架的两个节点上;数据需要通过网络在节点之间进行传输; 性能比较差
(4) ANY:无限制
        数据和task可能在集群中的任何地方,而且不在一个机架中;性能最差

6.2 数据本地化等待时长

spark.locality.wait,默认是3s
首先采用最佳的方式,等待3s后降级,还是不行,继续降级...,最后还是不行,只能够采用最差的。

6.3 如何调节参数并且测试

修改spark.locality.wait参数,默认是3s,可以增加
下面是每个数据本地化级别的等待时间,默认都是跟spark.locality.wait时间相同,
默认都是3s(可查看spark官网对应参数说明,如下图所示)
spark.locality.wait.node
spark.locality.wait.process
spark.locality.wait.rack
 
spark.locality.wait 3s 在放弃并在较少本地节点上启动数据本地任务之前等待多长时间才能启动数据本地任务。相同的等待将用于逐步执行多个位置级别(进程本地,节点本地,机架本地,然后是任何)。也可以通过设置spark.locality.wait.node等来自定义每个级别的等待时间。如果您的任务很长并且看到不良位置,则应该增加此设置,但默认情况通常很有效。
spark.locality.wait.node spark.locality.wait 自定义位置等待节点位置。例如,您可以将此值设置为0以跳过节点位置并立即搜索机架位置(如果您的群集有机架信息)。
spark.locality.wait.process spark.locality.wait 自定义位置等待进程位置。这会影响尝试访问特定执行程序进程中的缓存数据的任务。
spark.locality.wait.rack spark.locality.wait 自定义本地等待机架位置。
 
 
 
 
 
 
 
 
 
 
 
 
在代码中设置:
new SparkConf().set("spark.locality.wait","10")
然后把程序提交到spark集群中运行,注意观察日志,spark作业的运行日志,推荐大家在测试的时候,先用client模
式,在本地就直接可以看到比较全的日志。
日志里面会显示,starting task .... PROCESS LOCAL、NODE LOCAL.....
例如:
Starting task 0.0 in stage 1.0 (TID 2, 192.168.200.102, partition 0, NODE_LOCAL, 5254 bytes)
观察大部分task的数据本地化级别
如果大多都是PROCESS_LOCAL,那就不用调节了。如果是发现,好多的级别都是NODE_LOCAL、ANY,那么最好就去调节
一下数据本地化的等待时长。应该是要反复调节,每次调节完以后,再来运行,观察日志
看看大部分的task的本地化级别有没有提升;看看整个spark作业的运行时间有没有缩短。
注意注意:
在调节参数、运行任务的时候,别本末倒置,本地化级别倒是提升了, 但是因为大量的等待时长,spark作业的运行时
间反而增加了,那就还是不要调节了。 

7. 降低cache操作的内存占比

7.1 为什么需要jvm调优 

spark的scala代码调用了很多java api。scala也是运行在java虚拟机中的。spark是运行在java虚拟机中的。
java虚拟机可能会产生什么样的问题:内存不足??!!
我们的RDD的缓存、task运行定义的算子函数,可能会创建很多对象。都可能会占用大量内存,没搞好的话,可能导致
JVM出问题。
JVM调优(Java虚拟机):JVM相关的参数,通常情况下,如果你的硬件配置、基础的JVM的配置,都ok的话,JVM通常不
会造成太严重的性能问题;反而更多的是在troubleshooting(故障排除)中,JVM占了很重要的地位;JVM造成线上的
spark作业的运行报错,甚至失败(比如OOM)。

jvm堆内存模型图

 
有通过new创建的对象的内存都在堆中分配,其大小可以通过‐Xmx和‐Xms来控制。
堆被划分为年轻代和老年代,年轻代又被进一步划分为Eden和Survivor区
jvm堆空间内存分配,在默认情况下:
年轻代 :用于存放新产生的对象。我们在spark task执行算子函数操作的时候,可能会创建很多对象,这些对象都
是要放入JVM年轻代中的,它占用堆中三分之一的堆内存空间。
里面又分为三个区域
eden区: 8/10 的年轻代空间
survivor0 : 1/10 的年轻代空间
survivor1 : 1/10 的年轻代空间
老年代 : 用于存放被长期引用的对象,它占用堆中三分之一的堆内存空间。

7.2 jvm工作原理阐述 

每一次放对象的时候,都是放入eden区域,和其中一个survivor区域;另外一个survivor区域是空闲的。
当eden区域和一个survivor区域放满了以后(spark运行过程中,产生的对象实在太多了),就会触发minor gc,小型
垃圾回收。把不再使用的对象,从内存中清空,给后面新创建的对象腾出来点儿地方。
清理掉了不再使用的对象之后,那么也会将存活下来的对象(还要继续使用的),放入之前空闲的那一个
survivor区域中。这里可能会出现一个问题。默认eden、survior0和survivor1的内存占比是8:1:1。问题是,如果存
活下来的对象是1.5,一个survivor区域放不下,将多余的对象,直接放入老年代了。
如果你的JVM内存不够大的话,可能导致频繁的年轻代内存满溢,频繁的进行minor gc(清理Eden区和
Survivor区)。频繁的minor 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)、频繁gc会严重影响spark的性能和运行的速度。 

7.3 降低cache操作的内存占比

spark中,堆内存又被划分成了两块儿,一块儿是专门用来给RDD的cache、persist操作进行RDD数据缓存用
的;
另外一块儿,就是我们刚才所说的,用来给spark算子函数的运行使用的,存放函数中自己创建的对象。
       默认情况下,给RDD cache操作的内存占比是0.6(spark.storage.memoryFraction=0.6),60%的内存都给
了cache操作了。如果某些情况下,cache不是那么的紧张,在task算子函数中创建的对象过多,然后内存又不太大,导
致了频繁的minor gc,甚至频繁full gc,导致spark频繁的停止工作。性能影响会很大。、
       针对上述这种情况,大家可以在之前我们讲过的那个spark ui。yarn去运行的话,那么就通过yarn的界面,去
查看你的spark作业的运行统计,很简单大家一层一层点击进去就好。可以看到每个stage的运行情况,包括每个task的
运行时间、gc时间等等。如果发现gc太频繁,时间太长。此时就可以适当调节这个比例。
       降低cache操作的内存占比,大不了用persist操作,选择将一部分缓存的RDD数据写入磁盘,或者序列化方式,
配合Kryo序列化类,减少RDD缓存的内存占用;降低cache操作内存占比;此时对应的算子函数的内存占比就提升了。这个
时候,可能就可以减少minor gc的频率,同时减少full gc的频率。对性能的提升是有一定的帮助的。
总之一句话,让task执行算子函数时,有更多的内存可以使用。 

7.4 降低cache操作的内存占比代码实现

cache操作的内存占比为堆内存的0.6 也就是百分之60,可以适当调节,降低该值,
修改spark.storage.memoryFraction参数
可以设置为0.5‐‐‐>0.4‐‐‐‐‐‐>0.3
例如:
new SparkConf().set("spark.storage.memoryFraction","0.4")
把cache操作的内存占比修改为堆内存的百分之40,让堆内存可以容纳更多的对象,减少gc的频率,提高spark任务运行
的速度和性能。
 
posted @ 2020-03-07 11:13  mls12  阅读(98)  评论(0)    收藏  举报