Spark 优化(性能优化、解决数据倾斜)

11 篇文章 0 订阅

1. 性能调优

1.1 常规性能调优

1.1.1 优化资源配置

在资源允许范围内,增加资源的分配,提升性能。

bin/spark-submit \
--class com.daidai.spark.AnalysisDemo \
--master yarn
--deploy-mode cluster
--num-executors 80 \
--driver-memory 6g \
--executor-memory 6g \
--executor-cores 3 \
/.../spark.jar
名称说明
–num-executorsexecutor数量
–executor-memoryexecutor内存(影响不大)
–executor-cores每个executor的CPU core数
–driver-memoryDriver内存

举个栗子:

资源一共:400G内存、100 core

参考分配:50个executore、每个executor8G、2 core

1.1.2 RDD优化

  1. 提高 RDD 复用
  2. RDD 持久化
  3. 尽早过滤掉没用的数据,减少内存占用

1.1.3 并行度调节

设置合理的并行度,官方推荐,task 数量应为 Spark 总 core 的 2~3 倍。之所以没 有推荐 task 数量与 CPU core 总数相等,是因为 task 的执行时间不同,有的 task 执行速度快 而有的 task 执行速度慢,如果 task 数量与 CPU core 总数相等,那么执行快的 task 执行完成 后,会出现 CPU core 空闲的情况。如果 task 数量设置为 CPU core 总数的 2~3 倍,那么一个 task 执行完毕后,CPU core 会立刻执行下一个 task,降低了资源的浪费,同时提升了 Spark 作业运行的效率。

SparkConf().set("spark.default.parallelism", "500")

1.1.4 使用广播变量

广播变量在每个 Executor 保存一个副本,此 Executor 的所有 task 共用此广播变量,这让变量产生的副本数量大大减少。

1.1.5 使用 Kroy 序列化

默认情况下,Spark 使用 Java 的序列化机制。Java 序列化机制的效率不高,序列化速度慢并且序列化后的数据所占用的空间依然较大。

Kryo 序列化机制比 Java 序列化机制性能提高 10 倍左右,Spark 之所以没有默认使用 Kryo 作为序列化类库,是因为它不支持所有对象的序列化,同时 Kryo 需要用户在使用前注 册需要序列化的类型,不够方便,但从 Spark 2.0.0 版本开始,简单类型、简单类型数组、字 符串类型的 Shuffling RDDs 已经默认使用 Kryo 序列化方式了。

配置方式:

public class MyKryoRegistrator implements KryoRegistrator{
    @Override
    public void registerClasses(Kryo kryo){
        kryo.register(StartupReportLogs.class);
    }
}
val conf = new SparkConf().setMaster().setAppName()
//使用 Kryo 序列化库,如果要使用 Java 序列化库,需要把该行屏蔽掉
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer"); 
//在 Kryo 序列化库中注册自定义的类集合,如果要使用 Java 序列化库,需要把该行屏蔽掉
conf.set("spark.kryo.registrator", "atguigu.com.MyKryoRegistrator");

1.1.6 调节本地化等待时长

默认3s

SparkConf().set("spark.locality.wait", "6")

通常来说,task 可能不会被分配到它处理的数据所在的节点, 因为这些节点可用的资源可能已经用尽,此时,Spark 会等待一段时间,默认 3s,如果等待 指定时间后仍然无法在指定节点运行,那么会自动降级,尝试将 task 分配到其他节点上。

1.2 算子优化

1.2.1 mapPartitions

普通的 map 算子,假设一个 partition 有 1 万条数据,那么 map 算子中的 function 要执行 1 万次。

如果是 mapPartitions 算子,由于一个 task 处理一个 RDD 的 partition,那么一个 task 只会执行一次 function,function 一次接收所有的 partition 数据。

但是如果使用 mapPartitions 算子,但数据量非常大时,function 一次处理一个分区的数据,如果一旦内存不足,此时无法回收内存,就可能会 OOM,即内存溢出。

1.2.2 foreachPartition

使用了 foreachPartition 算子后,可以获得以下的性能提升:

  • 对于我们写的 function 函数,一次处理一整个分区的数据;

  • 对于一个分区内的数据,创建唯一的数据库连接;

  • 只需要向数据库发送一次 SQL 语句和多组参数;

与 mapPartitions 算子类似,如果一个分区的数据量特别大,可能会造成 OOM, 即内存溢出。

1.2.3 filter 与 coalesce

在数据未 filter 之前,每个分区中数据量是差不多的,但 filter 之后,可能会出现分区间数据量差异很大。

repartition 与 coalesce 都可以用来进行重分区,其中 repartition 只是 coalesce 接口中 shuffle 为 true 的简易实现,coalesce 默认情况下不进行 shuffle,

减少分区数量:coalesce

增加分区数量:repartition

1.2.4 repartition 解决 SparkSQL 低并行度

并行度的设置对于 Spark SQL 是不生效的,用户设置的并行度只对于 Spark SQL 以外的所有 Spark 的 stage 生 效。

对于 Spark SQL 查询出来的 RDD,立即使用 repartition 算子,去重新进行分区,这样可以重新分区为多个 partition,从 repartition 之后的 RDD 操作,由于不再涉及 Spark SQL,因此 stage 的并行度就会等于你手动设置的值,这样就避免了 Spark SQL 所在 的 stage 只能用少量的 task 去处理大量数据并执行复杂的算法逻辑。

1.2.5 reduceByKey 预聚合

reduceByKey 相较于普通的 shuffle 操作一个显著的特点就是会进行 map 端的本地聚合,考虑使用 reduceByKey 代替其他的 shuffle 算 子,例如 groupByKey。

使用 reduceByKey 对性能的提升如下:

  • 本地聚合后,在 map 端的数据量变少,减少了磁盘 IO,也减少了对磁盘空间的占用;
  • 本地聚合后,下一个 stage 拉取的数据量变少,减少了网络传输的数据量;
  • 本地聚合后,在 reduce 端进行数据缓存的内存占用减少;
  • 本地聚合后,在 reduce 端进行聚合的数据量减少。

1.3 suffle 调优

1.3.1 调节 map 端缓冲区大小

默认 32KB

 SparkConf().set("spark.shuffle.file.buffer", "64")

1.3.2 调节 reduce 端缓冲区大小

默认 48MB

SparkConf().set("spark.reducer.maxSizeInFlight", "96")

1.3.3 调节 reduce 端拉取数据重试次数

默认 3

SparkConf().set("spark.shuffle.io.maxRetries", "6")

1.3.4 调节 reduce 端拉取数据等待间隔

默认 5s

SparkConf().set("spark.shuffle.io.retryWait", "60s")

1.3.5 调节 SortShuffle 排序操作阈值

默认 200

如果的确不需要排序操作,那么建议将这个参数调大 一些,大于 shuffle read task 的数量。

SparkConf().set("spark.shuffle.sort.bypassMergeThreshold", "400")

1.4 JVM 调优

1.4.1 降低 cache 操作的内存占比

堆内存被划分为了两块,Storage 和 Execution。Storage 主要用于缓存 RDD 数据和 broadcast 数据,Execution 主要用于缓存在 shuffle 过程中产生的 中间数据,Storage 占系统内存的 60%,Execution 占系统内存的 20%。

SparkConf().set("spark.storage.memoryFraction", "0.4")

1.4.2 调节 Executor 堆外内存

--conf spark.yarn.executor.memoryOverhead=2048

1.4.3 调节连接等待时长

--conf spark.core.connection.ack.wait.timeout=300

2. 数据倾斜

数据倾斜的表现

大部分的 task 执行都很迅速,只有个 task 运行很慢,可能是出现了数据倾斜。

大部分的 task 执行都很迅速,但有的 task 可能出现 OOM,可能是出现了数据倾斜。

定位数据倾斜问题

查询代码中的 shuffle 算子,例如:reduceByKey、countByKey、groupByKey、join等算子。

查询 Spark 运行 log。

2.1 聚合原始数据

2.1.1 避免掉 shuffle

如果 Spark 数据来源 hive表,可提前在 hive 中对 key 聚合,将同一 key 的数据聚合到一条数据中。

2.1.2 缩小 key 粒度

增大数据倾斜可能性,降低每个 task 的数据量

key 的数量增加,可能出现数据倾斜。

2.1.3 增大 key 的颗粒度

减小数据倾斜可能性,增大每个 task 的数据量

如果 key 对应的是 省、城市、日期,可以考虑扩大为 省、日期。

2.2 过滤掉导致倾斜的 key

2.3 提高 shuffle 操作中的 reduce 并行度

reduce 端并行度的提高就增加了 reduce 端 task 的数量,那么每个 task 分配到的数据量就会相应减少,由 此缓解数据倾斜问题。

大部分 shuffle 算子都可以传入并行度的参数 例如:reduceByKey(500)

对于 SparkSQL 中的 shuffle,设置 spark.sql.shuffle.partitions,默认200

def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
    combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
  }

2.4 使用随机 key 双重聚合

适用groupByKey、reduceByKey,不适用 join

首先 map 算子给每个数据的 key 添加随机数前缀,对 key 进行 打散,将原先一样的 key 变成不一样的 key,然后进行第一次聚合,这样 就可以让原本被一个 task 处理的数据分散到多个 task 上去做局部聚合,之后,去除掉每个 key 的前缀,再次进行聚合。

    val words = text.flatMap(_.split(" "))

    //    val value = words.map((_, 1)).reduceByKey(_ + _)

    val rdd1: RDD[(String, Int)] = words.map(word => {
      val random = new Random()
      random.nextInt(10) + "_" + word
    }).map((_, 1)).reduceByKey(_ + _)

    val result = rdd1.map(data => {
      val strings = data._1.split("_")
      (strings(1), data._2)
    }).reduceByKey(_ + _)

2.5 将 reduce join 转换为 map join

当 join 操作有数据倾斜问题并且其中一个 RDD 的数据量较小时,广播小 RDD 全量数据 +map 算子,从 Broadcast 变量中获取较小 RDD 的全 量数据,与当前 RDD 的每一条数据按照连接 key 进行比对,如果连接 key 相同的话,那么就将两个 RDD 的数据用你需要的方式连接。

    val user: RDD[(Int, String)] = context.makeRDD(List((1, "张大三"), (2, "李小四"), (3, "王老五")))

    val score = context.makeRDD(List((1, 99), (2, 90), (3, 80)))
    user.join(score).foreach(println)

>>>(3,(王老五,80))
>>>(1,(张大三,99))
>>>(2,(李小四,90))

    val userBroadcast = context.broadcast(user.collect())
    val result: RDD[(Int, (String, Int))] = score.map(data => {

      var result: (Int, (String, Int))=(0, ("", 0))
      val userList = userBroadcast.value
      for (use <- userList) {
        if (use._1 == data._1) {
          result = (use._1, (use._2, data._2))
        }
      }
      result
    })
>>>(3,(王老五,80))
>>>(2,(李小四,90))
>>>(1,(张大三,99))

2.6 对倾斜 key 单独进行 join

如果某个 RDD 只有一个 key,那么在 shuffle 过程中会默 认将此 key 对应的数据打散,由不同的 reduce 端 task 进行处理。

将发生数据倾斜的 key 单独提取 出来,组成一个 RDD,然后用这个原本会导致倾斜的 key 组成的 RDD 根 其他 RDD 单独 join。

如果整个 RDD 就一个 key 的数据量特别多,那么就可以考虑使用这种方法。

2.7 使用随机数以及扩容表进行 join

选择一个 RDD,使用 flatMap 进行扩容,对每条数据的 key 添加数值 前缀(1~N 的数值),将一条数据映射为多条数据;(扩容)

选择另外一个 RDD,进行 map 映射操作,每条数据的 key 都打上一 个随机数作为前缀(1~N 的随机数);(稀释)

在这里插入图片描述

局限性: 如果两个 RDD 都很大,那么将 RDD 进行 N 倍的扩容显然行不通; 使用扩容的方式只能缓解数据倾斜,不能彻底解决数据倾斜问题。

优化:

  1. 对包含少数几个数据量过大的 key 的那个 RDD,通过 sample 算子 采样出一份样本来,然后统计一下每个 key 的数量,计算出来数据量最大 的是哪几个 key。
  2. 然后将这几个 key 对应的数据从原来的 RDD 中拆分出来,形成一 个单独的 RDD,并给每个 key 都打上 n 以内的随机数作为前缀,而不会导 致倾斜的大部分 key 形成另外一个 RDD。
  3. 接着将需要 join 的另一个 RDD,也过滤出来那几个倾斜 key 对应 的数据并形成一个单独的 RDD,将每条数据膨胀成 n 条数据,这 n 条数据 都按顺序附加一个 0~n 的前缀,不会导致倾斜的大部分 key 也形成另外一 个 RDD。
  4. 再将附加了随机前缀的独立 RDD 与另一个膨胀 n 倍的独立 RDD 进 行 join,此时就可以将原先相同的 key 打散成 n 份,分散到多个 task 中去 进行 join 了。
  5. 而另外两个普通的 RDD 就照常 join 即可。
  6. 最后将两次 join 的结果使用 union 算子合并起来即可,就是最终的 join 结果。
    都打上 n 以内的随机数作为前缀,而不会导 致倾斜的大部分 key 形成另外一个 RDD。
  7. 接着将需要 join 的另一个 RDD,也过滤出来那几个倾斜 key 对应 的数据并形成一个单独的 RDD,将每条数据膨胀成 n 条数据,这 n 条数据 都按顺序附加一个 0~n 的前缀,不会导致倾斜的大部分 key 也形成另外一 个 RDD。
  8. 再将附加了随机前缀的独立 RDD 与另一个膨胀 n 倍的独立 RDD 进 行 join,此时就可以将原先相同的 key 打散成 n 份,分散到多个 task 中去 进行 join 了。
  9. 而另外两个普通的 RDD 就照常 join 即可。
  10. 最后将两次 join 的结果使用 union 算子合并起来即可,就是最终的 join 结果。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值