Spark的数据倾斜

1.数据倾斜的概念

(这个不用说了都懂)略

2.发生数据倾斜的现象

  • 个别的task运行时间明显长于其他的task
  • 发生OOM异常

3.发生数据倾斜的原因

spark只要是发生数据倾斜必然经历了shuffle,也就是shuffle是数据倾斜的必要条件

4.发生数据倾斜之后的解决方案

1.提高并行度
程序运行缓慢,第一反应大多是资源分配不足,并行度不够。提高并行度是我们做数据倾斜调优的第一
步尝试,提高并行度会在一定程度上减轻数据倾斜的压力,但是并不能从彻底上根除数据倾斜。因为一
旦发生数据倾斜,倾斜的key无论如何提高并行度,经过shuffle操作都会直到一个分区中去。
如何提高并行度?两个地方进行设置。

  1. spark.default.parallelism 设置spark程序全局并行度
  2. shuffle操作的第二个参数进行设置(局部)并行度

2.过滤掉发生数据倾斜的key
找到导致数据倾斜的key的值,通过和业务人员进行沟通,确定该key值是否有价值,如果没有价值的话将这个key直接过滤掉即可,但是如果这个key是有效的话千万万不能搞掉,否则后果自负

3.Hive ETL中做预处理
这个处理的方法,主要在于Spark作业加载hive表中的数据,进行业务处理。加入hive的数据有倾斜现
象,在spark中的处理,自然会出现dataskew。而如果spark作业一般只是想web 端提供查询服务,针
对这种情况就比较适合这个解决方法。
Hive ETL预处理,数据倾斜的现象在hive中提前被处理,这样加载到spark中的数据有倾斜吗?没有!
此时spark给web服务端只提供一个查询服务,所以没有的数据倾斜,效率非常高!只不过此时将数据
倾斜解决掉了吗?是把spark端的dataskew转移到hive中,这是甩锅的行为,个人不推荐,毕竟不人道

4.进行两阶段的聚合

两阶段聚合操作,指的是局部聚合+全局聚合。该方法适合于哪些XxxxByKey的操作,比如

groupByKey、reduceByKey的聚合操作.

核心思路就是:某个key的量特别的大,如果直接进行聚合操作会灰常的困难,因为这个key会进入到XXXbykey之后的某个特定的分区内,这个ResultTask的任务量是相当大的,那么可以先将者这些key分成两部分(加上前缀,类似hbase的加盐),两部分分别进行一次聚合的操作,这样何有可能这此次聚合的实时两部分可以由两个ResultTask分别去完成,这样的话执行的速度就会上升.之后再将加的前缀去掉,再进行一次xxxbykey的操作,这样就完成了.一般情况是可以解决数据倾斜的

spark groupby数据倾斜 spark的数据倾斜_spark groupby数据倾斜


好吧搞上一个例子

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


/**
  * 当出现数据倾斜的时候,如果是XXXBykey的shuffle的过程出现的数据倾斜的话可以使用双重聚合的方法进行小狐狸数据倾斜
  * 处理的过程如下:
  * 1.通过sample算子找出数据量最大的key
  * 2.对这个key进行加上一个指定范围内的随机数的前缀
  * 3.做一次xxxByKey的聚合操作
  * 4.将前缀去掉
  * 5.再搞一次xxxByKey的聚合
  */
object _05DoubleTogether {
  def main(args: Array[String]): Unit = {
    var conf = new SparkConf()
      .setAppName("_05DoubleTogether".getClass.getSimpleName)
      .setMaster("local[2]")
    var sc = new SparkContext(conf)
    doubleTogether(sc)
    Thread.sleep(100000)
    sc.stop()
  }

  def doubleTogether(sc: SparkContext): Unit = {
    val list = List(
      "hello hello hello hello you you hello",
      "hello hello hello you you hei hei hello hello hello"
    )
    val listRDD = sc.parallelize(list)
    val pairsRDD: RDD[(String, Int)] = listRDD.flatMap(line => {
      line.split("\\s+")

    }).map((_, 1))
    //第一步找出数据倾斜的key
    val abnormalData: collection.Map[String, Long] = pairsRDD.sample(true, 0.8).countByKey().take(1)
    val abnoramlDataKey: String = abnormalData.head._1
    //第二步,找出所有的异常的key进行加随机的前缀的方式
    val firstTogetherRDD: RDD[(String, Int)] = pairsRDD.map { case (word, num) => {
      var random = new Random()
      if (word == abnoramlDataKey) {
        val pre: Int = random.nextInt(2)
        (s"${pre}_${word}", num)

      } else {
        (word, num)
      }
    }
    }
    //第三步进行局部的聚合
    val firstTogetherResultRDD: RDD[(String, Int)] = firstTogetherRDD.reduceByKey(_ + _)
    //第四去掉前缀word
    val deletePreRDD: RDD[(String, Int)] = firstTogetherResultRDD.map { case ((word, num)) => {
      //注意这里不能这样写,因为当出现没有加上前缀的word的时候word.indexOf("_")的返回值是-1,
      //word.substring(0, word.indexOf("_"))会报错java.lang.StringIndexOutOfBoundsException: String index out of range: -1
      //if (word.substring(0, word.indexOf("_")) == "1" | word.substring(0, word.indexOf("_")) == "0") {
      if (word.contains("_")) {
        // println("**********************************")
        (word.substring(word.indexOf("_") + 1), num)
      } else {
        (word, num)
      }
    }
    }
    //第五  第二次进行聚合
    val ResultRDD: RDD[(String, Int)] = deletePreRDD.reduceByKey(_ + _)
    ResultRDD.foreach { case (word, count) => {
      println(s"${word}---------------->${count}")
    }
    }
  }
}

结果

you---------------->4
hello---------------->11
hei---------------->2

5.使用mapjoin代替reducejoin
这个操作主要是针对join类的聚合操作,多表关联,前提条件是大小表关联。
所谓reduce-join操作就是很直白的调用join算子,执行操作,这个过程是有shuffle的。
所谓map-join操作呢,将小表广播到各个executor,在map类算子中完成关联操作。如果出现一张中表和一张大表的情况,也可以将中表瘦身成小表之后进行mapjoin
这个操作,请问,从根本上解决了数据倾斜了没有?从根本上解决了数据倾斜,因为有map-join代替
reduce-join没有shuffle操作,肯定就没有数据倾斜了。
代码如下:

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

/**
  * 测试广播变量
  * 使用一个案例就是小表关联大表的操作,小表就可以使用广播变量,大表在节点上就可以直接使用广播变量中的小表进行操作了
  */
object _07boradcast {
  def main(args: Array[String]): Unit = {
    val conf=new SparkConf()
      .setAppName("_07boradcast".getClass.getSimpleName)
      .setMaster("local[2]")
    val sc=new SparkContext(conf)
    val resultRDD: RDD[(String, String)] = joinops(sc,"file:///D:/aaa/class.txt","file:///D:/aaa/score.txt")
    resultRDD.foreach(println)

    /**
      * (赵六,100,100,2班)
      * (花花,90,90,2班)
      * (菲菲,40,40,2班)
      * (张三,100,100,1班)
      * (李四,90,90,1班)
      * (王五,50,50,1班)
      */

  }

  /**
    *
    * @param sc           上下文对象
    * @param minTablePath 小表的关联的路径
    * @param bigTablePath 大表的关联的路径
    * class.txt
    *                     张三,1班
    *                     李四,1班
    *                     王五,1班
    *                     赵六,2班
    *                     花花,2班
    *                     菲菲,2班
    *
    *  score.txt
    *                     张三,100,100
    *                     李四,90,90
    *                     王五,50,50
    *                     赵六,100,100
    *                     花花,90,90
    *                     菲菲,40,40
    * 需求:将量表进行关联输出--->姓名,分数,分数,班级
    */
  def joinops(sc:SparkContext,minTablePath:String,bigTablePath:String): RDD[(String, String)] = {
    val fileRDD: RDD[String] = sc.textFile(minTablePath)
    //将读取到的RDD的小表的内容collect到dirver端,之后进行加入广播变量
    val class_map: Map[String, String] = fileRDD.map(line => {
      val lines: Array[String] = line.split("\\,")
      (lines(0), lines(1))
    }).collect.toMap


    //将class的表的数据加入广播变量
    val broadcast_class: Broadcast[Map[String, String]] = sc.broadcast(class_map)
    //加载score.txt的数据
    val scoreRDD: RDD[String] = sc.textFile(bigTablePath)
    val resultRDD: RDD[(String, String)] = scoreRDD.map(line => {
      val lines: Array[String] = line.split("\\,")
      var name = lines(0)
      var math = lines(1)
      var chinese = lines(2)
      //从广播变量中将内容取出来
      val class_stu: Map[String, String] = broadcast_class.value
      var class_info = class_stu.getOrElse(name, null)
      (name, math +","+ chinese +","+ class_info)
    })
    resultRDD

  }

}

6.key进行分拆

如果有两张大表join,一张正常,但是另外一张有key非常的多怎么办呢

思路如图

spark groupby数据倾斜 spark的数据倾斜_数据倾斜_02


具体描述结合代码呈现

import java.util.Random
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.rdd.RDD
import scala.collection.mutable.ArrayBuffer

/**join 操作的是两张大表,分别是A表和B表
  * 但是A表中有一个key特别的多,join的过程中会出现数据的倾斜
  * 怎么办呢?\
  * 思路如下:
  * 1,找到A表中的那个key
  * 2.将这个Key的数据从A表中抽取出来,这样A表变成了两张表A-1(那个key)和A-2,对B表也将这个key的值进行抽取出来B表也变成了B-1(那个key)和B-2两张表
  * 3.将A-1表的key进行随机添加指定范围的前缀,
  * 4.B-1表进行相应倍数的扩容,也就是按照第三步的添加范围进行相应倍数扩容,比如3步随机添加0和1的前缀,那么这一步就扩容2倍
  * 5.A_1和B-1进行join操作  A-2和B-2进行join操作
  * 6.将A_1和B-1进行join操作之后的结果去掉前缀
  * 7.将第6步的结果和A-2和B-2进行join操作结果进行union操作得到最终的结果
  *
  *
  */
object _06TwoBigTabletogether {
  def main(args: Array[String]): Unit = {
    var conf=new SparkConf().setAppName("_06TwoBigTabletogether".getClass.getSimpleName).setMaster("local[2]")
    var sc =new SparkContext(conf)
    toBigTableTogether9(sc)
    sc.stop()
  }
  def toBigTableTogether9(sc:SparkContext): Unit ={
    //数据如下:
    val left = List(
      ("hello", 1),
      ("hello", 2),
      ("hello", 3),
      ("you", 1),
      ("me", 1),
      ("you", 2),
      ("hello", 4),
      ("hello", 5)
    )
    val right = List(
      ("hello", 11),
      ("hello", 12),
      ("you", 11),
      ("me", 12)
    )
    val leftRDD: RDD[(String, Int)] = sc.parallelize(left)
    val rightRDD: RDD[(String, Int)] = sc.parallelize(right)
    //1.找出left表中的异常的key
    val abnormalkey: String = leftRDD.sample(true,0.8).countByKey().take(1).head._1
    //2.将left表中和right表中的key的数据抽取出来形成不同的表
    val leftAbnormalRDD: RDD[(String, Int)] = leftRDD.filter(_._1==abnormalkey)
    val leftNormalRDD: RDD[(String, Int)] = leftRDD.filter{case (word,count)=>{word!=abnormalkey}}
    val rightAbnormalRDD: RDD[(String, Int)] = rightRDD.filter(_._1==abnormalkey)
    val rightNormalRDD: RDD[(String, Int)] = rightRDD.filter{case (word,count)=>{word!=abnormalkey}}
    //3.对A-1进行家加上随机的前缀  0  1
    val leftPreAbnormalRDD: RDD[(String, Int)] = leftAbnormalRDD.map { case (word, count) => {
      var random = new Random()
      (s"${random.nextInt(2)}_${word}", count)
    }
    }
    //4.B-1进行相应的扩容
    val expandRightAbnomalRDD: RDD[(String, Int)] = rightAbnormalRDD.flatMap { case (word, count) => {
      var ab = new ArrayBuffer[(String, Int)]()
      for (i <- 0 to 1) {
        ab.append((s"${i}_${word}", count))
      }
      ab
    }
    }
    //5.A_1和B-1进行join操作  A-2和B-2进行join操作
    val preAbnormalJoinRDD: RDD[(String, (Int, Int))] = leftPreAbnormalRDD.join(expandRightAbnomalRDD)
    val NormalJoinRDD: RDD[(String, (Int, Int))] = leftNormalRDD.join(rightNormalRDD)
    //6.将preAbnormalJoinRDD中的数据的前缀干掉
    val NopreAbnormalRDD: RDD[(String, (Int, Int))] = preAbnormalJoinRDD.map { case (word, count) => {
      (word.substring(2), count)
    }
    }
    //7.进行union
    val ResultRDD: RDD[(String, (Int, Int))] = NopreAbnormalRDD.union(NormalJoinRDD)
    ResultRDD.foreach{case(word,count)=>{
      println(s"${word}------------------>${count}")
    }}
  }
}

运行结果如下:

hello------------------>(2,11)
hello------------------>(1,11)
hello------------------>(2,12)
hello------------------>(1,12)
hello------------------>(3,11)
hello------------------>(5,11)
hello------------------>(3,12)
hello------------------>(5,12)
hello------------------>(4,11)
hello------------------>(4,12)
you------------------>(1,11)
you------------------>(2,11)
me------------------>(1,12)