Spark四種性能調優思路(四)——數據傾斜調優


數據傾斜,英文data skew,就是由於數據分佈不均勻,造成的數據以及任務計算時間有差異,絕大多數task任務執行很快結束,個別task任務執行非常緩慢,如果在mr中接觸過的就應該知道,dataskew的現象就是程序長時間停留在99%的階段,但是不結束

表現形式

  • 個別task運行很慢

    絕大多數task任務執行很快結束,個別task任務執行非常緩慢。一個spark程序執行時間是由最慢的task所決定的。這也是數據傾斜中最常見的現象。

在這裏插入圖片描述

  • 突然OOM(Out of Memory)

    正常運行的作業,突然某一天OOM,分析原因,是由於key的分佈不均勻造成的。

數據傾斜成因

在這裏插入圖片描述

處理數據傾斜的思路

發生數據傾斜的原因是由於在shuffle過程中key的分佈不均勻造成

解決方法的本質就是讓key變均

  1. 找到key

    使用sample算子可以進行抽樣,用樣本空間評估整體,比如抽取10% ,就可以計算出每一個key對應的次數 ,那麼出現次數最多的那些key就是那些發生數據傾斜的key。

  2. 變均勻

    最常用,也是最有用的方法,給這些key加上一個隨機數前綴,進行聚合操作。

  3. 去掉前綴

    基於第二步的結果進行再一次的聚合

優化一:提高shuffle並行度

發生數據傾斜之後,最初的嘗試就是提高shuffle的並行度,shuffle算子有第二個參數,比如reduceByKey(func, numPartitions),這種處理方案不能從根本上解決數據傾斜,但是會在一定程度上減輕數據傾斜的壓力,因爲下游分區數變多,自然每一個分區中的數據,相比較原來就減少了,但是,相同key的數據還是回到一個分區中去,所以如果發生數據傾斜,這種情況下是不可能解決數據傾斜。但是提高shuffle並行度,是解決數據傾斜的第一次嘗試!

在這裏插入圖片描述

優化二:過濾key

如果把這些發生數據傾斜的key幹掉,自然其餘的key都是分佈均勻的,分佈均勻在運行的時候,task運行時間也是相對均勻的,也就不會發生數據傾斜了。但是這種方案沒有從根本上解決數據傾斜,同時大多數傾斜下,發生數據傾斜的這部分key還是蠻有用的,不能直接過濾,大家需要和運營、產品、項目等相關人員進行確認之後纔可進行過濾。

FilterSkewKey.scala

package dataskew

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

/**
  * @Author Daniel
  * @Description 過濾key
  **/
//過濾掉數據傾斜的key
object FilterSkewKey {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
      .setAppName("_01FilterSkewKeyOps")
      .setMaster("local[*]")
    val sc = new SparkContext(conf)
    //模擬數據傾斜的數據
    val list = List(
      "hive spark hadoop hadoop",
      "spark hadoop hive spark",
      "spark spark",
      "spark spark",
      "kafka streaming spark"
    )
    val listRDD = sc.parallelize(list)
    val wordsRDD = listRDD.flatMap(_.split("\\s+"))
    //找到發生數據傾斜的key,使用抽樣算子sample(true有放回抽樣false爲無返回抽樣,抽樣比例取值範圍就是0~1)
    val sampleRDD = wordsRDD.sample(true, 0.8)
    //計數
    val counts = sampleRDD.map((_, 1)).countByKey()
    println("抽樣數據爲:")
    counts.foreach(println)
    //排序拿到出現次數最多的key,即發生數據傾斜的key
    val firstArr = counts.toList
      .sortWith { case ((k1, c1), (k2, c2)) => c1 > c2 }
      .take(1)
    //拿到第一個list中的第一個元素
    val first = firstArr(0)._1
    println("發生數據傾斜的key爲:" + first)
    //在原RDD中過濾掉髮生數據傾斜的key
    wordsRDD.filter(word => word != first)
      .map((_, 1))
      .reduceByKey(_ + _)
      .foreach(println)
    sc.stop()
  }
}

優化三:預處理

在spark階段發生的數據傾斜,是由於數據分佈不均勻造成,而spark加載的數據是從外部的介質拉取過來的,要想讓spark階段不發生dataskew,得讓拉取過來的數據本身就是已經處理完數據傾斜之後結果。

這種方案,對於類似一個java應用需要從spark計算的結果中拉取數據,所以就需要spark做快速的響應,所以如果有數據傾斜現象,就應該將這部分的操作轉移到上游處理,在spark中就沒有這部分shuffle操作,也就不會再有數據傾斜,此時spark相當於數據的快速查詢引擎,通常比如spark從hive,或者hdfs查數據的時候可以使用這種方案,而且效果是非常明顯。

這種方案,從根本上解決了spark階段的數據傾斜,因爲壓根兒就沒有shuffle操作,只不過是把對應的操作提前到前置階段,此時spark就只是利用它的一個特點——快,直接加載外部結果給程序調用者來使用。

優化四:兩階段聚合

  • 分析過程:

    首先需要清楚,這種階段方案主要是針對xxxByKey類的算子造成的數據傾斜。兩階段聚合=局部聚合+全局聚合。

以(hello, 1),(hello, 1),(hello, 1),(hello, 1)爲例

  • 局部聚合:

    給key添加N以內的隨機前綴,這裏比如加2以內,(0_hello, 1),(1_hello, 1),(0_hello, 1),(1_hello, 1),此時進行聚合統計,結果就變爲了(0_hello, 2),(1_hello, 2),這就得到了一個局部的統計結果。

  • 全局聚合:

    在局部聚合的基礎之上,將隨機的前綴幹掉,(hello, 2),(hello, 2),再次進行聚合操作,(hello, 4)。

MergeTwoStage.scala

package dataskew

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

import scala.util.Random

/**
  * @Author Daniel
  * @Description 兩階段聚合
  **/
object MergeTwoStage {
  def main(args: Array[String]): Unit = {
    val conf = new SparkConf()
      .setAppName("MergeTwoStage")
      .setMaster("local[*]")
    val sc = new SparkContext(conf)
    val list = List(
      "hive spark hadoop hadoop",
      "spark hadoop hive spark",
      "spark spark",
      "spark spark",
      "kafka streaming spark"
    )
    val listRDD = sc.parallelize(list)
    val wordsRDD = listRDD.flatMap(_.split("\\s+"))
    val pairsRDD = wordsRDD.map((_, 1))
    //取樣,計數,排序
    val sortedKeys = pairsRDD.sample(true, 0.7)
      .countByKey()
      .toList
      .sortWith(_._2 > _._2)
    println("排序之後的抽樣數據:")
    sortedKeys.foreach(println)
    //拿到排第一的List,取出第一個元素,即發生數據傾斜的key
    val dataSkewKey = (sortedKeys.take(1)) (0)._1
    //打散原始數據
    val newPairsRDD = pairsRDD.map { case (word, count) => {
      if (word == dataSkewKey) { //給數據傾斜的key加上隨機數(0或1)
        val random = new Random()
        (random.nextInt(2) + "_" + word, count)
      } else {
        //其他的不變
        (word, count)
      }
    }
    }
    println("打散之後的RDD數據:")
    newPairsRDD.foreach(println)
    //局部聚合
    val partAggr = newPairsRDD.reduceByKey(_ + _)
    println("局部聚合之後:")
    partAggr.foreach(println)
    //全局聚合
    val fullAggr = partAggr.map { case (prefixKey, count) => {
      if (prefixKey.contains("_")) {
        //去掉前綴
        (prefixKey.substring(prefixKey.indexOf("_") + 1), count)
      } else {
        (prefixKey, count)
      }
    }
    }
      .reduceByKey(_ + _)
    println("全局聚合之後的結果:")
    fullAggr.foreach(println)
    sc.stop()
  }
}

優化五:分拆進行join

  • join的分類

    分爲了map-join和reduce-join,這也是mr中的join分類。對於join的操作自然就從這兩個出發點去處理。map-join適合處理大小表關聯。reduce-join適合處理兩張大表關聯。

    對於map-join,就是大小表關聯,可以將小表加載到廣播變量中,和大表是用map類的算子完成關聯,這樣在程序中就不會出現join操作,便不會有shuffle,因此就不會出現數據傾斜。

    這個案例在Spark四種性能調優思路(一)——開發調優中優化四:量避免使用shuffle類算子出現過

    MapJoin.scala

    package optimization
    
    import org.apache.spark.broadcast.Broadcast
    import org.apache.spark.{SparkConf, SparkContext}
    
    /**
      * @Author Daniel
      * @Description 使用map+廣播變量代替join操作
      *
      **/
    object MapJoin {
      def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
          .setAppName(s"${MapJoin.getClass.getSimpleName}")
          .setMaster("local[*]")
    
        val sc = new SparkContext(conf)
        unJoinOps(sc)
    
        sc.stop()
      }
    
      //藉助map類的算子和廣播變量相當於mapreduce中的map join
      def unJoinOps(sc: SparkContext): Unit = {
        //sid, name, age, gender
        val stuMap = List(
          "1,Jacob,19,male",
          "2,William,20,male",
          "3,Emily,21,female",
          "4,Daniel,20,male",
          "5,Olivia,31,female"
        ).map(line => {
          val index = line.indexOf(",")
          (line.substring(0, index), line.substring(index + 1))
        }).toMap
    
        //轉化爲廣播變量
        val stuBC: Broadcast[Map[String, String]] = sc.broadcast(stuMap)
    
        //sid, course, score
        val scoreRDD = sc.parallelize(List(
          "1,Math,88",
          "2,Chinese,75",
          "3,English,87",
          "4,Math,100",
          "6,Chinese,77"
        ))
        scoreRDD.map(line => {
          val index = line.indexOf(",")
          //sid
          val id = line.substring(0, index)
          //course, score
          val otherInfo = line.substring(index + 1)
          //拿到廣播變量中的id字段
          val baseInfo = stuBC.value.get(id)
          //如果id被定義(即有值)
          if (baseInfo.isDefined) {
            //拼接
            (id, baseInfo.get + "," + otherInfo)
          } else {
            //否則設置爲Null值
            (id, null)
          }
          //過濾掉Null值
        }).filter(_._2 != null).foreach(println)
      }
    }
    

    如何進行大表關聯

  • 思路
    上述的兩階段聚合,並不能夠解決掉join類的shuffle,要想處理join類的shuffle,使用這種所謂的分拆數據傾斜key,並進行擴容join操作。

在這裏插入圖片描述

  • 代碼實現

    package dataskew
    
    import java.util.Random
    
    import org.apache.spark.{SparkConf, SparkContext}
    
    import scala.collection.mutable.ArrayBuffer
    
    /**
      * @Author Daniel
      * @Description 分拆join表數據進行關聯
      **/
    object SplitJoin {
      def main(args: Array[String]): Unit = {
        val conf = new SparkConf()
          .setAppName(s"${SplitJoin.getClass.getSimpleName}")
          .setMaster("local[*]")
        val sc = new SparkContext(conf)
        val random = new Random()
        val left = sc.parallelize(List(
          ("spark", "left0"),
          ("spark", "left1"),
          ("spark", "left2"),
          ("spark", "left3"),
          ("spark", "left4"),
          ("spark", "left5"),
          ("hadoop", "left"),
          ("mapreduce", "left"),
          ("rdd", "left")
        ))
    
        val right = sc.parallelize(List(
          ("spark", "right0"),
          ("spark", "right1"),
          ("hadoop", "right"),
          ("mapreduce", "right")
        ))
        //可以看到左表中key有傾斜,右表正常,所以首先在左表中找到傾斜的key
        val sortedKeys = left.sample(true, 0.7)
          .countByKey()
          .toList
          .sortWith(_._2 > _._2)
        println("排序之後的抽樣數據:")
        sortedKeys.foreach(println)
        val dataSkewKey = (sortedKeys.take(1)) (0)._1
        //拆分左右兩張表,進行單獨處理
        val leftDSRDD = left.filter { case (key, value) => key == dataSkewKey }
        val leftNormalRDD = left.filter { case (key, value) => key != dataSkewKey }
        val rightDSRDD = right.filter { case (key, value) => key == dataSkewKey }
        val rightNormalRDD = right.filter { case (key, value) => key != dataSkewKey }
        //先處理normal數據
        val normalJoinedRDD = leftNormalRDD.join(rightNormalRDD)
        println("正常數據進行join之後的結果: ")
        normalJoinedRDD.foreach(println)
        //打散左表異常數據,加上隨機數
        val leftPrefixDSRDD = leftDSRDD.map { case (key, value) => {
          val prefix = random.nextInt(3) + "_"
          (prefix + key, value)
        }
        }
        //使用flatMap將右表對應異常數據進行擴容
        val rightPrefixDSRDD = rightDSRDD.flatMap { case (key, value) => {
          val ab = ArrayBuffer[(String, String)]()
          //使每個結果均勻分佈
          for (i <- 0 until 3) {
            ab.append((i + "_" + key, value))
          }
          ab
        }
        }
        //異常數據進行join
        val prefixJoinedDSRDD = leftPrefixDSRDD.join(rightPrefixDSRDD)
        println("異常rdd進行join之後的結果:")
        prefixJoinedDSRDD.foreach(println)
        //去掉前綴
    val dsJoinedRDD = prefixJoinedDSRDD.map { case (prefix, value) => {
          (prefix.substring(prefix.indexOf("_") + 1), value)
    }
        }
        //異常數據與正常數據union
        val finalRDD = dsJoinedRDD.union(normalJoinedRDD)
        println("最終的join結果:")
        finalRDD.foreach(println)
        sc.stop()
      }
    }
    

    上面的join操作,僅僅針對一張表正常,一張表少部分異常,大部分正常的情況

    如果加入左表大部分的key都有傾斜的情況,右表正常,此時的處理方式就不適用了。因爲此時,有傾斜的數據佔大部分,所以分拆的效果也不明顯,左表就得全量添加隨機前綴,右表全量擴容。顯然對內存資源要求非常高,很容易出現OOM異常。

總結:

如果只是處理較爲簡單的數據傾斜場景,那麼使用上述方案中的某一種基本就可以解決。但是如果要處理一個較爲複雜的數據傾斜場景,那麼可能需要將多種方案組合起來使用。比如我們可以同時在提高shuffle並行度的同時,過濾掉key這樣雙管齊下可以更好的解決開發中遇到的問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章