一、Shuffle 的作用是什么?
Shuffle 操作可以理解为将集群中各个节点上的数据进行重新整理和分类的过程。这一概念源自 Hadoop 的 MapReduce 模型,Shuffle 是连接 Map 阶段和 Reduce 阶段的关键环节。在分布式计算中,每个计算节点通常只处理任务的一部分数据。如果下一个阶段需要依赖前一个阶段的所有计算结果,就需要对这些结果进行重新整合和分类,这就是 Shuffle 的主要任务。在 Spark 中,RDD 之间的依赖关系分为窄依赖和宽依赖,其中宽依赖涉及 Shuffle 操作。因此,在 Spark 程序中,每个 job 的阶段(stage)划分依据就是是否存在 Shuffle 操作,每个 stage 包含一系列的 RDD map 操作。
二、为什么 Shuffle 操作耗时?
Shuffle 操作需要对数据进行重新聚合和划分,并将这些数据分配到集群的各个节点上进行下一步的处理。这一过程中,不同节点之间需要进行大量的数据交换。由于数据传输需要通过网络,并且通常需要先将数据写入磁盘,因此每个节点都会进行大量的文件读写操作。这些读写操作不仅增加了 I/O 开销,还可能导致网络拥塞,从而使 Shuffle 操作变得非常耗时,相比之下,简单的 map 操作则要快得多。
三、Spark 当前的ShuffleManager模式及处理机制
在 Spark 程序中,Shuffle 操作由 ShuffleManager 对象进行管理。目前,Spark 支持两种主要的 ShuffleManager 模式:HashShuffleManager 和 SortShuffleManager。Shuffle 操作包括当前阶段的 Shuffle Write(写入磁盘)和下一阶段的 Shuffle Read(读取),这两种模式的主要区别在于 Shuffle Write 阶段的处理方式。
3.1、HashShuffleManager
HashShuffleManager 是 Spark 最初使用的 ShuffleManager 模式。在这种模式下,每个任务(task)会为每个分区(partition)创建一个临时文件,并将数据直接写入对应的文件中。这种方式简单直观,但在处理大量分区时会产生大量的小文件,导致磁盘 I/O 开销增加。此外,每个任务都需要为每个分区打开和关闭文件,这也会增加文件句柄的开销。
3.2、SortShuffleManager
SortShuffleManager 是目前 Spark 默认使用的 ShuffleManager 模式。在这种模式下,任务会先对数据进行排序,然后将排序后的数据写入一个或几个大文件中。这种方式减少了文件的数量,提高了磁盘 I/O 效率。此外,SortShuffleManager 还支持数据的内存缓存,只有在内存不足时才会将数据溢写到磁盘,从而进一步提高了性能。
四、Spark 程序的 Shuffle 调优
Shuffle 阶段需要将数据写入磁盘,这涉及到大量的读写文件操作和文件传输操作,对节点的系统 I/O 有较大的影响。通过调整一些关键参数,可以减少 Shuffle 阶段的文件数量和 I/O 读写次数,从而提高性能。以下是几个主要的调优参数:
1、spark.shuffle.manager:设置 Spark 任务的 ShuffleManager 模式。对于 Spark 1.2 以上版本,默认值为 sort
,即在 Shuffle Write 阶段会对数据进行排序,每个 executor 上生成的文件会合并成两个文件(一个数据文件和一个索引文件)。通常情况下,默认的 sort
模式已经能够提供较好的性能,除非有特殊情况,一般不需要更改此参数。
2、spark.shuffle.sort.bypassMergeThreshold:设置启用 bypass 机制的阈值。如果 Shuffle Read 阶段的 task 数量小于或等于该值,则 Shuffle Write 阶段会启用 bypass 机制。默认值为 200。如果 Shuffle Read 阶段的 task 数量较少,可以适当降低这个阈值,以启用 bypass 机制,减少文件合并操作,提高性能。
3、spark.shuffle.file.buffer:设置 Shuffle Write 阶段写文件时缓冲区的大小。默认值为 32MB。如果内存资源充足,可以将该值调大(例如 64MB),以减少 executor 的 I/O 读写次数,提高写入速度。
4、spark.shuffle.io.maxRetries:设置 Shuffle Read 阶段 fetch 数据时的最大重试次数。默认值为 3 次。如果 Shuffle 阶段的数据量很大,网络环境不稳定,可以适当增加重试次数,以提高数据传输的成功率。
除了上述参数外,还有一些其他常用的 Shuffle 调优参数,可以帮助进一步优化性能:
1、spark.shuffle.compress:是否启用 Shuffle 数据的压缩。默认值为 true
。启用压缩可以减少网络传输的数据量,但会增加 CPU 负载。如果网络带宽是瓶颈,建议开启压缩;如果 CPU 是瓶颈,可以考虑关闭压缩。
2、spark.shuffle.spill:是否启用 Shuffle 数据的溢写(spill)。默认值为 true
。启用溢写可以防止内存不足导致的任务失败,但会增加磁盘 I/O 开销。如果内存资源充足,可以考虑关闭溢写以提高性能。
3、spark.shuffle.spill.compress:是否启用 Shuffle 溢写数据的压缩。默认值为 true
。启用压缩可以减少磁盘 I/O 开销,但会增加 CPU 负载。如果磁盘 I/O 是瓶颈,建议开启压缩;如果 CPU 是瓶颈,可以考虑关闭压缩。
4、spark.shuffle.memoryFraction:分配给 Shuffle 操作的内存比例。默认值为 0.66。根据实际内存情况调整该值,以平衡 Shuffle 操作和其他操作的内存需求。
5、spark.shuffle.manager.numPartitions:设置 Shuffle 操作的分区数。默认值根据数据量自动确定。合理设置分区数,避免过多或过少的分区。过多的分区会导致更多的网络通信,过少的分区可能导致数据倾斜。
通过调整这些参数,可以显著改善 Shuffle 阶段的性能,从而提升整个 Spark 应用的效率。