spark性能优化

一、WHY?

    spark 应用程序虽然已经有代码生成器,执行优化器等内置工具,能让你的代码在执行时更快,但是它只是一个工具、框架,在这里需要你了解spark工作的原理,知道如何调节spark的内部参数,以达到性能最优。

在做 spark 应用程序的优化的时候,从下面几个点出发就够了:

  • 为什么:因为你的资源有限,因为你的应用上生产环境了会有很多不稳定的因素,在上生产前做好优化和测试是唯一一个降低不稳定因素影响的办法;
  • 怎么做:web ui + log 是做优化的倚天剑和屠龙刀,能掌握好这两点就可以了;
  • 何时做:应用开发成熟时,满足业务要求时,就可以根据需求和时间安排开始做了;
  • 做什么:一般来说,spark 应用程序 80% 的优化,都是集中在三个地方:内存,磁盘io,网络io。再细点说,就是 driver,executor 的内存,shuffle 的设置,文件系统的配置,集群的搭建,集群和文件系统的搭建[e.g 尽量让文件系统和集群都在一个局域网内,网络更快;如果可以,可以让 driver 和 集群也在一个局域网内,因为有时候需要从 worker 返回数据到 driver]
  • 备注:千万不要一心想着优化都从程序本身入手,虽然大多数时候都是程序自己的原因,但在入手检查程序之前最好先确认所有的 worker 机器情况都正常哦。比如说机器负载,网络情况

二、HOW

1. 分配更多的资源 

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

2. 提高并行度

1)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)、如何提高并行度

可以设置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运行效率和速度。提升性能。

如何设置task数量来提高并行度

设置参数spark.defalut.parallelism 默认是没有值的,如果设置了值为10,它会在shuffle的过程才会起作用。
比如 val rdd2 = rdd1.reduceByKey(_+_) 此时rdd2的分区数就是10,rdd1的分区数不受这个参数的影响。
可以通过在构建SparkConf对象的时候设置,例如: new SparkConf().set("spark.defalut.parallelism","500")

给RDD重新设置partition的数量

使用rdd.repartition 来重新分区,该方法会生成一个新的rdd,使其分区数变大。 此时由于一个partition对应一个task,那么对应的task个数越多,通过这种方式也可以提高并行度。

提高sparksql运行的task数量 

通过设置参数 spark.sql.shuffle.partitions=500 默认为200; 可以适当增大,来提高并行度。 比如设置为 spark.sql.shuffle.partitions=500

3. RDD的重用和持久化

 

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

如何对rdd进行持久化 

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

4. 使用Kryo序列化

1)优点:

  Spark支持使用Kryo序列化机制。Kryo序列化机制,比默认的Java序列化机制,速度要快,序列化后的数据要更小,大概是Java序列化机制的1/10。所以Kryo序列化优化以后,可以让网络传输的数据变少;在集群中耗费的内存资源大大减少。

2)Kryo序列化启用后生效的地方 :

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

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优化数据格式

1)fastutil介绍 

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

2)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映射的元素类型。        

3)关于fastutil调优的说明

fastutil其实没有你想象中的那么强大,也不会跟官网上说的效果那么一鸣惊人。广播变量、Kryo序列化类库、 fastutil,都是之前所说的,对于性能来说,类似于一种调味品,
烤鸡,本来就很好吃了,然后加了一点特质的孜然麻辣 粉调料,就更加好吃了一点。 分配资源、并行度、RDD架构与持久化,这三个就是烤鸡; broadcast、kryo、fastutil,类似于调料。
比如说,你的spark作业,经过之前一些调优以后,大概30分钟运行完, 现在加上broadcast、kryo、fastutil,也许就是优化到29分钟运行完、或者更好一点,也许就是28分钟、25分钟。
真正有意义的就是后面要学习的shuffle调优,可能优化之后只需要15分钟; 还有把groupByKey用reduceByKey改写,执行本地聚合,也许10分钟; 甚至可以向公司申请更多的资源,扩大
整个集群的计算能力,最后可能到达5分钟就完成任务了。

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

1)什么是数据本地化等待时长

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

2)本地化级别

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

3)数据本地化等待时长

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

7. 降低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执行算子函数时,有更多的内存可以使用。

降低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-04 19:16  Mr·Li程序员  阅读(426)  评论(0编辑  收藏  举报