数据倾斜是大数据处理无法规避的问题,大数据开发者都必须具备处理数据倾斜的思维和能力。
大数据采用分而治之、分布式并行处理大数据集,要想得到最好的处理性能,数据应该均衡的分布到集群各个计算节点上,这样才能真正实现N个节点提升N倍性能。
现实是,绝大多数情况下,业务数据是不均衡的,极有可能导致大部分数据被少数几个节点处理,而整个集群的性能是由最后执行完成的任务决定的。
所以一旦出现了数据倾斜,不仅整个作业的性能达不到预期,甚至由于数据的网络和磁盘IO,导致集群执行的性能还不如单机执行的奇怪现象。
数据倾斜是由于数据分布的不均衡导致的,处理数据倾斜要先弄清楚数据倾斜是怎么出现的。
节点读取数据可以分为两类:
- 一是Hdfs、Kafka这里数据存储系统和消息引擎
- 二是Shuffle Write的中间结果
第一部分 数据倾斜重现
一,数据源导致的数据倾斜
1,不可切分的压缩文件导致的数据倾斜
1.1,随机生成一些文件,上传到HDFS:
随机生成文件的代码,可以先生成一个1000万单词的文件,然后通过cat file1.txt >> file2.txt
的方式快速生成大文件:
#!/bin/bash
#randomWords.sh
#词典文件所在路径,linux自带
filepath=/usr/share/dict/words
#生成的结果文件
resultFile=./result.txt
#词典文件中总共有多少个单词
totalWordsNum=`wc -l $filepath | awk '{print $1}'`
idx=1
#NUM为要生成的随机单词的个数,命令行确定生成多少个单词
NUM=$1
declare -i num
declare -i randNum
ratio=37
while [ "$idx" -le "$NUM" ]
do
a=$RANDOM
num=$(( $a*$ratio ))
randNum=`expr $num%$totalWordsNum`
echo $randNum
// 取文件的第$randNum行
sed -n "$randNum"p $filepath >> $resultFile
idx=`expr $idx + 1`
done
有两个文件明显大于其他文件,其中有一个gz格式的压缩文件,压缩之前大约是4G,压缩之后是1.98G。
1.2,使用Spark对文件中的单词进行计数:
package com.asinking.app.test
import org.apache.spark.sql.{Dataset, SparkSession}
object SkewTestApp {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder
.appName("SkewTestApp")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.debug.maxToStringFields", "100")
// .master("local[*]")
.getOrCreate
spark.sparkContext.setLogLevel("warn")
import spark.implicits._
val wordsDf = spark.read
.text("hdfs://node1:8020/skew/words/")
.map(row => row.getAs[String]("value")->1)
.selectExpr("_1 as word","_2 as num")
.groupBy("word")
.count()
.selectExpr("word","cast(count as string) as cnt")
wordsDf.printSchema()
wordsDf.write.parquet("hdfs://node1:8020/skew/res/")
}
}
1.3,将任务提交到yarn-重现数据倾斜
如下图,明显出现了数据倾斜,大多数任务的分区分件约为128M,30秒内任务执行完成,有一个任务的文件为1.7G,执行了4分钟还未完成。
这种情况下,要解决数据倾斜,有两种方案:
- 一是,不压缩文件
- 二是,使用可以切分的压缩文件格式,如lzo
2,Kafka导致的数据倾斜
解决方法:
- 1,重新设计Key或者自定义分区器,是的分区数据均衡
- 2,在代码中重分区
二,Join时某个key的数据量特别大导致的倾斜
两个表Join,相同的Key会被Shuffle到一个Reduce的Partition进行处理,如果有一个Key的数据特别多,就会出现数据倾斜。
一份摇号数据(数据来自极客时间,点此从网盘进行下载,提取码为 ajs6。),只有两个字段batchNum/carNum,对其进行处理,使其出现一个数据量特别大的Key。
处理的逻辑是:
- 1,读取数据
- 2,按分区逐行新增字段index,每个分区内前500W行idx=1,500W行后的数据idx=行数
代码如下:
val rddRows: RDD[Row] = spark.read.parquet("hdfs://HDF501/data/apply/")
.rdd.mapPartitions(itr => {
var i = 0;
itr.map(row => {
i = i + 1
var num = i;
if (i < 5000000) {
num = 1
}
Row.fromSeq(Seq(
row.getAs[Int]("batchNum"),
row.getAs[String]("carNum"),
num
))
})
})
之后,用这份数据自关联:
spark.createDataFrame(rddRows,structType).createOrReplaceTempView("temp_table")
spark.sql("select t1.idx,count(1) as rn_cnt from temp_table t1 join temp_table t2 on t1.idx = t2.idx group by t1.idx")
.write
.parquet("hdfs://HDFS16501/data/res/apply/tmp/")
在yarn上可以看到明显的数据倾斜,设置出现了任务执行失败重试的情况:
当然上面的例子比较极端,同表join导致数据指数倍增长,以至于Task执行失败。
接下来采取另外一种处理方式:
- 1,将数据集转换为键值对结构(idx,carNum)
- 2,对上一步的结构groupByKey,导致大量key为1的数据被shuffle到一个partition,出现数据倾斜
import spark.implicits._
val l: Long = spark.sql("select * from temp_table")
.rdd
.map(row => row.getAs[Int]("idx") -> row.getAs[String]("carNum"))
.groupByKey(12)
.map(row=>{
row._2.iterator.map(
carNum => carNum -> row._1
)
})
.count()
如下图,大多数Task在秒级别完成,但是其中一个任务执行了4分钟还未完成。
三,Join时多个数据量大的key导致的数据倾斜
用如下代码生成一个具有3亿行的数据集,大约4G,将其写入Hdfs。
生成数据:
public static void main(String[] args) throws IOException {
String lineSeparator = (String) java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
File f = new File("data/data.txt");
BufferedWriter bw = new BufferedWriter(new FileWriter(f));
StringBuffer sb = new StringBuffer();
for(int i = 0 ; i < 300000000; i++) {
sb.append(String.format("%s,%s%s",""+i,""+i,lineSeparator));
if (i%1000000 == 0) {
bw.write(sb.toString());
sb = new StringBuffer();
bw.flush();
}
}
bw.close();
}
写入HDFS:
System.setProperty("HADOOP_USER_NAME","hadoop");
val spark = SparkSession
.builder
.appName("SkewTestApp")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.debug.maxToStringFields", "100")
.master("local[*]")
.getOrCreate
spark.sparkContext.setLogLevel("warn")
spark
.read
.text("file:///C:\\Users\\dell\\IdeaProjects\\costcenter\\data\\data.txt")
.write
.parquet("hdfs://HDFS/data/skew_data")
““重现数据倾斜:””
做了如下处理:
- 大表:对ID小于800W的数据的ID统一变更为48或者96,500W~1000W的id均匀的变更为1 ~ 100
- 小表:取前50W数据,id均匀的变更为1 ~ 100
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME","hadoop");
val spark = SparkSession
.builder
.appName("SkewTestApp")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.debug.maxToStringFields", "100")
// .master("local[*]")
.getOrCreate
spark.sparkContext.setLogLevel("warn")
import org.apache.spark.sql.functions._
spark
.read
.parquet("hdfs://HDFS/data/skew_data")
.select(split(col("value"),",").as("splitCol"))
.select(
col("splitcol").getItem(0).cast(IntegerType).as("id"),
col("splitcol").getItem(1).as("name"))
.createTempView("tmp")
spark.sql(
s"""
|SELECT CAST(CASE WHEN id < 8000000 THEN ((CAST (RAND() * 2 AS INT) + 1) * 48 ) ELSE CAST(id/100 AS INT) END AS STRING) as id,
| name
|FROM tmp
|WHERE id < 50000000;
|""".stripMargin)
.createTempView("tbl_big")
spark.sql(
s"""
|SELECT CAST(CAST(id/100 AS INT) AS STRING) as id,
| name
|FROM tmp
|WHERE id < 500000
|""".stripMargin)
.createTempView("tbl_small")
spark.sql(
s"""
| select b.id,b.name from tbl_big b
| left join tbl_small s
| on b.id = s.id
|""".stripMargin)
.write.parquet("hdfs://HDFS/data/skew_data_res")
}
大小表Join时,由于大表ID为48和96的分别有400W,比其他数据多,有两个Task会出现数据倾斜。如下图所示,大多数任务4秒级完成,有两个任务却需要3.7分钟。
第二部分 数据倾斜解决方案
一,数据源导致的数据倾斜
对于数据源导致的数据倾斜,有两种思路:
- ①调整数据源,使得数据源数据量分布均匀
- ②无法调整数据源的情况下,读取数据后重分区
1,Kafka
- ①生产者在生产消息时,定义好key的规则,使得消息均匀分布
- ②读取数据后重分区,如spark的repartition算子,Hive的distributed by
2,HDFS
- ①一般导致数据倾斜的都是不可分割的压缩文件,可以不压缩或者可以切分的压缩格式如lzo
- ②读取数据后重分区,如spark的repartition算子,Hive的distributed by
二,Shuffle-Join导致的数据倾斜
1,广播解决数据倾斜
1.1 准备数据。利用上面创建的数据派生出两份数据,一份5000万条,一份50万条。
对于5000W的这份数据处理逻辑和上面《三,Join时多个数据量大的key导致的数据倾斜》相似:
- id小于800W数据的ID转为48和96
- 800W后的数据的ID对100取模,均匀分布
- 处理后的数据重分区,使得相同的id均匀分布到各个分区
对于50W的这份数据处:
- ID对100取模,均匀分布
spark
.read
.parquet("hdfs://HDFS16501/data/skew_data")
.select(split(col("value"),",").as("splitCol"))
.select(
col("splitcol").getItem(0).cast(IntegerType).as("id"),
col("splitcol").getItem(1).as("name"))
.createTempView("tmp")
spark.sql(
s"""
|SELECT id as old_id, CAST(CASE WHEN id < 8000000 THEN ((CAST (RAND() * 2 AS INT) + 1) * 48 ) ELSE CAST(id/100 AS INT) END AS STRING) as id,
| name
|FROM tmp
|WHERE id < 50000000;
|""".stripMargin)
.createTempView("tbl_big")
spark
.sql("select * from tbl_big")
.repartition(8)
.write
.parquet("hdfs://HDFS16501/data/skew_data_eg/big/")
spark.sql(
s"""
|SELECT CAST(CAST(id/100 AS INT) AS STRING) as id,
| name
|FROM tmp
|WHERE id < 500000
|""".stripMargin)
.createTempView("tbl_small")
spark.
sql("select * from tbl_small")
.write
.parquet("hdfs://HDFS16501/data/skew_data_eg/small/")
1.2,重现数据倾斜
对上面两份数据进行join:
spark
.read
.parquet("hdfs://HDFS16501/data/skew_data_eg/big/")
.createTempView("tbl_big")
spark
.read
.parquet("hdfs://HDFS16501/data/skew_data_eg/small/")
.createTempView("tbl_small")
spark.sql(
s"""
| select b.id,b.name from tbl_big b
| left join tbl_small s
| on b.id = s.id
|""".stripMargin)
.write
.parquet("hdfs://HDFS16501/data/skew_data_res")
从下图可以看出,出现了明显的数据倾斜:
1.3,广播解决数据倾斜
两表JOIN时,广播小表,广播的方式:
- ①配置广播阈值,spark引擎会自动调整join策略,满足条件时会自动启用广播
.config("spark.sql.autoBroadcastJoinThreshold","2073741824")
- ②使用SQL hint,注意如果表名有取alias_name,BROADCAST时要使用alias_name
select /*+ BROADCAST(s) */ b.id,b.name from tbl_big b
spark
.read
.parquet("hdfs://HDFS16501/data/skew_data_eg/big/")
.createTempView("tbl_big")
spark
.read
.parquet("hdfs://HDFS16501/data/skew_data_eg/small/")
.createTempView("tbl_small")
spark.sql(
s"""
| select /*+ BROADCAST(s) */ b.id,b.name from tbl_big b
| left join tbl_small s
| on b.id = s.id
|""".stripMargin)
.write
.parquet("hdfs://HDFS16501/data/skew_data_res")
从图中可以看出,每个任务耗费的事件差距减小:
需要注意的是:这里有40个任务,只有8个任务有数据。原因是spark-submit时指定并行度是40,但数据源只有8个分区。
2,大key加盐处理数据倾斜
加盐听起来牛逼,其实是个唬人的名词,所谓加盐就是给key加上指定范围的随机数,大key就会被均匀的分配到各个Reduce Task。
加盐的重点:
- ①对大key加上指定范围的随机数前缀(如0-10)
key1----------->0_key1
key2----------->3_key2
key3----------->8_key2
…
- ②右表(小表)每个key加上固定范围的前缀(0-10),扩大10倍
key1----------->0_key1
key1----------->1_key1
key1----------->2_key1
key1----------->3_key1
key1----------->4_key1
key1----------->5_key1
key1----------->6_key1
key1----------->7_key1
key1----------->8_key1
key1----------->9_key1
key2----------->0_key2
key2----------->2_key2
…
- ③单独处理大key和大key之外的数据,将两部分结果union
参考代码:
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME","hadoop");
val spark = SparkSession
.builder
.appName("SkewTestApp")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.debug.maxToStringFields", "100")
.config("spark.sql.autoBroadcastJoinThreshold","-1")
// .master("local[*]")
.getOrCreate
spark.sparkContext.setLogLevel("warn")
val frame = spark
.read
.parquet("hdfs://H1/data/skew_data_eg/big/")
.cache()
frame.count()
frame.createTempView("tbl_big")
spark
.read
.parquet("hdfs://H1/data/skew_data_eg/small/")
.createTempView("tbl_small")
// 一,单独处理数据倾斜的两个key
// 1,把数据倾斜的两个key挑出来单独处理,id随机加上0-7的前缀
spark
.sql("select concat(cast( 8 * rand() as int),',', id) as new_id,id,name from tbl_big where (id = 48 or id = 96)")
.createTempView("tbl_big_left_skew")
// 2,把小表扩大8倍
val rdd: RDD[Row] = spark.sql("select * from tbl_small where (id = 48 or id = 96)")
.rdd.flatMap(row => {
(0 to 7).map(i => {
val newId = s"$i,${row.getString(0)}"
Row.fromSeq(
Seq(
newId,
row.getString(0),
row.getString(1)
)
)
})
})
val schema = StructType(Array(
StructField("new_id", StringType),
StructField("id", StringType),
StructField("name", StringType)
))
spark.createDataFrame(rdd, schema).createTempView("tbl_big_right_skew")
// 3,对挑出来的数据进行join
val resultLeft = spark.sql(
s"""
| select b.id,b.name from tbl_big_left_skew b
| left join tbl_big_right_skew s
| on b.new_id = s.new_id
|""".stripMargin)
// 二,对未倾斜的所有key的处理
val resultRight = spark.sql(
s"""
| select b.id,b.name from tbl_big b
| left join tbl_small s
| on b.id = s.id
| where b.id not in (48,96)
|""".stripMargin)
resultRight.unionAll(resultLeft)
.write
.parquet("hdfs://H01/data/skew_data_res_3")
}
3,所有Key加盐处理数据倾斜
如果有很多大key,不适用上一种方法,则可以对整个左表加盐,把右表扩大对应的N倍。
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME","hadoop");
val spark = SparkSession
.builder
.appName("SkewTestApp")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.debug.maxToStringFields", "100")
.config("spark.sql.autoBroadcastJoinThreshold","-1")
// .master("local[*]")
.getOrCreate
spark.sparkContext.setLogLevel("warn")
val frame = spark
.read
.parquet("hdfs://H1/data/skew_data_eg/big/")
.cache()
frame.count()
frame.createTempView("tbl_big")
spark
.read
.parquet("hdfs://H1/data/skew_data_eg/small/")
.createTempView("tbl_small")
// 1,对整个数据集的id随机加上0-7的前缀
spark
.sql("select concat(cast( 8 * rand() as int),',', id) as new_id,id,name from tbl_big ")
.createTempView("tbl_big_left_skew")
// 2,把小表扩大8倍
val rdd: RDD[Row] = spark.sql("select * from tbl_small")
.rdd.flatMap(row => {
(0 to 7).map(i => {
val newId = s"$i,${row.getString(0)}"
Row.fromSeq(
Seq(
newId,
row.getString(0),
row.getString(1)
)
)
})
})
val schema = StructType(Array(
StructField("new_id", StringType),
StructField("id", StringType),
StructField("name", StringType)
))
spark.createDataFrame(rdd, schema).createTempView("tbl_big_right_skew")
val resultLeft = spark.sql(
s"""
| select b.id,b.name from tbl_big_left_skew b
| left join tbl_big_right_skew s
| on b.new_id = s.new_id
|""".stripMargin)
resultRight.unionAll(resultLeft)
.write
.parquet("hdfs://H01/data/skew_data_res_3")
}
4,不同Key的数据集中到一个Task导致的数据倾斜
如果是不同Key集中到一个Task导致的数据倾斜,有两种解决方案:
- ①增大或者减小Task的并行度
- ②自定义分区器