Spark性能调优

96
终生学习丶
0.2 2017.11.04 17:19* 字数 5212

调优之前是将功能实现...然后算法优化,设计优化,再是spark调优!,需得一步一步来,不得直接越过,直接调优!

executor调优

对于exector的调优基于一个原则,那就是使用端口号界面看cpu的使用率.

在new SparkContext后,集群就会为在Worker上分配executor,如果executor对cpu使用率低的话,那就可以多增加几个executor. 但是一套机器上的使用内存是不变的,executor越多,每个executor分配的内存就越少,从而会导致shuffle过程溢写到磁盘上的文件过多,出现过多的数据spill over(溢出)甚至out of memory(内存不足)的情况.

每一次task只能处理一个partition的数据,这个值太小了会导致每片数据量太大,导致内存压力,或者诸多executor的计算能力无法利用充分;但是如果太大了则会导致分片太多,执行效率降低。

调优方式1: 程序跑的过程中,发现Spark job有时候特别慢,查看cpu的利用率很低,可以尝试减少每个executor占用cpu core的数量,增加并行executor的数量.配合增加切片,整体上提高cpu的利用率,从而提高性能.

调优方式2: 如果发现Spark job的时常发生内存溢出, 增加分片的数量,从而就可以减少每片数据的规模,能够更多的task处理同样的任务,减少executor的数量,这样每个executor能够分配到的内存就更大,相当于每个task的内存能够更大的分配. 可能运行速度变慢了些,但是不会内存溢出了.

调优方式3: 如果数据量很少,有大量的小文件生成, 那就减少文件分片,使task整体数量减少,每个task能够分到更大的内存.,处理更多的数据,就不会有大量的小文件生成.


频繁GC或者OOM优化

频繁GC(垃圾回收)或者OOM(内存溢出)在Driver端和Executor端分别有不同的处理方式

Driver端: 通常由于计算过大的结果集被回收到Driver端导致,需要调大Driver端的内存解决,或者进一步减少结果集的数量。

Executor端:

1. 以外部数据作为输入的Stage,这类Stage中出现GC通常是因为在Map侧进行map-side-combine时,由于group过多引起的。解决方法可以增加partition的数量(即task的数量)来减少每个task要处理的数据,来减少GC的可能性

2. 以shuffle作为输入的Stage:这类Stage中出现GC的通常原因也是和shuffle有关,常见原因是某一个或多个group的数据过多,也就是所谓的数据倾斜,简单的方法就是增加shuffle的数量.复杂点就在业务逻辑上调整,预先针对较大的group做单独处理。


数据倾斜优化

注: 数据倾斜一般是代码中的某些算子导致的

数据倾斜的物理本质

数据倾斜只会发生的shuffle阶段,有时候会将各个节点相同的key拉取到某一个Task进行处理(比如join等操作),如果某个相同的key远远超过其他的key,那么这个Task所处理的数据量也就特别庞大,而spark作业的进度,也就是由最长的那个task决定的.

可能会诱发数据倾斜的算子

开发当中,不影响业务和效率的情况下,应当尽量避免以下算子:

distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition..

如何使用web ui看数据倾斜

通过Spark Web UI来查看当前stage各个task分配的数据量,进而确定是不是task的数据分配不均匀导致了数据倾斜

数据倾斜

如图上就是典型的数据倾斜, 有的task处理完数据,只要5s,共1000多个key,而有的task要处理完数据需要4分钟,一万多个key..

如何处理数据倾斜

1. 预处理数据源

有些数据源的数据,本身分布就是不均匀的,那就可以实现预处理,如比如数据源是hive的话,而hive本身的数据刚好不均匀,那就对Hive数据源进行ETL,再进行spark作业. 也就是讲spark的shuffle操作提前到了Hive ETL中.

这种方法数据倾斜还是存在的,只不过发生在了spark处理之前

2. 增加stage中task的数量

一个stage中封装了一组具有相同逻辑的task,默认是200个.我们可以通过设置spark.sql.shuffle.partitions来修改task的数量,比如增加为1000,此时,stage分配的内存也会增加,stage里面更多的的task能够处理相同的key,这样的操作能够有效的缓解和减轻数据倾斜问题.

这种方法面对倾斜不是非常严重的情况,还是挺有用的,但是面对百万,千万key的话,就力不从心了

3.过滤一些导致倾斜的key

再某些场景下,某个key突然暴增,在对数据影响轻微的情况下,我们可以直接过滤这个key,让他不参与计算.

这种方法完美规避数据倾斜,但是有可能是多个key导致的,那这个就不太好了.

4. 添加随机前缀

对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案

这个方法需要两次聚合,第一次是局部聚合,先给每个key都打上一个随机数,比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello, 1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1) (2_hello, 1) (2_hello, 1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello, 2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。

将原本相同的key通过附加随机前缀的方式,变成多个不同的key,就可以让原本被一个task处理的数据分散到多个task上去做局部聚合,进而解决单个task处理数据量过多的问题。接着去除掉随机前缀,再次进行全局聚合,就可以得到最终的结果.

这种方法很好,我在其他的博客上看到的,还没实践过,真心可以.不过仅限于聚合类

5.多种方案组合完成

就是将以上几种方案一起组合起来使用,比如先预处理,在增加task,再增加前缀..当然,处理数据倾斜的方案肯定其他的,我也在不断的学习!


程序开发调优

1. 避免创建重复的RDD

代码如下: sc.textFile("hdfs://192.168.0.1:9000/hello.txt").map(...)

                 sc.textFile("hdfs://192.168.0.1:9000/hello.txt").reduce(...)

对于同一份数据,我们只需要创建一份RDD来处理即可,如果对同一份数据创建了多个RDD,那就意味着spark作业会进行多次创建代表相同数据的RDD,并且对每一个RDD都重复计算,对spark作业的效率.内存.性能开销都有明显的影响

2. 尽可能复用同一个RDD

代码如下: JavaPairRDD rdd1 = ...        JavaRDD rdd2 = rdd1.map(...)

                 rdd1.reduceByKey(...)            rdd1.map(tuple._2...)

针对不同的数据,但是不同的数据有重叠或者包含的关系,这样的情景下,我们尽量复用同一个RDD,因为这样可以尽可能地减少RDD的数量,从而尽可能减少算子执行的次数。

3. 对多次使用的RDD进行持久化

自动持久化: valrdd1= sc.textFile("hdfs://192.168.0.1:9000/666.txt").cache()

rdd1.map(...)         rdd1.reduce(...)   

//cache()使用非序列化的方式将RDD中的数据全部尝试持久化到内存

手动持久化: valrdd1= sc.textFile("hdfs://192.168.0.1:9000/666.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)

rdd1.map(...)        rdd1.reduce(...)

//prsist()使用指定的方式进行持久化,StorageLevel.MEMORY_AND_DISK_SER表示内存充足时有限持久化到内存中,不充足时持久化到磁盘中,后缀_SER表示使用序列化保存RDD数据

对多次使用的RDD进行持久化就是保证一个RDD执行多次算子操作时, 这个RDD本身仅仅被计算一次

对于多次使用的RDD进行持久化操作,Spark就会将数据保存到内存或磁盘中,下次使用此RDD时,就会从内存或磁盘中提取已持久化的rdd,而不需要将此RDD重新再计算一遍

4. 尽量避免使用shuffle算子

在上面开篇就提到过:在开发当中,不影响业务和效率的情况下,尽量避免使用shuffle类算子,这类算子包括distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition...

spark作业中最消耗性能的是shuffle,shuffle就是将key相同的数据,拉取到同一个task上进行join.reduceByKey等算子操作.这个过程中会有大量的磁盘IO,数据传输,内存消耗,对性能有很大的影响.

5. groupByKey算子替代

在要使用groupByKey算子的时候,尽量用reduceByKey或者aggregateByKey算子替代.因为调用groupByKey时候,按照相同的key进行分组,形成RDD[key,Iterable[value]]的形式,此时所有的键值对都将被重新洗牌,移动,对网络数据传输造成理论上的最大影响.

GroupByKey的方式

而reduceByKey的话,他会先将本地的数据预聚合一次(通过reduceByKey的lambda函数,),就只有一条key了, 而进行shuffle操作,将所有相同的key拉取到同一个task上时,就可以减少极大的IO以及数据传输. 所以用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子,这样可以提前预聚合,从而提高性能.

ReduceByKey的方式

6. 使用高性能的算子

像上面的reduceByKey或者aggregateByKey算子来替代掉groupByKey算子,这就是使用高性能算子,与此类似的还有

1) 使用mapPartitions替代普通map; mapPartitions算子,一次函数调用会处理一个partition的数据,而不是一次函数调用处理一条,性能也有大的提升. 但是一次处理一个partition的数据,就要一次处理一个partition的数据,如果内存不够,就会出现OOM操作.

2) 使用foreachPartitions替代foreach; foreachPartitions也是一次调用处理一个partition的数据,而非foreach的一条,对性能也有大的提升,但是和上面一样,也要注意内存的充足与否

3) filter之后进行coalesce操作; filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。这对spark的性能也有一定的稳定与帮助

4) repartitionAndSortWithinPartitions替代repartition与sort类操作; 如果需要在repartition重分区之后,还要进行排序, 建议直接使用repartitionAndSortWithinPartitions算子。因为该算子可以一边进行重分区的shuffle操作,一边进行排序。shuffle与sort两个操作同时进行,比先shuffle再sort来说,性能也能优化不少.

7. 使用广播大变量

代码如下: val list1= ...

                rdd1.map(list1...)

                val list1= ...

                val list1 Broadcast=sc.broadcast(list1)

               rdd1.map(list1Broadcast...)

使用一般的外部变量,如果外部变量内存较大,会导致频繁GC,这时候可以使用spark的广播功能. 广播后的变量,会保证每个Executor的内存中会有一份变量副本,此时task会共享它所在的Executor中的那个广播变量. 从而减少网络传输的性能开销,并减少了副本的数量,减少对Executor内存的占用开销,降低了GC的频率。

8. 优化数据结构

Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽量不要使用字符串.集合.对象三种数据结构.

尽量使用字符串代替对象,尽量用基本数据类型(Int,Long..)代替字符串,尽量舒勇数组代替集合,  这样可以尽可能减少内存占用,降低GC频率,提高性能


运行资源调优

这一段我摘抄的...

前面的调优一直都提到了一个点,那就是内存的分配,资源的分配.不然会导致OOM,频繁GC..   

1) num-executors

参数说明:该参数用于设置Spark作业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽可能按照你的设置来在集群的各个工作节点上,启动相应数量的Executor进程。这个参数非常之重要,如果不设置的话,默认只会给你启动少量的Executor进程,此时你的Spark作业的运行速度是非常慢的。

参数调优建议:每个Spark作业的运行一般设置50~100个左右的Executor进程比较合适,设置太少或太多的Executor进程都不好。设置的太少,无法充分利用集群资源;设置的太多的话,大部分队列可能无法给予充分的资源。

2) executor-memory

参数说明:该参数用于设置每个Executor进程的内存。Executor内存的大小,很多时候直接决定了Spark作业的性能,而且跟常见的JVM OOM异常,也有直接的关联。

参数调优建议:每个Executor进程的内存设置4G~8G较为合适。但是这只是一个参考值,具体的设置还是得根据不同部门的资源队列来定。可以看看自己团队的资源队列的最大内存限制是多少,num-executors乘以executor-memory,是不能超过队列的最大内存量的。此外,如果你是跟团队里其他人共享这个资源队列,那么申请的内存量最好不要超过资源队列最大总内存的1/3~1/2,避免你自己的Spark作业占用了队列所有的资源,导致别的同学的作业无法运行。

3) executor-cores

参数说明:该参数用于设置每个Executor进程的CPU core数量。这个参数决定了每个Executor进程并行执行task线程的能力。因为每个CPU core同一时间只能执行一个task线程,因此每个Executor进程的CPU core数量越多,越能够快速地执行完分配给自己的所有task线程。

参数调优建议:Executor的CPU core数量设置为2~4个较为合适。同样得根据不同部门的资源队列来定,可以看看自己的资源队列的最大CPU core限制是多少,再依据设置的Executor数量,来决定每个Executor进程可以分配到几个CPU core。同样建议,如果是跟他人共享这个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,也是避免影响其他同学的作业运行。

4) driver-memory

参数说明:该参数用于设置Driver进程的内存。

参数调优建议:Driver的内存通常来说不设置,或者设置1G左右应该就够了。唯一需要注意的一点是,如果需要使用collect算子将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

5) spark.default.parallelism

参数说明:该参数用于设置每个stage的默认task数量。这个参数极为重要,如果不设置可能会直接影响你的Spark作业性能。

参数调优建议:Spark作业的默认task数量为500~1000个较为合适。很多同学常犯的一个错误就是不去设置这个参数,那么此时就会导致Spark自己根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。通常来说,Spark默认设置的数量是偏少的(比如就几十个task),如果task数量偏少的话,就会导致你前面设置好的Executor的参数都前功尽弃。试想一下,无论你的Executor进程有多少个,内存和CPU有多大,但是task只有1个或者10个,那么90%的Executor进程可能根本就没有task执行,也就是白白浪费了资源!因此Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,比如Executor的总CPU core数量为300个,那么设置1000个task是可以的,此时可以充分地利用Spark集群的资源。

6) spark.storage.memoryFraction

参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,可以用来保存持久化的RDD数据。根据你选择的不同的持久化策略,如果内存不够时,可能数据就不会持久化,或者数据会写入磁盘。

参数调优建议:如果Spark作业中,有较多的RDD持久化操作,该参数的值可以适当提高一些,保证持久化的数据能够容纳在内存中。避免内存不够缓存所有的数据,导致数据只能写入磁盘中,降低了性能。但是如果Spark作业中的shuffle类操作比较多,而持久化操作比较少,那么这个参数的值适当降低一些比较合适。此外,如果发现作业由于频繁的gc导致运行缓慢(通过spark

web ui可以观察到作业的gc耗时),意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

7) spark.shuffle.memoryFraction

参数说明:该参数用于设置shuffle过程中一个task拉取到上个stage的task的输出后,进行聚合操作时能够使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操作。shuffle操作在进行聚合时,如果发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地降低性能。

参数调优建议:如果Spark作业中的RDD持久化操作较少,shuffle操作较多时,建议降低持久化操作的内存占比,提高shuffle操作的内存占比比例,避免shuffle过程中数据过多时内存不够用,必须溢写到磁盘上,降低了性能。此外,如果发现作业由于频繁的gc导致运行缓慢,意味着task执行用户代码的内存不够用,那么同样建议调低这个参数的值。

日记本