Spark性能调优(reduceByKey VS groupByKey,Map vs MapPartition,foreachPartitions vs foreach,Kryo优化序列化性能等)

1 概述

官网对于Spark的调优讲解
对于spark的性能调优我推荐大家去看看美团的文章,这里我会据一些例子让大家更好的去理解。

2 调优

这里我列举出美团文章中提出的一些调优,其实都在官网上。对一些难理解的通过代码和图片的方式进行解析。

  1. 避免创建重复的RDD
  2. 尽可能复用同一个RDD
  3. 对多次使用的RDD进行持久化 (这篇文章进行了详细的讲解
  4. 尽量避免使用shuffle类算子 (shuffle基础
  5. 使用map-side预聚合的shuffle操作
  6. 使用高性能的算子
  7. 广播大变量
  8. 使用Kryo优化序列化性能
  9. 优化数据结构
  10. 资源参数调优

3 reduceByKey VS groupByKey(使用map-side预聚合的shuffle操作)

这里通过代码给大家演示下为什么要选择reduceByKey来代替groupByKey

  • groupByKey
import org.apache.spark.{SparkConf, SparkContext}

object GroupByKey {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("GroupByKey").setMaster("local[2]")
    val sc=new SparkContext(conf)
    /*
    cat input.txt 
    hello java
    hello hadoop
    hello hive
    hello sqoop
    hello hdfs
    hello spark
     */
    val file=sc.textFile("file:///home/hadoop/data/input.txt")

    //通过groupByKey后得到的数据结构
    /*
    Array[(String, Iterable[Int])] =
    Array((hive,CompactBuffer(1)),
    (hello,CompactBuffer(1, 1, 1, 1, 1, 1)),
    (java,CompactBuffer(1)), (sqoop,CompactBuffer(1)),
    (spark,CompactBuffer(1)), (hadoop,CompactBuffer(1)),
    (hdfs,CompactBuffer(1)))
     */
    file.flatMap(x=>x.split(" ")).map((_,1)).groupByKey()
        .map(x=>(x._1,x._2.sum)).collect()

    sc.stop()
  }
}

结果:
Array[(String, Int)] = Array((hive,1), (hello,6), (java,1), (sqoop,1), (spark,1), (hadoop,1), (hdfs,1))
  • reduceByKey
import org.apache.spark.{SparkConf, SparkContext}


object ReduceByKey {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("GroupByKey").setMaster("local[2]")
    val sc=new SparkContext(conf)

    val file=sc.textFile("file:///home/hadoop/data/input.txt")

    file.flatMap(x=>x.split(" ")).map((_,1)).reduceByKey((_+_)).collect()

    sc.stop()
  }
}

结果:
Array[(String, Int)] = Array((hive,1), (hello,6), (java,1), (sqoop,1), (spark,1), (hadoop,1), (hdfs,1))

我们通过两幅图看看两者之间的区别:

groupByKey:
这里写图片描述

reduceByKey:
这里写图片描述

总结:
其中第一张图是groupByKey的原理图,可以看到,没有进行任何本地聚合时,所有数据都会在集群节点之间传输;第二张图是reduceByKey的原理图,可以看到,每个节点本地的相同key数据,都进行了预聚合(这里的预聚合不就是mr中的combiner吗),然后才传输到其他节点上进行全局聚合。

我们在通过ui界面更加确定他们的区别了:

  1. groupByKey的shuffle read:252b
    这里写图片描述
    2.reduceByKey的shuffle read:238b
    这里写图片描述

4 Map vs MapPartition

package cn.zhangyu

import java.util.Random

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object MapAndMapPartition {

  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("MapAndMapPartition").setMaster("local[3]")
    val sc=new SparkContext(conf)
    val users=new Array[String](100)
    for (i<- 0 to 99){
      users(i)="user"+i
    }
    //转化为一个RDD
    val usersRDD=sc.parallelize(users)

    //调用map函数
    map(usersRDD)

    //mapPartition函数
    //mapPartition(usersRDD)
    sc.stop()
  }

  //map函数
  /**
    * Return a new RDD by applying a function to all elements of this RDD.
    * 对每个元素进行操作
    */
  def map(usersRDD:RDD[String]): Unit ={
    usersRDD.map(x=>{
      val connection = DBUtils.getConnection()
      println("----------")
      DBUtils.returnConnection(connection)
    }).foreach(println)
  }

  //mapPartition
  def mapPartition(userRDD:RDD[String]): Unit ={
    userRDD.mapPartitions(partition=>{
      val connection = DBUtils.getConnection()
      println("-------------")
      DBUtils.returnConnection(connection)
      partition

    }).foreach(println)
  }
}
//模拟连接数据库
object DBUtils {

  def getConnection() = {
    new Random().nextInt(10) + ""
  }

  def returnConnection(connection:String) = {

  }

}

mapPartitions类的算子,一次函数调用会处理一个partition所有的数据,而不是一次函数调用处理一条,性能相对来说会高一些。但是有的时候,使用mapPartitions会出现OOM(内存溢出)的问题。因为单次函数调用就要处理掉一个partition所有的数据,如果内存不够,垃圾回收时是无法回收掉太多对象的,很可能出现OOM异常。所以使用这类操作时要慎重!

建议: 再操作数据库的时候使用mapPartitions代替partition,如果出现oom那就意味着你一个partition处理的数据量太大,那就要增加partition的数量。

注意: 其实跑上面一段代码相信大家可能有一个疑问我使用了mapPartitions算子到底有几个partition呢?
如果把setMaster("local[3]") 这一段注释掉那么就是两个partition因为默认就是两个partition我们可以通过setMaster("local[ n]")去设置。

5 foreachPartitions vs foreach

使用foreachPartitions替代foreach原理类似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的所有数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提升还是很有帮助的。比如在foreach函数中,将RDD中所有数据写MySQL,那么如果是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会创建一个数据库连接,此时就势必会频繁地创建和销毁数据库连接,性能是非常低下;但是如果用foreachPartitions算子一次性处理一个partition的数据,那么对于每个partition,只要创建一个数据库连接即可,然后执行批量插入操作,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能可以提升30%以上。

6 广播大变量

官方地址http://spark.apache.org/docs/latest/rdd-programming-guide.html#broadcast-variables

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

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

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

package cn.zhangyu

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

/**
  * 通过mapjoin展示广播变量的好处
  */
object MapJoin {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf().setAppName("MapJoin").setMaster("local[1]")
    val sc=new SparkContext(conf)

    //定义一个学生表
    val students=sc.parallelize(Array(
      (1,"张三"),
      (2,"李四"),
      (3,"王五"))
    ).map(x=>(x._1,x))

    //定义一个学生信息表
    val message=sc.parallelize(Array(
      (1,"school1","201"),
      (2,"school2","202"),
      (3,"school3","203"))
    ).map(x=>(x._1,x))

    //publicdef map[U](f: ((Int, ((Int, String), (Int, String, String))))
   // students.join(message).map(x=>((x._1),(x._2._1._2),(x._2._2._2),(x._2._2._3))).collect().foreach(println)
    /*
    结果:
    (1,张三,school1,201)
    (3,王五,school3,203)
    (2,李四,school2,202)
     */


    //广播变量
    broadcastJoin(sc)
  }
  def broadcastJoin(sc:SparkContext): Unit ={
    //定义一个学生表
    val students=sc.parallelize(Array(
      (1,"张三"),
      (2,"李四"),
      (3,"王五"))
    ).collectAsMap()

    //定义一个学生信息表
    val message=sc.parallelize(Array(
      (1,"school1","201"),
      (2,"school2","202"),
      (3,"school3","203"))
    ).map(x=>(x._1,x))

    // Broadcast+map的join操作,不会导致shuffle操作。
    // 使用Broadcast将一个数据量较小作为广播变量
    //这里要注意,使用broadcast时,不能直接对RDD进行broadcast的操作.
    val studentsBroadcast = sc.broadcast(students)
    message.map(x => {
      val broadcastValue = studentsBroadcast.value // Broadcast[Map[Int, String]] = sc.broadcast(students)

      for (value <- broadcastValue if (broadcastValue.equals(x._1)) {
        yield (x._1,value._2)
      })

    }).collect().foreach(println)
  }
}

使用Kryo优化序列化性能

官方地址:http://spark.apache.org/docs/latest/tuning.html#data-serialization

  • 使用java序列化
package cn.zhangyu

import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable.ArrayBuffer

object SerializerApp1 {

  def main(args: Array[String]): Unit = {
    //创建sprakconf
    val conf = new SparkConf().setMaster("local[2]").setAppName("SerializerApp1")
    //创建sparkContext
    val  sc = new SparkContext(conf)
    //创建一个数组对象
    val persons = new ArrayBuffer[Person]()
    for (i <- 1 to 1000){
      persons += (Person(i,"person"+i,i % 2))
    }
    //转化为rdd
    val rdd = sc.parallelize(persons)
    //设置缓存级别
    //rdd.persist(StorageLevel.MEMORY_ONLY)
    rdd.persist(StorageLevel.MEMORY_ONLY_SER)
    //persist 是一个lazy操作 所以这里需要一个action触发
    rdd.count()
    //删除缓存
    //rdd.unpersist()
    sc.stop()
  }
}
//定义一个case class
case class Person(id:Int , name:String , gender:Int)

这里你可以通过打包上传进行测试,也可以通过spark-shell进行测试:

  • MEMORY_ONLY缓存级别
    在这里插入图片描述

  • MEMORY_ONLY_SER
    在这里插入图片描述

  • 使用Kryo序列化

package cn.zhangyu

import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable.ArrayBuffer

object SerializerApp1 {

  def main(args: Array[String]): Unit = {
    //创建sprakconf
    val conf = new SparkConf().setMaster("local[2]").setAppName("SerializerApp1")
    //设置为kryo序列化
    conf.set("spark.serializer","org.apache.spark.serializer.KryoSerializer")
    //需要进行注册否则不生效
    conf.registerKryoClasses(Array(classOf[Person]))
    //创建sparkContext
    val  sc = new SparkContext(conf)
    //创建一个数组对象
    val persons = new ArrayBuffer[Person]()
    for (i <- 1 to 1000){
      persons += (Person(i,"person"+i,i % 2))
    }
    //转化为rdd
    val rdd = sc.parallelize(persons)
    //设置缓存级别
    //rdd.persist(StorageLevel.MEMORY_ONLY)
    rdd.persist(StorageLevel.MEMORY_ONLY_SER)
    //persist 是一个lazy操作 所以这里需要一个action触发
    rdd.count()
    //rdd.unpersist()
    //为了查看效果让他睡一会
    Thread.sleep(100000)
    sc.stop()
  }
}
//定义一个case class
case class Person(id:Int , name:String , gender:Int)

这里通过spark-submit提交;

[hadoop@hadoop lib]$ spark-submit \
> --class cn.zhangyu.SerializerApp1 \
> --master local[2] \
> --name SerializerApp1 \
> spark_rdd-1.0-SNAPSHOT.jar

在这里插入图片描述

可以看到使用默认的序列化(java)大小为33.68k,使用了kryo序列化大小为14.5k。确实效果提升了。 但是大家要注意使用kryo进行序列化时一定要进行注册否则不会生效。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值