1-Spark 性能调优

1.1常规性能调优

1.1.1 常规性能调优一:最优资源配置

在一定范围内,增大资源的分配与性能的提升成正比,正所谓大力出奇迹!实现了最优的资源配置后,在此基础上再考虑之后的调优策略。

# 正常的提交脚本
bin/spark-submit \
--class com.atguigu.spark.Analysis \
--master yarn
--deploy-mode cluster
--num-executors 80 \
--driver-memory 6g \
--executor-memory 6g \
--executor-cores 3 \
--queue root.default \
--conf spark.yarn.executor.memoryOverhead=2048 \
--conf spark.core.connection.ack.wait.timeout=300 \
/usr/opt/modules/spark/jar/spark.jar

# 正常的提交脚本
bin/spark-submit \
--class com.atguigu.spark.Analysis \
--master yarn
--deploy-mode cluster
--num-executors 80 \
--driver-memory 6g \
--executor-memory 6g \
--executor-cores 3 \
--queue root.default \
--conf spark.yarn.executor.memoryOverhead=2048 \
--conf spark.core.connection.ack.wait.timeout=300 \
/usr/opt/modules/spark/jar/spark.jar

可进行资源配置的项如下表:

名称

说明

影响

--driver-memory

driver的内存,配置2G就可,因为driver不做计算

无明显提升

--executor-memory

每个executor的内存

在资源允许的情况下,增加每个 Executor 的 内存量以后,对性能的提升有三点:1. 可 以 缓 存 更 多 的 数 据 ( 即 对 RDD 进 行 cache),写入磁盘的数据相应减少,甚至 可以不写入磁盘,减少了可能的磁盘 IO;2. 可以为 shuffle 操作提供更多内存,即有更 多空间来存放 reduce 端拉取的数据,写入 磁盘的数据相应减少,甚至可以不写入磁 盘,减少了可能的磁盘 IO;3. 可以为 task 的执行提供更多内存,在 task 的执行过程中可能创建很多对象,内存较 小时会引发频繁的 GC,增加内存后,可 以避免频繁的 GC,提升整体性能。

--executor-cores

每个executor的cpu核数

在资源允许的情况下,增加每个 Executor 的 Cpu core 个数,可以提高执行 task 的并行度。比如有 4 个 Executor,每个 Executor 有 2 个 CPU core,那么可以并行执行 8 个 task,如果 将每个 Executor 的 CPU core 个数增加到 4 个 (资源允许的情况下),那么可以并行执行 16 个 task,此时的并行能力提升了一倍。

--num-executors

配置executor的数量

在资源允许的情况下,增加 Executor 的个数 可以提高执行 task 的并行度。比如有 4 个 Executor,每个 Executor 有 2 个 CPU core,那 么可以并行执行 8 个 task,如果将 Executor 的 个数增加到 8 个(资源允许的情况下),那么 可以并行执行 16 个 task,此时的并行能力提 升了一倍。

例子:

硬件资源:6 节点,每个节点16 cores, 64 GB 内存
每个节点在计算资源时候,给操作系统和Hadoop的进程预留1core,1GB,所以每个节点剩下15个core和63GB
内存。
core的个数,决定一个executor能够并发任务的个数。所以通常认为,一个executor越多的并发任务能够得到更好的性能,但有研究显示一个应用并发任务超过5,导致更差的性能。所以core的个数暂设置为5个。
5个core是表明executor并发任务的能力,并不是说一个系统有多少个core,即使我们一个CPU有32个core,也设置5个core不变。
executor个数,接下来,一个executor分配 5 core,一个node有15 core,从而我们计算一个node上会有3 executor(15 / 5),然后通过每个node的executor个数得到整个任务可以分配的executors个数。
我们有6个节点,每个节点3个executor,6 3 = 18个executors,额外预留1个executor给AM,最终要配置17个executors。
最后spark-submit启动脚本中配置 –num-executors = 17
memory,配置每个executor的内存,一个node,3 executor, 63G内存可用,所以每个executor可配置内存为63 / 3 = 21G
从Spark的内存模型角度,Executor占用的内存分为两部分:ExecutorMemory和MemoryOverhead,预留出MemoryOverhead的内存量之后,才是ExecutorMemory的内存。
MemoryOverhead的计算公式:max(384M, 0.07 spark.executor.memory)

硬件资源:6 节点,每个节点16 cores, 64 GB 内存
每个节点在计算资源时候,给操作系统和Hadoop的进程预留1core,1GB,所以每个节点剩下15个core和63GB
内存。
core的个数,决定一个executor能够并发任务的个数。所以通常认为,一个executor越多的并发任务能够得到更好的性能,但有研究显示一个应用并发任务超过5,导致更差的性能。所以core的个数暂设置为5个。
5个core是表明executor并发任务的能力,并不是说一个系统有多少个core,即使我们一个CPU有32个core,也设置5个core不变。
executor个数,接下来,一个executor分配 5 core,一个node有15 core,从而我们计算一个node上会有3 executor(15 / 5),然后通过每个node的executor个数得到整个任务可以分配的executors个数。
我们有6个节点,每个节点3个executor,6 3 = 18个executors,额外预留1个executor给AM,最终要配置17个executors。
最后spark-submit启动脚本中配置 –num-executors = 17
memory,配置每个executor的内存,一个node,3 executor, 63G内存可用,所以每个executor可配置内存为63 / 3 = 21G
从Spark的内存模型角度,Executor占用的内存分为两部分:ExecutorMemory和MemoryOverhead,预留出MemoryOverhead的内存量之后,才是ExecutorMemory的内存。
MemoryOverhead的计算公式:max(384M, 0.07 spark.executor.memory)
1.1.2 常规性能调优一:并行度调节

Spark 作业中的并行度指各个 stage 的 task 的数量。Spark 官方推荐,task 数量应该设置为 Spark 作业总 CPU 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 作业运行的效率。

如果并行度设置不合理而导致并行度过低,会导致资源的极大浪费,例如,20 个 Executor, 每个 Executor 分配 3 个 CPU core,而 Spark 作业有 40 个 task,这样每个 Executor 分配到的 task 个数是 2 个,这就使得每个 Executor 有一个 CPU core 空闲,导致资源的浪费。理想的并行度设置,应该是让并行度与资源相匹配,简单来说就是在资源允许的前提下, 并行度要设置的尽可能大,达到可以充分利用集群资源。合理的设置并行度,可以提升整个 Spark 作业的性能和运行速度。

val conf = new SparkConf()
.set("spark.default.parallelism", "500")

val conf = new SparkConf()
.set("spark.default.parallelism", "500")
1.1.3 常规性能调优四:广播大变量

默认情况下,task 中的算子中如果使用了外部的变量,每个 task 都会获取一份变量的复 本,这就造成了内存的极大消耗。一方面,如果后续对 RDD 进行持久化,可能就无法将 RDD 数据存入内存,只能写入磁盘,磁盘 IO 将会严重消耗性能;另一方面,task 在创建对象的 时候,也许会发现堆内存无法存放新创建的对象,这就会导致频繁的 GC,GC 会导致工作 线程停止,进而导致 Spark 暂停工作一段时间,严重影响 Spark 性能。

假设当前任务配置了 20 个 Executor,指定 500 个 task,有一个 20M 的变量被所有 task 共用,此时会在 500 个 task 中产生 500 个副本,耗费集群 10G 的内存,如果使用了广播变 量, 那么每个 Executor 保存一个副本,一共消耗 400M 内存,内存消耗减少了 5 倍。广播变量在每个 Executor 保存一个副本,此 Executor 的所有 task 共用此广播变量,这让变 量产生的副本数量大大减少。

在初始阶段,广播变量只在 Driver 中有一份副本。task 在运行的时候,想要使用广播变 量中的数据,此时首先会在自己本地的 Executor 对应的 BlockManager 中尝试获取变量,如 果本地没有,BlockManager 就会从 Driver 或者其他节点的 BlockManager 上远程拉取变量的 复本,并由本地的 BlockManager 进行管理;之后此 Executor 的所有 task 都会直接从本地的 BlockManager 中获取变量。

1.1.4 常规性能调优五:Kryo 序列化

默认情况下,Spark 使用 Java 的序列化机制。Java 的序列化机制使用方便,不需要额外 的配置,在算子中使用的变量实现 Serializable 接口即可,但是,Java 序列化机制的效率不 高,序列化速度慢并且序列化后的数据所占用的空间依然较大。

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

1.1.5 常规性能调优六:调节本地化等待时长

Spark 作业运行过程中,Driver 会对每一个 stage 的 task 进行分配。根据 Spark 的 task 分 配算法,Spark 希望 task 能够运行在它要计算的数据算在的节点(数据本地化思想),这样 就可以避免数据的网络传输。通常来说,task 可能不会被分配到它处理的数据所在的节点, 因为这些节点可用的资源可能已经用尽,此时,Spark 会等待一段时间,默认 3s,如果等待 指定时间后仍然无法在指定节点运行,那么会自动降级,尝试将 task 分配到比较差的本地化级别所对应的节点上,比如将 task 分配到离它要计算的数据比较近的一个节点,然后进 行计算,如果当前级别仍然不行,那么继续降级。

当 task 要处理的数据不在 task 所在节点上时,会发生数据的传输。task 会通过所在节 点的 BlockManager 获取数据,BlockManager 发现数据不在本地时,户通过网络传输组件从 数据所在节点的 BlockManager 处获取数据。

网络传输数据的情况是我们不愿意看到的,大量的网络传输会严重影响性能,因此,我 们希望通过调节本地化等待时长,如果在等待时长这段时间内,目标节点处理完成了一部分 task,那么当前的 task 将有机会得到执行,这样就能够改善 Spark 作业的整体性能。

Spark 的本地化等级请参考上篇文章。

在 Spark 项目开发阶段,可以使用 client 模式对程序进行测试,此时,可以在本地看到 比较全的日志信息,日志信息中有明确的 task 数据本地化的级别,如果大部分都是 PROCESS_LOCAL,那么就无需进行调节,但是如果发现很多的级别都是 NODE_LOCAL、 ANY,那么需要对本地化的等待时长进行调节,通过延长本地化等待时长,看看 task 的本 地化级别有没有提升,并观察 Spark 作业的运行时间有没有缩短。

注意,过犹不及,不要将本地化等待时长延长地过长,导致因为大量的等待时长,使得 Spark 作业的运行时间反而增加了。

1.2 算子调优

1.2.1 算子调优:repartition 解决 SparkSQL 低并行度问题

在第一节的常规性能调优中我们讲解了并行度的调节策略,但是,并行度的设置对于 Spark SQL 是不生效的,用户设置的并行度只对于 Spark SQL 以外的所有 Spark 的 stage 生 效。

Spark SQL 的并行度不允许用户自己指定,Spark SQL 自己会默认根据 hive 表对应的 HDFS 文件的 split 个数自动设置 Spark SQL 所在的那个 stage 的并行度,用户自己通 spark.default.parallelism 参数指定的并行度,只会在没 Spark SQL 的 stage 中生效。

由于 Spark SQL 所在 stage 的并行度无法手动设置,如果数据量较大,并且此 stage 中 后续的 transformation 操作有着复杂的业务逻辑,而 Spark SQL 自动设置的 task 数量很少, 这就意味着每个 task 要处理为数不少的数据量,然后还要执行非常复杂的处理逻辑,这就 可能表现为第一个有 Spark SQL 的 stage 速度很慢,而后续的没有 Spark SQL 的 stage 运行 速度非常快。

为了解决 Spark SQL 无法设置并行度和 task 数量的问题,我们可以使用 repartition 算子。

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

1.3 Shuffle 调优

1.3.1 Shuffle 调优一:调节 map 端缓冲区大小

在 Spark 任务运行过程中,如果 shuffle 的 map 端处理的数据量比较大,但是 map 端缓 冲的大小是固定的,可能会出现 map 端缓冲数据频繁 spill 溢写到磁盘文件中的情况,使得 性能非常低下,通过调节 map 端缓冲的大小,可以避免频繁的磁盘 IO 操作,进而提升 Spark 任务的整体性能。

map 端缓冲的默认配置是 32KB,如果每个 task 处理 640KB 的数据,那么会发生 640/32 = 20 次溢写,如果每个 task 处理 64000KB 的数据,机会发生 64000/32=2000 此溢写,这对 于性能的影响是非常严重的。

val conf = new SparkConf()
 .set("spark.shuffle.file.buffer", "64")

val conf = new SparkConf()
 .set("spark.shuffle.file.buffer", "64")
1.3.2 Shuffle 调优二:调节 reduce 端拉取数据缓冲区大小

Spark Shuffle 过程中,shuffle reduce task 的 buffer 缓冲区大小决定了 reduce task 每次能 够缓冲的数据量,也就是每次能够拉取的数据量,如果内存资源较为充足,适当增加拉取数 据缓冲区的大小,可以减少拉取数据的次数,也就可以减少网络传输的次数,进而提升性能。reduce 端数据拉取缓冲区的大小可以通过 spark.reducer.maxSizeInFlight 参数进行设置,默认 为 48MB,该参数的设置方法如代码清单所示:

val conf = new SparkConf()
.set("spark.reducer.maxSizeInFlight", "96")

val conf = new SparkConf()
.set("spark.reducer.maxSizeInFlight", "96")
1.3.3 Shuffle 调优三:调节 reduce 端拉取数据重试次数

Spark Shuffle 过程中,reduce task 拉取属于自己的数据时,如果因为网络异常等原因导 致失败会自动进行重试。对于那些包含了特别耗时的 shuffle 操作的作业,建议增加重试最 大次数(比如 60 次),以避免由于 JVM 的 full gc 或者网络不稳定等因素导致的数据拉取失 败。在实践中发现,对于针对超大数据量(数十亿~上百亿)的 shuffle 过程,调节该参数可 以大幅度提升稳定性。

reduce 端拉取数据重试次数可以通过 spark.shuffle.io.maxRetries 参数进行设置,该参数 就代表了可以重试的最大次数。如果在指定次数之内拉取还是没有成功,就可能会导致作业 执行失败,默认为 3,该参数的设置方法如代码清单所示:

val conf = new SparkConf()
 .set("spark.shuffle.io.maxRetries", "6")

val conf = new SparkConf()
 .set("spark.shuffle.io.maxRetries", "6")
1.3.4 Shuffle 调优四:调节 reduce 端拉取数据等待间隔

Spark Shuffle 过程中,reduce task 拉取属于自己的数据时,如果因为网络异常等原因导 致失败会自动进行重试,在一次失败后,会等待一定的时间间隔再进行重试,可以通过加大 间隔时长(比如 60s),以增加 shuffle 操作的稳定性。

reduce 端拉取数据等待间隔可以通过 spark.shuffle.io.retryWait 参数进行设置, 默认值为 5s,该参数的设置方法如代码清单所示:

val conf = new SparkConf()
 .set("spark.shuffle.io.retryWait", "60s")

val conf = new SparkConf()
 .set("spark.shuffle.io.retryWait", "60s")
1.3.5 Shuffle 调优五:调节 SortShuffle 排序操作阈值

对于 SortShuffleManager,如果 shuffle reduce task 的数量小于某一阈值则 shuffle write 过 程中不会进行排序操作,而是直接按照未经优化的 HashShuffleManager 的方式去写数据,但 是最后会将每个 task 产生的所有临时磁盘文件都合并成一个文件,并会创建单独的索引文 件。

当你使用 SortShuffleManager 时,如果的确不需要排序操作,那么建议将这个参数调大 一些,大于 shuffle read task 的数量,那么此时 map-side 就不会进行排序了,减少了排序的 性能开销,但是这种方式下,依然会产生大量的磁盘文件,因此 shuffle write 性能有待提高。SortShuffleManager 排序操作阈值的设置可以通过 spark.shuffle.sort. bypassMergeThreshold 这 一参数进行设置,默认值为 200,该参数的设置方法如代码清单所示:

val conf = new SparkConf()
 .set("spark.shuffle.sort.bypassMergeThreshold", "400")

val conf = new SparkConf()
 .set("spark.shuffle.sort.bypassMergeThreshold", "400")

1.4 JVM 调优

对于 JVM 调优,首先应该明确,full gc/minor gc,都会导致 JVM 的工作线程停止工作, 即 stop the world。

1.4.1 JVM 调优一:降低 cache 操作的内存占比
  1. 静态内存管理机制

根据 Spark 静态内存管理机制,堆内存被划分为了两块,Storage 和 Execution。Storage 主要用于缓存 RDD 数据和 broadcast 数据,Execution 主要用于缓存在 shuffle 过程中产生的 中间数据,Storage 占系统内存的 60%,Execution 占系统内存的 20%,并且两者完全独立。在一般情况下,Storage 的内存都提供给了 cache 操作,但是如果在某些情况下 cache 操作内 存不是很紧张,而 task 的算子中创建的对象很多,Execution 内存又相对较小,这回导致频 繁的 minor gc,甚至于频繁的 full gc,进而导致 Spark 频繁的停止工作,性能影响会很大。在 Spark UI 中可以查看每个 stage 的运行情况,包括每个 task 的运行时间、gc 时间等等,如 果发现 gc 太频繁,时间太长,就可以考虑调节 Storage 的内存占比,让 task 执行算子函数 式,有更多的内存可以使用。

Storage 内存区域可以通过 spark.storage.memoryFraction 参数进行指定,默认为 0.6,即 60%,可以逐级向下递减,如代码清单所示:

val conf = new SparkConf()
 .set("spark.storage.memoryFraction", "0.4")

val conf = new SparkConf()
 .set("spark.storage.memoryFraction", "0.4")
  1. 统一内存管理机制

根据 Spark 统一内存管理机制,堆内存被划分为了两块,Storage 和 Execution。Storage 主要用于缓存数据,Execution 主要用于缓存在 shuffle 过程中产生的中间数据,两者所组成 的内存部分称为统一内存,Storage 和 Execution 各占统一内存的 50%,由于动态占用机制的 实现,shuffle 过程需要的内存过大时,会自动占用 Storage 的内存区域,因此无需手动进行 调节。

1.4.2 JVM 调优二:调节 Executor 堆外内存

Executor 的堆外内存主要用于程序的共享库、Perm Space、 线程 Stack 和一些 Memory mapping 等, 或者类 C 方式 allocate object。

有时,如果你的 Spark 作业处理的数据量非常大,达到几亿的数据量,此时运行 Spark 作业会时不时地报错,例如 shuffle output file cannot find,executor lost,task lost,out of memory 等,这可能是 Executor 的堆外内存不太够用,导致 Executor 在运行的过程中内存溢出。

stage 的 task 在运行的时候,可能要从一些 Executor 中去拉取 shuffle map output 文件, 但是 Executor 可能已经由于内存溢出挂掉了,其关联的 BlockManager 也没有了,这就可能 会报出 shuffle output file cannot find,executor lost,task lost,out of memory 等错误,此时, 就可以考虑调节一下 Executor 的堆外内存,也就可以避免报错,与此同时,堆外内存调节的 比较大的时候,对于性能来讲,也会带来一定的提升。

默认情况下,Executor 堆外内存上限大概为 300 多 MB,在实际的生产环境下,对海量 数据进行处理的时候,这里都会出现问题,导致 Spark 作业反复崩溃,无法运行,此时就会 去调节这个参数,到至少 1G,甚至于 2G、4G。

Executor 堆外内存的配置需要在 spark-submit 脚本里配置,如代码清单所示:

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

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

2-Spark 数据倾斜

数据倾斜的表现:

  • Spark 作业的大部分 task 都执行迅速,只有有限的几个 task 执行的非常慢,此时可能出 现了数据倾斜,作业可以运行,但是运行得非常慢;
  • Spark 作业的大部分 task 都执行迅速,但是有的 task 在运行过程中会突然报出 OOM, 反复执行几次都在某一个 task 报出 OOM 错误,此时可能出现了数据倾斜,作业无法正 常运行。

定位数据倾斜问题:

  • 查阅代码中的 shuffle 算子,例如 reduceByKey、countByKey、groupByKey、join 等算 子,根据代码逻辑判断此处是否会出现数据倾斜;
  • 查看 Spark 作业的 log 文件,log 文件对于错误的记录会精确到代码的某一行,可以根 据异常定位到的代码位置来明确错误发生在第几个 stage,对应的 shuffle 算子是哪一个;
2.1 解决方案二:过滤导致倾斜的 key

如果在 Spark 作业中允许丢弃某些数据比如空值key引起的数据倾斜,那么可以考虑将可能导致数据倾斜的 key 进行 过滤,滤除可能导致数据倾斜的 key 对应的数据,这样,在 Spark 作业中就不会发生数据倾 斜了。

2.2 解决方案四:使用随机 key 实现双重聚合

当使用了类似于 groupByKey、reduceByKey 这样的算子时,可以考虑使用随机 key 实 现双重聚合。

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

此方法对于由 groupByKey、reduceByKey 这类算子造成的数据倾斜由比较好的效果, 仅仅适用于聚合类的 shuffle 操作,适用范围相对较窄。如果是 join 类的 shuffle 操作,还得 用其他的解决方案。

2.3 解决方案五:将 reduce join 转换为 map join

正常情况下,join 操作都会执行 shuffle 过程,并且执行的是 reduce join,也就是先将所 有相同的 key 和对应的 value 汇聚到一个 reduce task 中,然后再进行 join。普通 join 的过程 如下图所示:



spark yarn 运行 foreach传入外部变量 spark 外部表_并行度

普通的 join 是会走 shuffle 过程的,而一旦 shuffle,就相当于会将相同 key 的数据拉取 到一个 shuffle read task 中再进行 join,此时就是 reduce join。但是如果一个 RDD 是比较小 的,则可以采用广播小 RDD 全量数据+map 算子来实现与 join 同样的效果,也就是 map join, 此时就不会发生 shuffle 操作,也就不会发生数据倾斜。(注意,RDD 是并不能进行广播的,只能将 RDD 内部的数据通过 collect 拉取到 Driver 内 存然后再进行广播)

核心思路:

不使用 join 算子进行连接操作,而使用 Broadcast 变量与 map 类算子实现 join 操作,进 而完全规避掉 shuffle 类的操作,彻底避免数据倾斜的发生和出现。将较小 RDD 中的数据直 接通过 collect 算子拉取到 Driver 端的内存中来,然后对其创建一个 Broadcast 变量;接着对 另外一个 RDD 执行 map 类算子,在算子函数内,从 Broadcast 变量中获取较小 RDD 的全量 数据,与当前 RDD 的每一条数据按照连接 key 进行比对,如果连接 key 相同的话,那么就 将两个 RDD 的数据用你需要的方式连接起来。

根据上述思路,根本不会发生 shuffle 操作,从根本上杜绝了 join 操作可能导致的数据 倾斜问题。

当 join 操作有数据倾斜问题并且其中一个 RDD 的数据量较小时,可以优先考虑这种方 式,效果非常好。

不适用场景分析:

由于Spark 的广播变量是在每个Executor中保存一个副本,如果两个 RDD数据量都比较大, 那么如果将一个数据量比较大的 RDD 做成广播变量,那么很有可能会造成内存溢出。

2.4 解决方案六:sample 采样对倾斜 key 单独进行 join

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

当由单个 key 导致数据倾斜时,可有将发生数据倾斜的 key 单独提取出来,组成一个 RDD,然后用这个原本会导致倾斜的 key 组成的 RDD 根其他 RDD 单独 join,此时,根据 Spark 的运行机制,此 RDD 中的数据会在 shuffle 阶段被分散到多个 task 中去进行 join 操 作。倾斜 key 单独 join 的流程如图所示:



spark yarn 运行 foreach传入外部变量 spark 外部表_spark写表指定外部表_02

1. 适用场景分析:

对于 RDD 中的数据,可以将其转换为一个中间表,或者是直接使用 countByKey()的方 式,看一个这个 RDD 中各个 key 对应的数据量,此时如果你发现整个 RDD 就一个 key 的 数据量特别多,那么就可以考虑使用这种方法。

当数据量非常大时,可以考虑使用 sample 采样获取 10%的数据,然后分析这 10%的数 据中哪个 key 可能会导致数据倾斜,然后将这个 key 对应的数据单独提取出来。

2. 不适用场景分析:

如果一个 RDD 中导致数据倾斜的 key 很多,那么此方案不适用。

spark动态资源分配?

batch模式和streaming模式的资源分配/动态分配有什么区别?