Spark性能优化

来源:互联网 发布:护肤方法 知乎 编辑:程序博客网 时间:2024/04/30 14:12

spark的优化 就是为spark分配更多的资源 让集群更好的运行,下分为以下两种,资源优化与代码优化

资源优化

  1. 集群层面优化

    a.搭建Spark集群的时候,分配给Spark集群更多的资源,调整spark-env.sh配置

    SPARK_WORKER_CORES SPARK_WORKER_MEMORY 1G默认配置SPARK_WORKER_INSTANCE : 在每一台节点上启动多少个Worker  默认是1个

    b.提交任务的时候,为当前application分配资源

        --executor-cores 默认情况下每一个Executor使用这台Worker所有的core    --executor-memory  1    --driver-cores 1    --driver-memory  1    --total-executor-cores

提高并行度

1.如果数据来源hdfs可以控制block数量(没啥用)2.sc.textFile(path,numPartitions)3、shuffle类的算子,可以执行numpartitions4、自定义分区器 partitionBy(Partitioner) numPartitions5、spark.default.parallelism  not default            new SparkConf.set("spark.default.parallelism","100")            ./spark-submit --master  --conf spark.default.parallelism=10        每一个Core一般分配2-3个task运行 

代码优化

1、避免创建重复的RDD

    val rdd1 = sc.textFile("hdfs://hadoop1:8020/file1.txt")    val rdd2 = sc.textFile("hdfs://hadoop1:8020/file1.txt")    创建重复的RDD 有什么问题吗?        执行性能上没有任何的影响,代码比较乱

2、尽可能复用同一个RDD

    val rdd1:RDD[(String,String)] = ...    val rdd2:RDD[String] = rdd1.map(_._2)    rdd2是rdd1的子集    rdd2.filter(_>10)    rdd2是没有必要创建出来    rdd1.filter(_._2>10)

3、对多次使用的RDD进行持久化(cache persist checkpoint)

    cache MEMORY_ONLY  数据不会序列化    persist 可以通过StorageLevel执定持久化的级别        a、MEMORY_ONLY        b、MEMORY_ONLY_SER        c、MEMORY_AND_DISK_SER  将数据序列化后,先放内存,内存放不了放磁盘        d、不建议使用DSIK_ONLY或者_2            有的时候,去磁盘上读数据还不如重新计算一次快        使用注意事项:            a、cache或者persist后,要将返回结果赋值给一个变量            b、都是懒执行,cache和persist后不能立即紧跟action类算子     checkpoint        a、持久化数据   HDFS        b、切断RDD的依赖关系        执行流程:            a、我们的job 的 job执行完成后,会从final RDD 从后往前回溯            b、在回溯的过程,哪一个RDD调用了checkpoint就对这个RDD做一个标记            c、框架会自动重新启动一个新的job,重新计算这些RDD,然后将结果持久化到HDFS上            d、切断这些RDD的依赖关系,统一改名为checkpointRDD

4、尽量避免使用shuffle类算子

        例:join 可以用 广播变量+filter        join 或者 广播变量+map   根据不同场景        如果这两个RDD的value对最终结果都有影响 必须使用 join = 广播变量+map使用场景:        一个RDD数据量比较大,一个RDD数据量比较小        多大算大?多小算小?        理论上来说:            广播变量的大小不能大于Executor内存的百分之5

5、使用map-side预聚合的shuffle操作

    reduceByKey 代替 groupByKey    aggregateByKey    combineByKey

6、使用高性能的算子

    mapPartition map    foreachPartition foreach    reduceByKey    aggreateByKey    repartition(增加分区数)    filter+coalesce(一般用来减少分区)

7、广播变量

    如果不适用广播变量,那么每一个task中都会有一个变量副本     使用广播变量,那么每一个Executor只有一份广播变量

8、使用Kryo优化序列化性能

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

9、优化数据结构

– Java中,有三种类型比较耗费内存:    • 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。    • 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。    • 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来    封装集合元素,比如Map.Entry。    • 因此Spark官方建议,在Spark编码实现中,特别是对于算子函数中的代码,尽    量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如    Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用    ,从而降低GC频率,提升性能

10、使用高性能的库fastutil

– fastutil介绍:• fastutil是扩展了Java标准集合框架(Map、List、Set;HashMap、ArrayList、HashSet)的类库,提供了特殊类型的map、set、list和queue;• fastutil能够提供更小的内存占用,更快的存取速度;我们使用fastutil提供的集合类,来替代自己平时使用的JDK的原生的Map、List、Set,好处在于,fastutil集合类,可以减小内存的占用,并且在进行集合的遍历、根据索引(或者key)获取元素的值和设置元素的值的时候,提供更快的存取速度;• fastutil最新版本要求Java 7以及以上版本;• fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中。• fastutil的每一种集合类型,都实现了对应的Java中的标准接口(比如fastutil的map,实现了Java的Map接口),因此可以直接放入已有系统的任何代码中。– 使用?<dependency><groupId>fastutil</groupId><artifactId>fastutil</artifactId><version>5.0.9</version></dependency>

shuffer调优

1、spark.shuffle.file.buffer 32k

参数说明:该参数用于设置shuffle write task的BufferedOutputStream的buffer缓冲大小。将数据写到磁盘文件之前,会先写入buffer缓冲中,待缓冲写满之后,才会溢写到磁盘。调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如64k),从而减少shuffle write过程中溢写磁盘文件的次数,也就可以减少磁盘IO次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。

2、spark.reducer.maxSizeInFlight

默认值:48m参数说明:该参数用于设置shuffle read task的buffer缓冲大小,而这个buffer缓冲决定了每次能够拉取多少数据。调优建议:如果作业可用的内存资源较为充足的话,可以适当增加这个参数的大小(比如96m),从而减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。在实践中发现,合理调节该参数,性能会有1%~5%的提升。错误:reduce oomreduce task去map拉数据,reduce 一边拉数据一边聚合   reduce段有一块聚合内存(executor memory * 0.2)解决办法:   1、增加reduce 聚合的内存的比例  设置spark.shuffle.memoryFraction            2、 增加executor memory的大小  --executor-memory 5G            3、减少reduce task每次拉取的数据量  设置spark.reducer.maxSizeInFlight  24m

3、spark.shuffle.io.maxRetries

默认值:3参数说明:shuffle read task从shuffle write task所在节点拉取属于自己的数据时,如果因为网络异常导致拉取失败,是会自动进行重试的。该参数就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业执行失败。调优建议:对于那些包含了特别耗时的shuffle操作的作业,建议增加重试最大次数(比如60次),以避免由于JVM的full gc或者网络不稳定等因素导致的数据拉取失败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的shuffle过程,调节该参数可以大幅度提升稳定性。shuffle file not find    taskScheduler不负责重试task,由DAGScheduler负责重试stage

4、spark.shuffle.io.retryWait

默认值:5s参数说明:具体解释同上,该参数代表了每次重试拉取数据的等待间隔,默认是5s。调优建议:建议加大间隔时长(比如60s),以增加shuffle操作的稳定性。

5、spark.shuffle.memoryFraction

默认值:0.2参数说明:该参数代表了Executor内存中,分配给shuffle read task进行聚合操作的内存比例,默认是20%。调优建议:在资源参数调优中讲解过这个参数。如果内存充足,而且很少使用持久化操作,建议调高这个比例,给shuffle read的聚合操作更多内存,以避免由于内存不足导致聚合过程中频繁读写磁盘。在实践中发现,合理调节该参数可以将性能提升10%左右。

6、spark.shuffle.manager

默认值:sort参数说明:该参数用于设置ShuffleManager的类型。Spark 1.5以后,有三个可选项:hash、sort和tungsten-sort。HashShuffleManager是Spark 1.2以前的默认选项,但是Spark 1.2以及之后的版本默认都是SortShuffleManager了。tungsten-sort与sort类似,但是使用了tungsten计划中的堆外内存管理机制,内存使用效率更高。调优建议:由于SortShuffleManager默认会对数据进行排序,因此如果你的业务逻辑中需要该排序机制的话,则使用默认的SortShuffleManager就可以;而如果你的业务逻辑不需要对数据进行排序,那么建议参考后面的几个参数调优,通过bypass机制或优化的HashShuffleManager来避免排序操作,同时提供较好的磁盘读写性能。这里要注意的是,tungsten-sort要慎用,因为之前发现了一些相应的bug。

7、spark.shuffle.sort.bypassMergeThreshold

默认值:200参数说明:当ShuffleManager为SortShuffleManager时,如果shuffle read task的数量小于这个阈值(默认是200),则shuffle write过程中不会进行排序操作,而是直接按照未经优化的HashShuffleManager的方式去写数据,但是最后会将每个task产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文件。调优建议:当你使用SortShuffleManager时,如果的确不需要排序操作,那么建议将这个参数调大一些,大于shuffle read task的数量。那么此时就会自动启用bypass机制,map-side就不会进行排序了,减少了排序的性能开销。但是这种方式下,依然会产生大量的磁盘文件,因此shuffle write性能有待提高。

8、spark.shuffle.consolidateFiles

默认值:false参数说明:如果使用HashShuffleManager,该参数有效。如果设置为true,那么就会开启consolidate机制,会大幅度合并shuffle write的输出文件,对于shuffle read task数量特别多的情况下,这种方法可以极大地减少磁盘IO开销,提升性能。调优建议:如果的确不需要SortShuffleManager的排序机制,那么除了使用bypass机制,还可以尝试将spark.shffle.manager参数手动指定为hash,使用HashShuffleManager,同时开启consolidate机制。在实践中尝试过,发现其性能比开启了bypass机制的SortShuffleManager要高出10%~30%。

结论:

合理选择shuffle 的类型 Hash sorthash 启用合并机制sort 启用bypass机制  reduce task去map端拉数据,失败等待时间,失败重试次数reduce task去map端拉数据,一次最多拉取48M

可能出现的问题:
reduce端 oom

    分析原因        Executor中聚合内存不够了 20%    解决        1、调整聚合内存的比例        2、调整Executor的内存大小        3、减小每次拉取的数据量  从48m 24m

JVM调优

堆内存分为年轻代与老年代
年轻代分为eden与两个survior,survior1与survoir2,eden占年轻带的80%,survior分别占1%,对象创建出来的时候先放入eden与survior1中.当这俩内存满了会出吗 minor gc 小型垃圾回收,清理不在用的对象,然后将还在使用的对象放入survior2中,如果survior2内存不够直接放入老年代中.每进行一次minor gc servior2中对象加一岁,默认大于15岁放入老年代,如果年轻代内存不是很大的,会频繁触发 minor gc ,造成老年代中存放了大量的短成名周期对象,老年代应该存放时数量较少并且会长街使用的对象 比如数据库连接池
这样的话,老年代容易会满溢,触发 full gc 比较消耗时间与性能.
不管 minor gc还是 full gc都会导致jvm的工作线停止

所以,需要降低gc的执行时间或者说执行频率1.曾加executor的内存2.提高运行内存的比例(默认20%)    a,降低缓存数据的内存比例(默认60%)    b,降低shuffle内存的比例(默认20%)    ![这里写图片描述](http://img.blog.csdn.net/20170527000828407?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbWVuZ3hiMTIxMzg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

数据本地化

我们遵循着,计算移动数据不移动,也就是最好的方案是要让计算逻辑在数据所在的节点上.
下面用一张图简单解释

shuffle file cannot found

造成这个异常可能的原因两种情况

1.executor挂掉了  可能原因有    调节堆内内存     解决上面的方案里面有参数信息    堆外内存oom        默认情况下, 这个堆外内存上限默认是每一个executor的内存                               大小的10%;处理大数据的时候,这里很容易出现问题, 导致spark作业反复崩溃, 无法运行; 此时就会去调节这个参数, 到至少1G(1024M) , 甚至说2G、 4G    调节堆外内存    • yarn下:    – --conf spark.yarn.executor.memoryOverhead=2048 单位M    • standlone下:    – --conf spark.executor.memoryOverhead=2048    spark-submit脚本里面, 去用    --conf的方式, 去添加配置;    一定要注意,不要写死在代码里2.executor没挂a,建立通讯executor在进行shuffle write,优先从自己本地关联的BlockManager中获取某份数据如果本地block manager没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取,如果本地block manager没有的话,那么会通过TransferService,去远程连接其他节点上executor的block manager去获取,尝试建立远程的网络连接,并且去拉取数据 频繁的让JVM堆内存满溢,进行垃圾回收。正好碰到那个exeuctor的JVM在垃圾回收。处于垃圾回收过程中,所有的工作线程全部停止;相当于只要一旦进行垃圾回收,spark / executor停止工作,无法提供响应,spark默认的网络连接的超时时长,是60s;如果卡住60s都无法建立连接的话,那么这个task就失败了。– 调节等待时长--conf spark.core.connection.ack.wait.timeout=300b,拉取数据的时候这个在上面也有优化方案

数据倾斜方案解决

1、使用Hive ETL预处理数据(己所不欲勿施于人,治标不治本不推荐)

• 方案适用场景:如果导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀(比如某个key对应了100万数据,其他key才对应了10条数据),而且业务场景需要频繁使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。• 方案实现思路:此时可以评估一下,是否可以通过Hive来进行数据预处理(即通过Hive ETL预先对数据按照key进行聚合,或者是预先和其他表进行join),然后在Spark作业中针对的数据源就不是原来的Hive表了,而是预处理后的Hive表。此时由于数据已经预先进行过聚合或join操作了,那么在Spark作业中也就不需要使用原先的shuffle类算子执行这类操作了。• 方案实现原理:这种方案从根源上解决了数据倾斜,因为彻底避免了在Spark中执行shuffle类算子,那么肯定就不会有数据倾斜的问题了。但是这里也要提醒一下大家,这种方式属于治标不治本。因为毕竟数据本身就存在分布不均匀的问题,所以Hive ETL中进行group by或者join等shuffle操作时,还是会出现数据倾斜,导致Hive ETL的速度很慢。我们只是把数据倾斜的发生提前到了HiveETL中,避免Spark程序发生数据倾斜而已。

– 2、过滤少数导致倾斜的key(某一用户访问频繁异常可能发生)

• 方案适用场景:如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。• 方案实现思路:如果我们判断那少数几个数据量特别多的key,对作业的执行和计算结果不是特别重要的话,那么干脆就直接过滤掉那少数几个key。比如,在Spark SQL中可以使用where子句过滤掉这些key或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后再进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可。• 方案实现原理:将导致数据倾斜的key给过滤掉之后,这些key就不会参与计算了,自然不可能产生数据倾斜。

– 3、提高shuffle操作的并行度

• 方案实现思路:在对RDD执行shuffle算子时,给shuffle算子传入一个参数,比如reduceByKey(1000),该参数就设置了这个shuffle算子执行时shuffle read task的数量。对于Spark SQL中的shuffle类语句,比如group by、join等,需要设置一个参数,即spark.sql.shuffle.partitions,该参数代表了shuffle read task的并行度,该值默认是200,对于很多场景来说都有点过小。• 方案实现原理:增加shuffle read task的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle readtask以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。

– 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、将reduce join转为map join

• 方案适用场景:在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小(比如几百M或者一两G),比较适用此方案。• 方案实现思路:不使用join算子进行连接操作,而使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作,彻底避免数据倾斜的发生和出现。将较小RDD中的数据直接通过collect算子拉取到Driver端的内存中来,然后对其创建一个Broadcast变量;接着对另外一个RDD执行map类算子,在算子函数内,从Broadcast变量中获取较小RDD的全量数据,与当前RDD的每一条数据按照连接key进行比对,如果连接key相同的话,那么就将两个RDD的数据用你需要的方式连接起来。• 方案实现原理:普通的join是会走shuffle过程的,而一旦shuffle,就相当于会将相同key的数据拉取到一个shuffle read task中再进行join,此时就是reduce join。但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现与join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜

– 6、采样倾斜key并分拆join操作

• 方案适用场景:两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用“解决方案五”,那么此时可以看一下两个RDD/Hive表中的key分布情况。如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。• 方案实现思路:– 对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出来数据量最大的是哪几个key。– 然后将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD。– 接着将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD。– 再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了。– 而另外两个普通的RDD就照常join即可。– 最后将两次join的结果使用union算子合并起来即可,就是最终的join结果

– 7、使用随机前缀和扩容RDD进行join

• 方案适用场景:如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能使用最后一种方案来解决问题了。• 方案实现思路:– 该方案的实现思路基本和“解决方案六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。– 然后将该RDD的每条数据都打上一个n以内的随机前缀。– 同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀。– 最后将两个处理后的RDD进行join即可。• 方案实现原理:将原先一样的key通过附加随机前缀变成不一样的key,然后就可以将这些处理后的“不同key”分散到多个task中去处理,而不是让一个task处理大量的相同key。该方案与“解决方案六”的不同之处就在于,上一种方案是尽量只对少数倾斜key对应的数据进行特殊处理,由于处理过程需要扩容RDD,因此上一种方案扩容RDD后对内存的占用并不大;而这一种方案是针对有大量倾斜key的情况,没法将部分key拆分出来进行单独处理,因此只能对整个RDD进行数据扩容,对内存资源要求很高
原创粉丝点击