Spark shuffle详细过程

本文深入探讨了Spark中MapReduce的shuffle过程,详细介绍了HashShuffle、SortShuffle及TungstenSort三种shuffle方式的工作原理与优缺点,并给出了针对不同场景的最佳实践建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有许多场景下,我们需要进行跨服务器的数据整合,比如两个表之间,通过Id进行join操作,你必须确保所有具有相同id的数据整合到相同的块文件中。那么我们先说一下mapreduce的shuffle过程。

 

Mapreduce的shuffle的计算过程是在executor中划分mapper与reducer。Spark的Shuffling中有两个重要的压缩参数。spark.shuffle.compress true---是否将会将shuffle中outputs的过程进行压缩。将spark.io.compression.codec编码器设置为压缩数据,默认是true.同时,通过spark.shuffle.manager 来设置shuffle时的排序算法,有hash,sort,tungsten-sort。(用hash会快一点,我不需要排序啊~)

 

Hash Shuffle

使用hash散列有很多缺点,主要是因为每个Map task都会为每个reduce生成一份文件,所以最后就会有M * R个文件数量。那么如果在比较多的Map和Reduce的情况下就会出问题,输出缓冲区的大小,系统中打开文件的数量,创建和删除所有这些文件的速度都会受到影响。如下图:

 

这里有一个优化的参数spark.shuffle.consolidateFiles,默认为false,当设置成true时,会对mapper output时的文件进行合并。如果你集群有E个executors(“-num-excutors”)以及C个cores("-executor-cores”),以及每个task又T个CPUs(“spark.task.cpus”),那么总共的execution的slot在集群上的个数就是E * C / T(也就是executor个数×CORE的数量/CPU个数)个,那么shuffle过程中所创建的文件就为E * C / T * R(也就是executor个数 × core的个数/CPU个数×reduce个数)个。外文文献写的太公式化,那么我用通俗易懂的形式阐述下。就好比总共的并行度是20(5个executor,4个task)  Map阶段会将数据写入磁盘,当它完成时,他将会以reduce的个数来生成文件数。那么每个executor就只会计算core的数量/cpu个数的tasks.如果task数量大于总共集群并行度,那么将开启下一轮,轮询执行。

速度较快,因为没有再对中间结果进行排序,减少了reduce打开文件时的性能消耗。

当然,当数据是经过序列化以及压缩的。当重新读取文件,数据将进行解压缩与反序列化,这里reduce端数据的拉取有个参数spark.reducer.maxSizeInFlight(默认为48MB),它将决定每次数据从远程的executors中拉取大小。这个拉取过程是由5个并行的request,从不同的executor中拉取过来,从而提升了fetch的效率。 如果你加大了这个参数,那么reducers将会请求更多的文数据进来,它将提高性能,但是也会增加reduce时的内存开销

 

 

Sort Shuffle

Sort Shuffle如同hash shuffle map写入磁盘,reduce拉取数据的一个性质,当在进行sort shuffle时,总共的reducers要小于spark.shuffle.sort.bypassMergeThrshold(默认为200),将会执行回退计划,使用hash将数据写入单独的文件中,然后将这些小文件聚集到一个文件中,从而加快了效率。(实现自BypassMergeSortShuffleWriter中)

那么它的实现逻辑是在reducer端合并mappers的输出结果。Spark在reduce端的排序是用了TimSort,它就是在reduce前,提前用算法进行了排序。  那么用算法的思想来说,合并的M N个元素进行排序,那么其复杂度为O(MNlogM) 具体算法不讲了~要慢慢看~

随之,当你没有足够的内存保存map的输出结果时,在溢出前,会将它们disk到磁盘,那么缓存到内存的大小便是 spark.shuffle.memoryFraction * spark.shuffle.safyFraction.默认的情况下是”JVM Heap Size * 0.2 * 0.8 = JVM Heap Size * 0.16”。需要注意的是,当你多个线程同时在一个executor中运行时(spark.executor.cores/spark.task.cpus 大于1的情况下),那么map output的每个task将会拥有 “JVM Heap Size * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction / spark.executor.cores * spark.task.cpus。运行原理如下图:

使用此种模式,会比使用hashing要慢一点,可通过bypassMergeThreshold找到集群的最快平衡点。

 

Tungsten Sort

使用此种排序方法的优点在于,操作的二进制数据不需要进行反序列化。它使用 sun.misc.Unsafe模式进行直接数据的复制,因为没有反序列化,所以直接是个字节数组。同时,它使用特殊的高效缓存器ShuffleExtemalSorter压记录与指针以及排序的分区id.只用了8 Bytes的空间的排序数组。这将会比使用CPU缓存要效率。

每个spill的数据、指针进行排序,输出到一个索引文件中。随后将这些partitions再次合并到一个输出文件中。

 

本文翻译自一位国外大神的博客:https://0x0fff.com/spark-memory-management/

### 三级标题:Spark Shuffle 过程详解及其工作原理 SparkShuffle 是分布式计算过程中非常关键的一步,它用于在不同阶段(Stage)之间传递数据。Shuffle 操作主要包含两个阶段:Shuffle Write 和 Shuffle Read。在 Shuffle Write 阶段,数据按照指定的分区规则写入磁盘;在 Shuffle Read 阶段,下游任务从多个上游任务中读取属于自己的数据分区[^3]。 ShuffleMapTask 是负责执行 Shuffle Write 的核心组件,它将 Map 阶段的输出数据根据分区器(Partitioner)进行分区,并通过 ShuffleWriter 写入磁盘。这一过程通常会将数据写入每个工作节点的本地磁盘,默认路径为 `/shuffle` 目录,但可以通过配置参数修改[^4]。 在 Shuffle Write 阶段,Spark 支持两种主要的 ShuffleManage 模式:HashShuffleManage 和 SortShuffleManage。HashShuffleManage 每个分区都会生成一个临时文件,这种方式在分区数较多时会产生大量的小文件,从而影响性能。SortShuffleManage 则通过排序的方式将多个分区的数据合并写入一个文件,减少了文件数量并提高了 I/O 效率[^1]。 Shuffle Read 阶段的任务是根据自身的分区信息,从多个上游任务的输出中拉取对应的数据。这一过程涉及网络传输,数据需要从源节点传输到目标节点,这可能会带来一定的网络开销,影响作业的整体性能[^4]。 SparkShuffle 过程还可以在不排序 records 的情况下完成 shuffle write 和 shuffle read,这种机制将 shuffle 操作无缝融入 RDD 的 computing chain 中,同时兼顾内存磁盘之间的平衡,并 Hadoop MapReduce 的 shuffle 机制在实现细节上存在差异[^2]。 以下是一个简单的示例,展示如何在 Spark 中触发 Shuffle 操作: ```scala val data = sc.parallelize(Seq((1, "a"), (2, "b"), (3, "c"), (1, "d"))) val shuffled = data.reduceByKey(_ + _) // 触发 Shuffle 操作 shuffled.collect().foreach(println) ``` 在此示例中,`reduceByKey` 是一个典型的宽依赖操作,它会触发 Shuffle,将数据按照 Key 进行重分区并聚合。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值