实战数据倾斜及其优化-Spark&Hive

数据倾斜是大数据处理无法规避的问题,大数据开发者都必须具备处理数据倾斜的思维和能力。

大数据采用分而治之、分布式并行处理大数据集,要想得到最好的处理性能,数据应该均衡的分布到集群各个计算节点上,这样才能真正实现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的并行度
  • ②自定义分区器
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小手追梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值