spark调优

本文介绍了SparkCore开发调优的关键技巧,包括避免重复加载RDD,合理使用缓存策略,选择高效的算子,避免shuffle操作,使用Kryo序列化,以及解决数据倾斜问题。通过避免RDD的重复创建,对常用RDD进行持久化,减少shuffle操作,优化序列化方式,以及针对数据倾斜提出的具体解决方案,可以显著提升Spark应用的性能。
摘要由CSDN通过智能技术生成

SparkCore开发调优

避免创建重复的RDD

Spark需要从HDFS上两次加载hello.txt文件的内容,并创建两个单独的RDD;第二次加载HDFS文件以及创建RDD的性能开销,很明显是白白浪费掉的。

val rdd1 = sc.textFile("hdfs://master:9000/hello.txt")

rdd1.map(...)

val rdd2 = sc.textFile("hdfs://master:9000/hello.txt")

rdd2.reduce(...)

正确使用。

val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt")

rdd1.map(...)

rdd1.reduce(...)

 

对多次使用的RDD进行缓冲持久化

如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。

缓存,使用非序列化的方式将RDD中的数据全部持久化到内存中。

只有在第一次执行map算子时,才会将这个rdd1从源头处计算一次。第二次执行reduce算子时,就会直接从内存中提取数据进行计算,不会重复计算一个rdd。

val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache()

rdd1.map(...)

rdd1.reduce(...)

持久化,一共有12种持久化的方式。基于内存、磁盘、序列化、副本。

val rdd1 = sc

.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER)

rdd1.map(...)

rdd1.reduce(...)

 

尽量避免使用shuffle类算子(广播变量)

尽可能避免使用reduceByKey、join、distinct、repartition等会进行shuffle的算子。

可以通过广播变量的方式将数据量较小的RDD广播出去。广播大变量,小数据。

val rdd1Data = rdd1.collect()

val rdd1DataBroadCast = sc.broadcast(rdd1Data)

rdd2.map(rdd1DataBroadCast...)

 

使用性能较高的算子

reduceByKey替代groupByKey,因为reduceByKey会先进行一次局部聚合。

foreachPartitions替代foreach,例如在将数据写入数据库时,如果使用foreach每条数据都会连接一次数据库,频繁的连接数据库,势必会造成性能低下,但是使用foreachPartitions每个分区连接一次数据库,进行批量插入操作,性能较高。

filter之后进行coalesce操作(过略的脏数据比较多时),因为filter之后,RDD的每个partition中都会有很多数据被过滤掉,此时如果照常进行后续的计算,其实每个task处理的partition中的数据量并不是很多,有一点资源浪费,而且此时处理的task越多,可能速度反而越慢。因此用coalesce减少partition数量,将RDD中的数据压缩到更少的partition之后,只要使用更少的task即可处理完所有的partition。在某些场景下,对于性能的提升会有一定的帮助。

 

使用Kryo序列化

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

在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输,如广播变量。

将自定义的类型作为RDD的泛型类型时(比如Student是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现Serializable接口。或者定义成样例类,默认实现了序列化接口。

使用可序列化的持久化策略时(比如MEMORY_ONLY_SER),Spark会将RDD中的每个partition都序列化成一个大的字节数组。

Spark默认使用的是Java的序列化机制,但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。但是Spark同时支持使用Kryo序列化库,Kryo序列化类库的性能比Java序列化类库的性能要高很多。官方介绍,Kryo序列化机制比Java序列化机制,性能高10倍左右。Spark之所以默认没有使用Kryo作为序列化类库,是因为Kryo要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说,这种方式比较麻烦。

/ 创建SparkConf对象。

val conf = new SparkConf().setMaster(...).setAppName(...)

// 设置序列化器为KryoSerializer。

conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")

// 注册要序列化的自定义类型。

conf.registerKryoClasses(Array(classOf[MyClass1], classOf[MyClass2]))

 

SparkCore解决数据倾斜

 

数据倾斜发生的场景

绝大多数task执行得都非常快,但个别task执行极慢。比如,总共有1000个task,997个task都在1分钟之内执行完了,但是剩余两三个task却要一两个小时。

 

某个分区的数据异常的多。所以一个Task处理的时间也就比较长。数据倾斜一般发生在Shuffle过程中。

自定义分区器。

提高shuffle操作的并行度

说白了就是增多分区的个数。让Task增多。让之前一个分区中的数据分散成多个分区。但是这种方式对于某个同key值比较多的数据,显然是不适用的。读取文件的时候,无论是读取HDFS上的文件还是读取本地的文件都,Task的个数都和文件个数有关。

Hehe heheehhehehahahahaahhaahheihei  加入它们都在一个分区中。我增加我的分区个数。

he、ha、hei

 

代码实现:

val func = (index:Int,iter:Iterator[(String,Int)]) => {

    iter.map(index+":"+_)

  }

 

  val func2 = (index:Int,iter:Iterator[String]) => {

    iter.map(index+":"+_)

  }

 

  def main(args: Array[String]): Unit = {

    val conf = new SparkConf()

      .setAppName("haha")

      .setMaster("local[1]")

      .set("spark.testing.memory", "2147480000")

    val sc = new SparkContext(conf)

    val lines = sc.textFile("D:\\abc\\wordcount\\input")

    lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_,10)

      .mapPartitionsWithIndex(func)

      .foreach(println)

 

    val lines2 = sc.makeRDD(List("tom","jim","susan"))

    lines2.mapPartitionsWithIndex(func2)

      .foreach(println)

  }

 

 

         加盐打散数据局部聚合、去盐再全局聚合。进行了两次聚合任务,但对于有某个key值数据倾斜比较严重时,这么做是值得的。

加盐的好处:使用默认的HashPartitioner,不用自定义分区。

          坏处:要去盐。

自定义分区:返回随机的分区号。

 Hello  1 2 3

  1  hello  10

2  hello  20

 

代码实现。

val func = (index:Int,iter:Iterator[(String,Int)]) => {

    iter.map(index+":"+_)

  }

 

  def main(args: Array[String]): Unit = {

    val conf = new SparkConf()

      .setAppName("haha")

      .setMaster("local[3]")

      .set("spark.testing.memory", "2147480000")

    val sc = new SparkContext(conf)

 

 

    val lines = sc.makeRDD(List("aa","aa","aa","aa","aa","aa","aa"))

    //优化前

    lines.map((_,1))

      .reduceByKey(_+_)

      .mapPartitionsWithIndex(func)

      .foreach(println)

 

    //优化后

    val reduceData = lines.map(x => {

      val i: Int = Random.nextInt(3)

      (i.toString + "-" + x, 1)

    }).reduceByKey(_ + _)

    //拼上随机前缀聚合后

    reduceData

      .mapPartitionsWithIndex(func)

      .foreach(println)

    //去掉随机前缀,再次聚合后

    reduceData

      .map(x => (x._1.substring(2),x._2))

      .reduceByKey(_+_)

      .mapPartitionsWithIndex(func)

      .foreach(println)

  }

 

         大小表join时使用广播变量。将某一个RDD比较小的数据广播出去。

def main(args: Array[String]): Unit = {

    val conf = new SparkConf()

      .setAppName("haha")

      .setMaster("local[3]")

      .set("spark.testing.memory", "2147480000")

    val sc = new SparkContext(conf)

    val weight = sc.textFile("D:\\abc\\join\\weigth.txt")

      .map(line => {

        val arr = line.split(",")

        (arr(0),arr(1))

      }).collect().toMap

    // 将体重数据广播出去,小数据

    val broadData = sc.broadcast(weight)

 

    val student = sc.textFile("D:\\abc\\join\\student.txt")

      .map(line => {

        val arr = line.split(",")

        //通过广播变量避免了使用join算子执行出现的shuffle操作

        val weightData = broadData.value

        val str = weightData.get(arr(0)).get

        (arr(0),arr(1),str)

      }).foreach(println)

  }

 

         抽取出倾斜的key,拆分RDD,加盐再join。适用于一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀。

步骤如下。

通过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结果。

关于sample算子。

sample(withReplacement : scala.Boolean, fraction : scala.Double,seed scala.Long)

sample算子时用来抽样用的,其有3个参数

withReplacement:表示抽出样本后是否在放回去,true表示会放回去,这也就意味着抽出的样本可能有重复。

fraction :抽出多少,这是一个double类型的参数,0-1之间,eg:0.3表示抽出30%

seed:表示一个种子,根据这个seed随机抽取,一般情况下只用前两个参数就可以,那么这个参数是干嘛的呢,这个参数一般用于调试,有时候不知道是程序出问题还是数据出了问题,就可以将这个参数设置为定值。

代码演示。

def main(args: Array[String]): Unit = {

    val conf=new SparkConf().setAppName("WordCount").setMaster("local[2]")

    val sc =new SparkContext(conf)

    val keys = getKeyBySample(sc)

    System.out.println("导致数据倾斜的key是:" + keys)

  }

  def getKeyBySample(sc:SparkContext)={

    val data = Array("A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A",

      "A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A",

      "A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A","A",

      "B","B","B","B","B","B","B","B","C","D","E","F","G")

    val rdd= sc.parallelize(data)

 

    val tupleRdd: Array[(Int, String)] =rdd.map(line=>(line,1)) //变成 (word,1) 的形式

      .sample(true,0.4) //采样,取40%做样本

      .reduceByKey((x,y)=>x+y)  //单词统计 结果类似 (A,8),(B,18)

        .map(line=>(line._2,line._1)) //==>交换顺序(8,A) ,(18,B) 为了便于后面把单词次数多的,排序筛选出来

      .sortBy(line=>line._1,false,2) //排序,按照单词频次倒排

      .take(3) //取出来单词数前三的 ,这些可能就是脏key 后者热点key

    for(ele <- tupleRdd){

      println(ele._2+"===的个数为======"+ele._1)

    }

  }

 

资源调优

executor-cores                       

total-executor-cores

executor-memory

driver-memory

该参数用于设置Driver进程的内存,如果需要使用collect算子,比如广播变量时,将RDD的数据全部拉取到Driver上进行处理,那么必须确保Driver的内存足够大,否则会出现OOM内存溢出的问题。

spark.default.parallelism

该参数用于设置每个stage的默认task数量,默认是一个HDFS block对应一个task,Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值