spark开发调优

在开发过程中需要时刻注意开发调优的一些基本原则,并将这些原则根据具体的业务以及实际的应用场景,灵活地运用到自己的spark作业开发中。

1. 避免创建重复的RDD

对于同一份数据,只应该创建一个RDD,不能创建多个RDD来代表同一份数据。

错误示例 :对于一份数据执行多次算子操作时,使用多个RDD

// 需要从HDFS上两次加载hello.txt文件的内容,并创建两个单独的RDD


val rdd1 = sc.textFile("hdfs://xydwns/user/hive/warehouse/xy_temp.db/hello.txt")
rdd1.map(...)


val rdd2 = sc.textFile("hdfs://xydwns/user/hive/warehouse/xy_temp.db/hello.txt")
rdd2.reduce(...)

正确示例:对于一份数据执行多次算子操作时,只使用一个RDD。

// 正确用法:对于一份数据执行多次算子操作时,只使用一个RDD。
val rdd1 = sc.textFile("hdfs://xydwns/user/hive/warehouse/xy_temp.db/hello.txt")
rdd1.map(...)
rdd1.reduce(...) 

由于rdd1被执行了两次算子操作,第二次执行reduce操作的时候,还会再次从源头处重新计算一次rdd1的数据,因此还是会有重复计算的性能开销,对多次使用的RDD进行持久化”,才能保证一个RDD被多次使用时只被计算一次。

2. 尽可能复用同一个RDD

除了要避免在开发过程中对一份完全相同的数据创建多个RDD之外,在对不同的数据执行算子操作时还要尽可能地复用一个RDD。

错误做法

// 对rdd1执行了一个map操作,而rdd2中的数据仅仅是rdd1中的value值而已,是rdd1的子集
JavaPairRDD<Long, String> rdd1 = ...
JavaRDD<String> rdd2 = rdd1.map(...)

// 分别对rdd1和rdd2执行了不同的算子操作。
rdd1.reduceByKey(...)
rdd2.map(...)

正确做法

// 正确的做法
JavaPairRDD<Long, String> rdd1 = ...
rdd1.reduceByKey(...)
rdd1.map(tuple._2...) 

对rdd1我们还是执行了两次算子操作,rdd1实际上还是会被计算两次,对多次使用的RDD进行持久化”进行使用,才能保证一个RDD被多次使用时只被计算一次

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

针对上面1和2正确做法中还存在的问题,再做进一步优化

正确示例1

// cache()方法表示:使用非序列化的方式将RDD中的数据全部尝试持久化到内存中。
val rdd1 = sc.textFile("hdfs://xydwns/user/hive/warehouse/xy_temp.db/hello.txt").cache()
rdd1.map(...)
rdd1.reduce(...)

正确示例2

// persist()方法表示:手动选择持久化级别,并使用指定的方式进行持久化。
// 序列化的方式可以减少持久化的数据对内存/磁盘的占用量,进而避免内存被持久化数据占用过多,从而发生频繁GC。
val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)
rdd1.map(...)
rdd1.reduce(...)

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

Spark作业运行过程中,最消耗性能的地方就是shuffle过程。shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作,比如reduceByKey、join、distinct、repartition等算子,都会触发shuffle操作,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作,磁盘IO和网络数据传输也是shuffle性能较差的主要原因。

在我们的开发过程中,能避免则尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子,尽量使用map类的非shuffle算子。

假设rdd1数据量很大,rdd2数据量比较小(比如几百M)

错误示例

// 因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。
val rdd3 = rdd1.join(rdd2)

正确示例

// broadcast+map的join操作,不会导致shuffle操作。
val rdd2Data = rdd2.collect()
val rdd2DataBroadcast = sc.broadcast(rdd2Data)
val rdd3 = rdd1.map(rdd2DataBroadcast...) 

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

有时候因业务需要,不得不使用shuffle操作,无法用broadcast+map来替代,那么尽量使用可以map-side预聚合的算子。

所谓的map-side预聚合,说的是在每个节点本地对相同的key进行一次聚合操作,类似于MapReduce中的本地combiner。map-side预聚合之后,每个节点本地就只会有一条相同的key,因为多条相同的key都被聚合起来了。其他节点在拉取所有节点上的相同key时,就会大大减少需要拉取的数据数量,从而也就减少了磁盘IO以及网络传输开销。

比如,通常情况下建议使用reduceByKey或者aggregateByKey算子来替代掉groupByKey算子。

6. 使用高性能的算子

  • reduceByKey/aggregateByKey替代groupByKey
  • mapPartitions替代普通map
  • foreachPartitions替代foreach
  • filter之后进行coalesce操作
  • repartitionAndSortWithinPartitions替代repartition与sort

7. 广播大变量

有时在开发过程中,会遇到需要在算子函数中使用外部变量的场景(尤其是大变量,比如100M以上的大集合),那么此时就应该使用Spark的广播(Broadcast)功能来提升性能。

在算子函数中使用到外部变量时,默认情况下,Spark会将该变量复制多个副本,通过网络传输到task中,此时每个task都有一个变量副本。如果变量本身比较大的话(比如100M,甚至1G),那么大量的变量副本在网络中传输的性能开销,以及在各个节点的Executor中占用过多内存导致的频繁GC,都会极大地影响性能。

因此对于上述情况,如果使用的外部变量比较大,建议使用Spark的广播功能,对该变量进行广播。广播后的变量,会保证每个Executor的内存中,只驻留一份变量副本,而Executor中的task执行时共享该Executor中的那份变量副本。这样的话,可以大大减少变量副本的数量,从而减少网络传输的性能开销,并减少对Executor内存的占用开销,降低GC的频率。

假设list1变量几百M甚至1-2G

错误示例

// 此时没有做任何特殊操作,每个task都会有一份list1的副本。
val list1 = ...
rdd1.map(list1...)

正确示例

// 以下代码将list1封装成了Broadcast类型的广播变量
val list1 = ...
val list1Broadcast = sc.broadcast(list1)
rdd1.map(list1Broadcast...)

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

在Spark中,主要有三个地方涉及到了序列化:

  • 在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输
  • 将自定义的类型作为RDD的泛型类型时(比如JavaRDD,Student是自定义类型),所有自定义类型对象,都会进行序列化
  • 使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组

Spark支持的序列化机制

  • Java的序列化机制:是spark默认使用的序列化机制;性能比较差
  • Kryo序列化机制:比Java序列化机制,性能高10倍左右;但要求最好要注册所有需要进行序列化的自定义类型,比较麻烦

Kryo序列化示例

// 创建SparkConf对象
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

// 设置序列化器为KryoSerializer。
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

// 注册要序列化的自定义类型。
conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

9. 优化数据结构

Java中,有三种类型比较耗费内存:

  • 对象,每个Java对象都有对象头、引用等额外的信息,因此比较占用内存空间。
  • 字符串,每个字符串内部都有一个字符数组以及长度等额外信息。
  • 集合类型,比如HashMap、LinkedList等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如Map.Entry。

Spark官方建议,特别是对于算子函数中的代码,尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比如Int、Long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用,从而降低GC频率,提升性能。这个建议仅供参考,开发过程中还是要结合实际情况。

 

参考

强烈推荐:https://www.cnblogs.com/itboys/p/6515682.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值