數據傾斜,英文data skew,就是由於數據分佈不均勻,造成的數據以及任務計算時間有差異,絕大多數task任務執行很快結束,個別task任務執行非常緩慢,如果在mr中接觸過的就應該知道,dataskew的現象就是程序長時間停留在99%的階段,但是不結束
表現形式
-
個別task運行很慢
絕大多數task任務執行很快結束,個別task任務執行非常緩慢。一個spark程序執行時間是由最慢的task所決定的。這也是數據傾斜中最常見的現象。
-
突然OOM(Out of Memory)
正常運行的作業,突然某一天OOM,分析原因,是由於key的分佈不均勻造成的。
數據傾斜成因
處理數據傾斜的思路
發生數據傾斜的原因是由於在shuffle過程中key的分佈不均勻造成
解決方法的本質就是讓key變均
-
找到key
使用sample算子可以進行抽樣,用樣本空間評估整體,比如抽取10% ,就可以計算出每一個key對應的次數 ,那麼出現次數最多的那些key就是那些發生數據傾斜的key。
-
變均勻
最常用,也是最有用的方法,給這些key加上一個隨機數前綴,進行聚合操作。
-
去掉前綴
基於第二步的結果進行再一次的聚合
優化一:提高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這樣雙管齊下可以更好的解決開發中遇到的問題