目录
什么时候出现了数据倾斜?
- 绝大多数task执行得都非常快,但个别task执行极慢。比如,总共有1000个task,997个task都在1分钟之内执行完了,但是剩余两三个task却要一两个小时。这种情况很常见。
- 原本能够正常执行的Spark作业,某天突然报出OOM(内存溢出)异常,观察异常栈,是我们写的业务代码造成的。这种情况比较少见。
spark web ui可以看到task中的数据和时间
数据倾斜原因
数据倾斜的原理很简单:在进行shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个task来进行处理,比如按照key进行聚合或join等操作。此时如果某个key对应的数据量特别大的话,就会发生数据倾斜
场景及解决方法
一:使用Hive ETL预处理数据
适用场景
导致数据倾斜的是Hive表,如果该Hive表中的数据本身很不均匀(比如某个Key对应了100万条数据,其他key才对应了10条数据),而且业务场景需要频繁使用Spark对Hive表执行某个分析操作。
方案实现
如果spark处理hive表,其中hive表中数据不均,并且要求spark处理要足够快时,可将hive做ETL预处理,将shuffle操作提前到了HIVE ETL
优缺点
优点:实现起来简单便捷,完全规避掉了数据倾斜,spark作业的性能会大幅度提升
缺点:治标不治本,Hive ETL中还是会发生数据倾斜
二:过滤少量导致倾斜的key
适用场景
当发现导致倾斜的key就少数几个,并且对作业的执行和计算结果不是很重要的话,就干脆直接过滤掉那几个key
方案实现
比如:在spark sql中使用where自居过滤掉这些key,或者spark rdd中执行filter算子过滤掉这些key
如果需要每次作业执行时,动态判定哪些key的数量最多然后进行过滤,那么可以使用sample算子对RDD进行采样,然后计算出每个key的数量,取数据量最多的key过滤掉即可
优缺点
优点:实现简单,效果好
缺点:适用场景不多,大都数情况下,导致倾斜的key还是很多的,并不是只有少数几个
三:提高shuffle并行度
适用场景
如果必须要对数据倾斜迎难而上,那么建议有限使用这种方案,因为这是处理数据倾斜最简单的一种方案
方案实现
reduceByKey(1000),groupByKey(48) 增大shuffle时task数量
使用spark sql的话,使用
spark.conf.set("spark.sql.shuffle.partitions",500) //shuffle read task ,默认是200
优缺点
优点:提高shuffle操作得并行度,根据hash算的哪个key放哪个分区,当分区多时,就能减轻数据倾斜。
缺点:但是对于某个key多分到一个分区,无法解决。
流程图
四:两阶段聚合(聚不聚和+全局聚合)
方案适用场景
对RDD执行reduceByKey等聚合类shuffle算子或者在spark sql中使用group by语句进行分组聚合时,比较适用
优缺点
优点:对于聚合类的shuffle操作导致的数据倾斜,效果非常不错,spark作业性能提升数倍以上
缺点:只适用聚合类shuffle操作,如果时join类的shuffle操作,还得用其他的解决方案
流程图
代码
package com.cy
import org.apache.spark.{SparkConf, SparkContext}
import scala.util.Random
object DataSkew {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("testRdd");
sparkConf.set("spark.default.parallelism", "2")
val sc = new SparkContext(sparkConf);
val rdd=sc.makeRDD(List(("hello",1),("hello1",1),("hello",1),("hello",1)))
val stageOne= rdd.map(x=>
{
val prifix=new Random().nextInt(2)
(prifix+"_"+x._1,x._2)
})
val newRdd=stageOne.reduceByKey(_+_)
val stageTwo=newRdd.map(x=>(x._1.split("_")(1),x._2))
stageTwo.reduceByKey(_+_).foreach(println)
}
}
五:将reduce join变为map join
适用场景
当一个表中的数据只有几百M或者1、2G
方案实现原理
普通的join会产生shuffle过程,就相当于会将相同key的数据拉取到一个shuffle read task中,再进行join,此时就是reduce join
但是如果一个RDD是比较小的,则可以采用广播小RDD全量数据+map算子来实现join同样的效果,也就是map join,此时就不会发生shuffle操作,也就不会发生数据倾斜
缺点
缺点: 值适用于其中一个表数据比较小的情况,不适用两个都是大表的情况
六:采样倾斜key并分拆join操作
适用场景
两个RDD/Hive表进行join的时候,如果数据量都比较大,无法采用”解决方案五“,那么此时可以看一下两个RDD/Hive表中的key分布情况。
如果出现数据倾斜,是因为其中某一个RDD/Hive表中的少数几个key的数据量过大,而另一个RDD/Hive表中的所有key都分布比较均匀,那么采用这个解决方案是比较合适的。
优缺点
优点:对于join导致的数据倾斜,如果只是某几个key导致了倾斜,采用该方式可以用最有效的方式打散key进行join。而且只需要针对少数倾斜key对应的数据进行扩容n倍,不需要对全量数据进行扩容。避免了占用过多内存。
缺点:如果导致倾斜的key特别多的话,比如成千上万个key都导致数据倾斜,那么这种方式也不合适
流程
代码
数据1:text.txt
001 hello 001 hello 001 hello 002 hello 002 hello 003 hello 003 hello 003 hello 003 hello 003 hello 003 hello
数据2:text1.txt
001 wawa01 002 wawa02 003 wawa03
inner join代码
package com.cy
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.util.Random
case class AdClickLog(c1:String,c2:String)
case class Skew(c1:String,c2:String,c3:String)
object SparkDataSkewDemo {
def main(args: Array[String]): Unit = {
val userId = null
val spark = SparkSession
.builder()
.master("local[*]")
.appName("SparkSessionZipsExample")
.getOrCreate()
val sc=spark.sparkContext
import spark.implicits._
val buffer= new ArrayBuffer[String]()
for (i<-1 to 3){
buffer.+=(i+"")
}
val df = spark.read.textFile("D:\\spark-project\\src\\main\\resources\\text.txt")
val df1 = spark.read.textFile("D:\\spark-project\\src\\main\\resources\\text1.txt")
val ds = df.map(x => {
val row = x.split(" ")
AdClickLog(row(0), row(1))
})
val ds1 = df1.map(x => {
val row = x.split(" ")
AdClickLog(row(0), row(1))
})
val id = ds.sample(false, 0.5)
.map(x => (x.c1, 1)).groupByKey(_._1).count().sort($"count(1)".desc).rdd.take(1)
val bc=sc.broadcast(id)
val bcBuffer=sc.broadcast(buffer)
val commonRDD = ds.filter(x => !x.c1.contains("003"))
val skewRDD=ds.filter(x => x.c1.contains("003")).map(x=>{
val random=new Random().nextInt(3)
(random+1+"_"+x.c1,x.c2)
})
//skewRDD.show()
val skewRDD1=ds1.filter(x => x.c1.contains("003")).flatMap(x=>{
bcBuffer.value.map(y=>(y+"_"+x.c1,x.c2))
})
// skewRDD1.show()
val skewJoinRdd:DataFrame=skewRDD.join(skewRDD1,Seq("_1"),"inner")
val skewJoinRdd1=skewJoinRdd.map(row=>{
Skew(row(0).toString.split("_")(1),row(1).toString,row(2).toString)
})
// skewJoinRdd1.show()
val joinRDD2=commonRDD.join(ds1,Seq("c1"), "inner")
//
commonRDD.join(ds1, Seq("c1"), "inner").show()
skewJoinRdd1.toDF().union(joinRDD2).show()
}
}
七:使用随机前缀和扩容RDD进行join
适用场景
如果在进行join操作时,RDD中有大量的key导致数据倾斜,那么进行分拆key也没什么意义,此时就只能适用最后一种方案来解决问题了
实现原理
与方案“六”类似,首先查看RDD/Hive表中的数据分布情况,找到那个造成数据倾斜的RDD/Hive表,比如有多个key都对应了超过1万条数据。
然后将该RDD的每条数据都打上一个n以内的随即前缀
同时对另外一个正常的RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都一次打上一个0~n的前缀。
方案优缺点
优点:对join类型的数据倾斜基本都可以处理,而且效果也相对比较显著,性能提升效果非常不错。
缺点:该方案更多的是缓解数据倾斜,而不是彻底避免数据倾斜。而且需要对整个RDD进行扩容,堆内存资源要求很高
八:多种方案组合使用
多数情况下,复杂的数据倾斜场景,可能需要将多种方案组合使用
根据业务场景,使用1-7方案来组合使用
创作不易,感谢支持,您的支持是我创作最大的动力