Spark性能调优之开发过程

文章讲述了如何提高Spark中RDD的复用率,包括减少重复创建、复用RDD和持久化。避免shuffle过程可以通过预聚合和使用非shuffle算子实现。此外,推荐使用mapPartitions和broadcast等高性能算子以优化性能。
摘要由CSDN通过智能技术生成

提高RDD的复用率

Spark中对于一个RDD执行多次算子的默认原理是这样的:每次对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。
因此针对这种情况,可以从以下三个方面避免

1.减少RDD的重复创建

// False
val rdd1 = sc.textFile("file:///./test.txt")
rdd1.map()
val rdd2 = sc.textFile("file:///./test.txt")
rdd1.reduce()

// True
val rdd1 = sc.textFile("file:///./test.txt")
rdd1.map()
rdd1.reduce()

2.使用同一个RDD
在对不同的数据进行操作时,需要尽可能的复用一个RDD。例如,rdd1是tuple(a, b, c), 我们需要使用rdd1的c字段,此时不需要创建一个新的RDD,直接使用rdd1就能满足需求,减少创建新rdd带来的性能开销。

3.对需要多次使用的RDD进行持久化
Spark会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。
如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。

// 示例
// cache()调用的persist(),是使用默认存储级别的快捷设置方法,即MEMORY_ONLY
val rdd1 = sc.textFile("file:///./test1.txt").cache()
val rdd2 = sc.textFile("file:///./test2.txt").persist(MEMORY_AND_DISK_SER)

(1)详细的存储级别介绍如下:

  • MEMORY_ONLY : 将 RDD 以反序列化 Java 对象的形式存储在 JVM中。如果内存空间不够,部分数据分区将不再缓存,在每次需要用到这些数据时重新进行计算。这是默认的级别。
  • MEMORY_AND_DISK : 将 RDD 以反序列化 Java 对象的形式存储在 JVM中。如果内存空间不够,将未缓存的数据分区存储到磁盘,在需要使用这些分区时从磁盘读取。
  • MEMORY_ONLY_SER : 将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte数组)。这种方式会比反序列化对象的方式节省很多空间,尤其是在使用 fast serializer时会节省更多的空间,但是在读取时会增加CPU 的计算负担。
  • MEMORY_AND_DISK_SER : 类似于 MEMORY_ONLY_SER,但是溢出的分区会存储到磁盘,而不是在用到它们时重新计算。
  • DISK_ONLY : 只在磁盘上缓存 RDD。
  • MEMORY_ONLY_2,MEMORY_AND_DISK_2,等等 : 与上面的级别功能相同,只不过每个分区在集群中两个节点上建立副本。
  • OFF_HEAP: 类似于 MEMORY_ONLY_SER ,但是将数据存储在 off-heap memory,这需要启动off-heap 内存。

(2)存储级别的选择:
Spark 的存储级别的选择,核心问题是在内存使用率和 CPU 效率之间进行权衡。建议按下面的过程进行存储级别的选择 :

  • 如果使用默认的存储级别(MEMORY_ONLY),存储在内存中的 RDD 没有发生溢出,那么就选择默认的存储级别。默认存储级别可以最大程度的提高 CPU 的效率,可以使在 RDD 上的操作以最快的速度运行。
  • 如果内存不能全部存储 RDD,那么使用 MEMORY_ONLY_SER,并挑选一个快速序列化库将对象序列化,以节省内存空间。使用这种存储级别,计算速度仍然很快。
  • 除了在计算该数据集的代价特别高,或者在需要过滤大量数据的情况下,尽量不要将溢出的数据存储到磁盘。因为,重新计算这个数据分区的耗时与从磁盘读取这些数据的耗时差不多。
  • 如果想快速还原故障,建议使用多副本存储级别(例如,使用 Spark 作为 web 应用的后台服务,在服务出故障时需要快速恢复的场景下)。所有的存储级别都通过重新计算丢失的数据的方式,提供了完全容错机制。但是多副本级别在发生数据丢失时,不需要重新计算对应的数据库,可以让任务继续运行。

避免shuffle过程

shuffle过程就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。比如reduceByKey、join等算子,都会触发shuffle操作。shuffle过程是整个job过程中最消耗性能的地方,要减少shuffle过程,就需要尽量避免使用shuffle算子。

做法:
尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子。没有shuffle操作或者仅有较少shuffle操作的Spark作业,可以大大减少性能开销。

// 示例
// 传统的join操作,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。
val rdd3 = rdd1.join(rdd2)
// 通过broadcas将小数据量的rdd进行广播,在map阶段进行join,不产生shuffle
val rdd2Data = rdd1.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)
val rdd3 = rdd1.map(rdd2DataBroadcast...)
// 注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用。
// 因为每个Executor的内存中,都会驻留一份rdd2的全量数据

在map阶段进行预聚合,减少shuffle的数据量

一定要使用shuffle操作时,尽量使用可以map-side预聚合的算子。map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。通常来说,在可能的情况下,建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。
例如下图,上面是groupByKey,下面是reduceByKey,在map阶段进行一次预聚合,大大减少了shuffle的数据量。

上:groupByKey,下:reduceByKey

使用一次性处理的高性能算子

(1)使用mapPartitions替代普通map
(2)使用foreachPartitions替代foreach
(3)使用filter之后进行coalesce操作

  • mapPartitions和foreachPartitions是一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。
  • 对一个RDD执行filter算子过滤掉RDD中较多数据后(比如30%以上的数据),建议使用coalesce算子,手动减少RDD的partition数量,将RDD中的数据压缩到更少的partition中去。因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值