为什么要处理数据倾斜问题
什么是数据倾斜
对Spark/Hadoop这样的大数据系统来讲,数据量大并不可怕,可怕的是数据倾斜。
何谓数据倾斜?数据倾斜指的是,并行处理的数据集中,某一部分(如Spark或Kafka的一个Partition)的数据显著多于其它部分,从而使得该部分的处理速度成为整个数据集处理的瓶颈。
数据倾斜原因
在Spark中,同一个Stage的不同Partition可以并行处理,而具有依赖关系的不同Stage之间是串行处理的。假设某个Spark Job分为Stage 0和Stage 1两个Stage,且Stage 1依赖于Stage 0,那Stage 0完全处理结束之前不会处理Stage 1。而Stage 0可能包含N个Task,这N个Task可以并行进行。如果其中N-1个Task都在10秒内完成,而另外一个Task却耗时1分钟,那该Stage的总时间至少为1分钟。换句话说,一个Stage所耗费的时间,主要由最慢的那个Task决定。
由于同一个Stage内的所有Task执行相同的计算,在排除不同计算节点计算能力差异的前提下,不同Task之间耗时的差异主要由该Task所处理的数据量决定。
- 数据源数据文件不均匀
- 计算过程中key的分布不均
- 单个rdd中进行groupby 的时候key分布不均
- 多个rdd进行join过程中key的不均匀
如何缓解/消除数据倾斜
以Spark Stream通过DirectStream方式读取Kafka数据为例。由于Kafka的每一个Partition对应Spark的一个Task(Partition),所以Kafka内相关Topic的各Partition之间数据是否平衡,直接决定Spark处理该数据时是否会产生数据倾斜。
原理
Spark在做Shuffle时,默认使用HashPartitioner(非Hash Shuffle)对数据进行分区。如果并行度设置的不合适,可能造成大量不相同的Key对应的数据被分配到了同一个Task上,造成该Task所处理的数据远大于其它Task,从而造成数据倾斜。
如果调整Shuffle时的并行度,使得原本被分配到同一Task的不同Key发配到不同Task上处理,则可降低原Task所需处理的数据量,从而缓解数据倾斜问题造成的短板效应。在此引用一张图来阐述:
案例
现有一张测试表,名为student_external,内有10.5亿条数据,每条数据有一个唯一的id值。现从中取出id取值为9亿到10.5亿的共1.5条数据,并通过一些处理,使得id为9亿到9.4亿间的所有数据对12取模后余数为8(即在Shuffle并行度为12时该数据集全部被HashPartition分配到第8个Task),其它数据集对其id除以100取整,从而使得id大于9.4亿的数据在Shuffle时可被均匀分配到所有Task中,而id小于9.4亿的数据全部分配到同一个Task中。处理过程如下
INSERT OVERWRITE TABLE test
SELECT CASE WHEN id < 940000000 THEN (9500000 + (CAST (RAND() * 8 AS INTEGER)) * 12 )
ELSE CAST(id/100 AS INTEGER)
END,
name
FROM student_external
WHERE id BETWEEN 900000000 AND 1050000000;
通过上述处理,一份可能造成后续数据倾斜的测试数据即以准备好。接下来,使用Spark读取该测试数据,并通过groupByKey(12)
对id分组处理,且Shuffle并行度为12。代码如下
public class SparkDataSkew {
public static void main(String[] args) {
SparkSession sparkSession = SparkSession.builder()
.appName("SparkDataSkewTunning")
.config("hive.metastore.uris", "thrift://hadoop1:9083")
.enableHiveSupport()
.getOrCreate();
Dataset<Row> dataframe = sparkSession.sql( "select * from test");
dataframe.toJavaRDD()
.mapToPair((Row row) -> new Tuple2<Integer, String>(row.getInt(0),row.getString(1)))
.groupByKey(12)
.mapToPair((Tuple2<Integer, Iterable<String>> tuple) -> {
int id = tuple._1();
AtomicInteger atomicInteger = new AtomicInteger(0);
tuple._2().forEach((String name) -> atomicInteger.incrementAndGet());
return new Tuple2<Integer, Integer>(id, atomicInteger.get());
}).count();
sparkSession.stop();
sparkSession.close();
}
}
本次实验所使用集群节点数为4,每个节点可被Yarn使用的CPU核数为16,内存为16GB。使用如下方式提交上述应用,将启动4个Executor,每个Executor可使用核数为12(该配置并非生产环境下的最优配置,仅用于本文实验),可用内存为12GB。
spark-submit --queue ambari --num-executors 4 --executor-cores 12 --executor-memory 12g --class com.jasongj.spark.driver.SparkDataSkew --master yarn --deploy-mode client SparkExample-with-dependencies-1.0.jar
GroupBy Stage的Task状态如下图所示,Task 8处理的记录数为4500万,远大于(9倍于)其它11个Task处理的500万记录。而Task 8所耗费的时间为38秒,远高于其它11个Task的平均时间(16秒)。整个Stage的时间也为38秒,该时间主要由最慢的Task 8决定。
在这种情况下,可以通过调整Shuffle并行度,使得原来被分配到同一个Task(即该例中的Task 8)的不同Key分配到不同Task,从而降低Task 8所需处理的数据量,缓解数据倾斜。
Spark在处理数据倾斜的问题上也设计了几个重要的函数,用于消除或缓解数据倾斜的问题,例如:再分区函数(Reptition),减少分区函数(coalesce),groupByKey(partition)
等,该函数主要作用是对当前分区进行调整,在spark中,一个task对应一个partition,重新调整partition个数,实质上是调整task数。
除此之外,spark分为Transformation和action两种类型,transformation阶段为懒加载模式,如果没有action的触发,transformation结果不会主动触发,利用transformation该特性,可以在数据未触发action前,通过RDD函数的组合,将现有分区打散,以达到各分区负载均衡的目的。具体的打散操作有很多,需根据现实业务情况来进行组合,在此举一经典案例(wordCount)进行说明。在上一篇博客中((七)Spark实战之 wordCount ),以WordCount这一案例做了代码的详细设计和展示,在此不做具体的阐述,如有不清楚地方,可以参考该章节代码;
首先,通过sc.textFile("d:/scala//test.txt");函数获取了文本中的行数据信息;
其次,通过RDD.flatMap(Function)函数对行数据进行按切割符进行切分,产生一个打散的word RDD;
然后,mapToPair(Function)函数将word RDD进行了转换,形成(word,1)RDD;
最后,利用ReduceByKey(Function)函数将相同Word的value进行累加,得到result RDD;
如果我们的文本足够大,且某些单次的重复率较高时,在reduceByKey()阶段将会出现热点问题,造成数据倾斜。此时就需要我们对现有的(word,1)进行打散再聚合的操作。具体操作如下:
1.通过mapToPair(Function)函数,获取(k.v)对中的k.为每个key添加一个随机数(如:word---> word_123243),此时,相同word因为后边跟的随机数不同,会被均分到不同的分区中。
2.对该word_*****进行计算后,重新利用mapToPair(Function)函数,再将word还原。
3.在此执行ReduceByKey函数,从而实现打散聚合,规避数据倾斜。