彻底讲透Spark Shuffle与MapReduce Shuffle的区别

发布时间:2026/10/17 16:42:32
彻底讲透Spark Shuffle与MapReduce Shuffle的区别 刚开始接触大数据那几年我被一个问题反复折磨过Spark shuffle 和 MapReduce shuffle 到底差在哪。面试会问日常排查作业慢也会问但很多时候只能说出“Spark快、MapReduce慢”这种结论一旦被追问“为什么”就有点答不上来。后来我自己维护过几套离线计算集群把这层窗户纸捅破之后才发现表面上是 Shuffle 实现不同背后其实是两套执行引擎的设计哲学完全不一样MapReduce 追求稳定可靠宁愿把中间结果落得明明白白Spark 追求计算效率能留在内存就绝不写盘能合并 IO 就绝不多开文件。这篇文章就按我自己的理解把这层区别彻底拆开讲清楚适合正在准备面试的同学也适合已经被 Shuffle 问题困扰的开发者。1. 先搞清楚Shuffle到底在解决什么问题1.1 一个“重新分班”的动作在分布式计算里数据是分散在不同节点上的。做 group by、join 这类操作时系统需要把相同 key 的数据拉到同一个节点上才能进行后续聚合或关联。这个“按 key 把数据重新组织并跨节点搬运”的过程就是 Shuffle。你可以把它想象成全校重新分班每个学生在不同教室但最终要按照“兴趣爱好”把所有人重新编组那教务处就必须把所有学生名单收集起来按兴趣归类再分发到对应教室。这个步骤本身不产生新的业务价值却是绕不开的物理动作。由于涉及网络传输和磁盘读写Shuffle 是分布式计算中最昂贵、也最容易出问题的环节。我见过不少新手把 Shuffle 和排序混为一谈总觉得 Shuffle 就一定要先排序。其实 Shuffle 的核心是“按 key 分组定位”排序只是某些框架为了实现分组或文件合并而采用的手段。这也是后面两种框架差异的第一个伏笔。1.2 MapReduce和Spark都把Shuffle放在关键位置但出场次数不一样MapReduce 是严格的两阶段模型Map 阶段处理输入然后进入 Shuffle最后 Reduce 阶段汇总。无论业务多复杂整个计算过程都被切成一连串的 Map 作业和 Reduce 作业中间需要多次落盘、多次读写 HDFS。Spark 则把计算描述成一个 DAG有向无环图Shuffle 只会出现在“宽依赖”的边界处。同一个 Stage 内部的多个算子可以流水线执行一个算子的输出直接作为下一个算子的输入不需要立刻写盘。也就是说同一个复杂的 Group By Join 过滤 排序逻辑用 MapReduce 可能需要串联四五个作业而 Spark 可能只需要两三个 StageShuffle 次数少了一个量级。理解了这一点再看 Spark Shuffle 和 MapReduce Shuffle 的区别就不是简单背参数而是能想明白它们各自的设计取舍。2. MapReduce Shuffle的排序与落盘设计究竟牺牲了什么2.1 Map端的环形缓冲区与溢写合并MapReduce 的 Map 端 Shuffle 是整个框架的基石。MapTask 每处理一条记录会把 key-value 写进一个环形缓冲区这个缓冲区的大小默认在 100MB 左右由mapreduce.task.io.sort.mb控制。当缓冲区使用率达到阈值默认 0.8对应 80MB时后台线程开始溢写spill。溢写并不是简单地倒数据它在此之前会做两件事按分区Partition划分数据每个分区内再按 key 排序。为什么要排序因为 Map 阶段可能溢写多次每次都会生成一个临时文件最后任务结束时需要把这些临时文件合并成一个最终输出文件。如果每个临时文件内部都是有序的合并时只需要多路归并复杂度会低很多。当配置了 Combiner 时溢写过程中还会先做一轮本地合并减少写盘数据量。Combiner 是 MapReduce 提供的 map 端预聚合手段但要保证操作满足交换律和结合律且不能改变最终结果语义。实际生产中很多人为了省事只写一次 Combiner可能还会踩坑。完成合并后Map 端最终输出一个已分区且每个分区有序的数据文件以及一个索引文件。这个文件放在本地磁盘不是 HDFS。Reduce 任务会按索引去定位属于自己分区的数据段。整个过程给我的感觉就是所有中间状态都被安排得整整齐齐数据一旦落盘就是有序、可索引的后续 Reduce 无论如何出现都能按部就班地工作。2.2 Reduce端的Copy、Merge、Sort三件套Reduce 端拿到 Map 输出的位置信息后会启动多个并发线程去拉取属于自己分区的数据默认并发数由mapreduce.reduce.shuffle.parallelcopies控制默认值是 5。拉过来的数据先放在内存缓冲缓冲占堆内存的比例可以通过mapreduce.reduce.shuffle.input.buffer.percent调整超过了阈值就会写到本地磁盘。所有从不同 Map 任务拉来的数据最终还要经历一次“合并 排序”的过程。由于每个 Map 输出在分区内已经是有序的Reduce 端只需要做多路归并就能得到一个按 key 有序的输入流。MapReduce 随后通过比较器对 key 进行分组同一个 key 的所有 value 会连续递给一次 reduce 调用。这意味着 MapReduce 天然具备“按 key 排序”的输出结果。如果你需要做全排序只要 Reduce 数量为 1或者使用自定义分区器保证分区间的 key 范围有序就能直接拿到全局有序结果。这个特性在按 key 范围查找、有序导出报表时非常有用但代价就是为了这份有序性每个阶段都在做额外排序。2.3 这套设计牺牲了什么IO放大与长尾我拆过很多 MapReduce 作业的 Counter发现一个规律当数据量稍大时Spilled Records溢写记录数往往会达到输入记录的几十倍。环形缓冲的溢写、合并、Reduce 端的再次溢写和归并把本来只需一次读写的中间数据放大了很多倍。每条数据可能经历“内存写入 - 磁盘 spill - 多路归并 - 网络传输 - 内存 - 再次 spill - 再次归并”的漫长路径。在硬件资源有限的年代这种设计换来了极高的稳定性。每个阶段都落盘计算节点的故障可以通过重新调度来恢复Reduce 端对数据来源的依赖也被拆得很细。但是成也落盘败也落盘IO 放大直接拉高了作业延迟Reduce 端 copy 某个慢节点数据时还会形成典型的长尾。这些痛点正是 Spark 选择打破常规的直接原因。3. Spark Shuffle的演进路线从Hash到Sort再到Tungsten3.1 第一代Hash Shuffle快但文件爆炸Spark 早期版本默认使用 Hash Shuffle。它的思路非常直白每个 Map 任务或者说每个 ShuffleMapTask为下游每个 Reduce 任务生成一个文件MapTask 处理数据时按分区直接写入对应文件不做排序。由于少了一步全局排序在小规模作业里 Hash Shuffle 确实比 MapReduce 快不少。但这个设计有一个致命问题文件数量是 M 乘 R。假设有 10000 个 Map 任务、5000 个 Reduce 任务那就是五千万个小文件。大量文件句柄直接把节点磁盘和操作系统拖垮即使后来引入了 Consolidation 机制将同一个 Core 上连续执行的多个 Map 任务合并写入同一组文件文件数量也只是从 M*R 降到了“Core 数 × R”在大作业里依然不够看。这个坑后来成了 Spark 演进的重要动力快很重要但不能为了快把整个集群搞崩。3.2 Sort Shuffle用排序换文件数量但排序目的不一样从 Spark 1.2 开始Sort Shuffle 成为默认实现。每个 ShuffleMapTask 最终只写一个数据文件和一个索引文件数据文件内部按照分区编号排列相同分区的数据连成一段段内是否有序看具体场景。Reduce 任务拉数据时只需要根据索引文件定位到自己的偏移区间顺序读取即可。这时 Sort Shuffle 里的排序和 MapReduce 的排序目的已经不一样了。MapReduce 排序是为了让 Reduce 端能通过归并得到有序输入而 Spark 排序更多是为了把多个分区的数据紧凑地组织到一个文件里减少文件数量、方便索引。排序的对象也不是所有数据而是分区边界的信息。如果分区数不多并且不需要 map 端聚合Spark 还留了一个 bypass 开关当分区数小于spark.shuffle.sort.bypassMergeThreshold默认 200时直接走类似 Hash Shuffle 的写文件路径不排序直接把每个分区的数据写到同一个文件的不同段。这是 Sort Shuffle 对简单场景的妥协也是和 MapReduce“无论如何都排序”最大的差别排序是可选的不是必须的。3.3 内存优先与map端聚合省掉大量IO默认情况下reduceByKey、aggregateByKey这类算子会在 map 端做一个聚合操作也就是先把同一个 key 的多个 value 在内存里合并成一条再考虑是否溢写。这个能力在 MapReduce 里需要手动写 Combiner而且 Combiner 并不保证一定执行Spark 则直接从算子设计上内置了 map 端聚合数据量可以大幅压缩后再落盘。配合 Unified Memory统一内存模型Spark 可以让执行内存和存储内存之间动态借用同一个任务的内存充足时shuffle 数据可以完全不落盘只有内存压力过大才通过溢写来保证不 OOM。我在实践中见过一个 1TB 左右的 Group By 作业输入记录有几十亿条但由于 key 的基数不高reduceByKey 的 map 端聚合把输出压缩到了输入的几十分之一Shuffle 写的数据量比 MapReduce 少两个量级。这就是“能留内存就绝不写盘”策略的真实收益。再往后Tungsten 项目又做了缓存友好优化直接操作二进制数据减少序列化和反序列化开销在排序和聚合过程中尽量避免对象创建。这些优化叠加起来让 Spark Shuffle 在大数据量下依然能保持相对 MapReduce 的高性能和低 IO 放大。4. Spark shuffle 和 MapReduce shuffle 在工程实现上的硬核差异看完演进路线很多人心里已经有了一些碎片。我再从工程实现层面把两者放在一起逐项对比。对比项MapReduce ShuffleSpark Shuffle排序策略Map 端强制分区内排序Reduce 端归并排序默认 Sort Shuffle 文件内部按分区排布可走 bypass 跳过排序文件组织每个 Map 输出一个合并后的分区文件 索引文件每个 ShuffleMapTask 写一个 data 文件 一个 index 文件map 端聚合可选 Combiner需手动配置不一定执行reduceByKey/aggregateByKey 原生支持 map 端聚合内存优先中间数据落地spill 一次写一个临时文件多次 spill 多次落盘尽量保留在内存内存不足才溢写溢写后也尽量合并较少文件默认调优目标稳定性和可预测性优先容错靠重计算 磁盘缓冲性能和资源利用优先容错靠血缘和重算分区阶段模型固定两阶段Map - Shuffle - Reduce一个复杂作业串多个 jobDAG 划分 StageShuffle 只在宽依赖边界发生Stage 内管道化这张表看起来清晰但真正决定性能的是下面几个隐藏在细节里的取舍。4.1 排序定位MapReduce的强制特性Spark的可选手段MapReduce 在排序这件事上没有让步空间。即使你的业务完全不需要按 key 排序框架依然会在 map 端对每个分区排序再在 reduce 端归并。这个机制带来两个结果一是任何 MapReduce 作业的 shuffle 数据都是有序的二是排序开销和额外的 IO 是“固定税”不管你需不需要都得交。Spark 则把排序还原成了工具。默认 Sort Shuffle 用排序来整理文件结构但排序的对象是“分区段”不是给下游的“全局有序流”。当分区数较少、不需要 map 端预聚合、又显式关闭排序需求时Spark 可以直接绕过排序按分区顺序把数据写进文件段。这个灵活的“可选排序”机制让 Spark 在面对不同 shuffle 场景时都能找到近似最优的路径而 MapReduce 只有一条路走到底。4.2 内存与磁盘MapReduce“满了再落”Spark“能留就留”MapReduce 的内存管理是固定模型的环形缓冲区只有写满阈值才触发溢写reduce 端的内存缓冲有限一旦超过阈值就落盘。这种设计和当时 JVM 内存普遍较小、GC 能力有限的环境是匹配的缺点是中间数据生命周期非常短内存利用率并不高。Spark 的内存模型经历过一次重要演进后来采用 Unified MemoryExecution Memory 和 Storage Memory 之间的边界是弹性的同一块堆内内存可以在执行和缓存之间流转。shuffle 的聚合数据会尽量放进 Execution Memory内存不够才 spillspill 的数据还会做压缩以减少磁盘占用。换句话说MapReduce 是“到了阈值就必须落盘”Spark 是“先想办法在内存里做完实在不行才落盘”。这种差异在内存足够大的集群上会被放大Spark 的 Shuffle 读和 Shuffle 写往往比 MapReduce 小一个量级。4.3 阶段与流水线为什么Spark的shuffle次数更少MapReduce 的一个作业只有 Map 和 Reduce 两个阶段。如果你的逻辑是“过滤 - join - group by - 排序”它会被拆成多个 MR 作业前一个作业的输出写到 HDFS后一个作业重新读取。每次作业切换都是一次完整的 shuffle而且中间数据还会写三副本。Spark 在 DAG 里遇到窄依赖算子时会直接放进同一个 Stage多个算子连续执行不产生 shuffle。只有当遇到宽依赖比如 group by、join才切开 Stage。这样可以做到同一个 Stage 内一个算子的输出直接作为下一个算子的输入不落盘不序列化不走网络。最终一个复杂任务往往只需要在两三次 shuffle 边界处落数据而 MapReduce 同样的逻辑可能要经历五六次完整落盘。这个“减少 shuffle 次数”的能力是 Spark 在复杂 ETL 场景下明显快于 MapReduce 的重要原因。5. 用同一个Group By作业对比两种Shuffle的实际表现5.1 构造一个可复现的模拟场景为了更直观地理解两套 Shuffle 的差异我假设一个通用场景某离线数仓有一张用户订单明细表大约 100GB近 2 亿条记录需要按“用户ID”统计消费总额。集群规模约为 10 个数据节点每个节点 16 核、64GB 内存没有人为做特别调优只是默认配置。这个场景不算极端却能非常典型地反映两种框架在排序、落盘和聚合上的不同表现。5.2 MapReduce侧的配置和调参思路用 MapReduce 跑这个统计典型的调参方式有三步把mapreduce.task.io.sort.mb从默认 100MB 适当调大比如 200MB减少 map 端溢写次数。把mapreduce.reduce.shuffle.parallelcopies从默认 5 调大到 10 或 20加快 reduce 端数据拉取速度。合理设置 Reduce 数量一般取节点数和数据量的平衡点。如果只为了求全局排序Reduce 数量设为 1但会带来严重长尾所以这里不追求全局有序。在实际作业里Map 端会频繁溢写每个 MapTask 结束时还会做一次多路归并Reduce 端再从十几个节点的本地磁盘并发拉数据最后归并排序。整个过程中磁盘 IO 会非常高Counter 里Spilled Records通常膨胀到输入记录的数倍。5.3 Spark侧的配置和调参思路同一份数据用 Spark SQL 跑可以这样配置设置spark.sql.shuffle.partitions为 200让每个分区数据量更均匀避免某个分区过大。内存充足时可以适当调大 Executor 内存比如 32GB让 map 端聚合尽量在内存完成。确认spark.shuffle.sort.bypassMergeThreshold默认行为当分区数少于 200 时如果不需要 map 端聚合可以跳过排序这里虽然分区数等于 200但因为用了reduceByKey/SQL 聚合还是会走完整的 Sort Shuffle。在 Spark UI 的 Shuffle 信息里能看到Shuffle Write和Shuffle Read的量。由于 map 端聚合生效往往写入量远小于输入数据量溢写比例很低GC 时间也比 MapReduce 少很多。5.4 从结果反推原理差异在相似集群规模下这个场景的 MapReduce 版本可能需要二三十分钟Spark 版本一般可以在十分钟以内跑完。快的原因不是简单的“Spark 比 MapReduce 快”一句话而是排序不再是强制税Spark 只在必要的时候做分区段整理map 端聚合把同用户的记录先合并Shuffle 数据量大幅减少同一个 Stage 内的过滤和投影结果直接流向聚合不需要多次落盘DAG 只需一次 ShuffleMapReduce 则要把整个查询拆成至少两三个 MR 作业串联。这种差距在作业越复杂时越明显尤其是多表 Join、多级聚合的场景。6. 两类Shuffle的调优与排查经验实录6.1 从监控里快速发现Shuffle异常排查 Shuffle 问题先学会看监控指标比盲目调参靠谱得多。MapReduce 作业里重点关注Shuffle Bytes、Spilled Records、Reduce Shuffle Bytes这几个 Counter。如果Spilled Records是输入记录的几十倍说明内存配置太小或者溢写合并策略不合理如果 Reduce 的Shuffle Bytes巨大说明 map 端 combiner 没有生效或者 partitioner 选择有问题导致数据分配不均。Spark 作业里打开 4040 页面看每个 Stage 的Shuffle Write和Shuffle Read。正常情况下 Shuffle Write 不应远大于预期输出如果出现Shuffle Write接近输入数据量多半是 map 端聚合没生效比如groupByKey被误用如果Shuffle Read特别大且伴随大量磁盘溢写就要检查 Executor 内存和分区数设置。另外FetchFailedException是 Spark 里最常见的 Shuffle 异常之一。它看起来像是网络问题但很多时候根因是某个 Executor 崩溃或 GC 过久导致 shuffle 文件丢失。这时不是简单地重试而是先看失败的 Executor 日志确认是否存在内存溢出或节点负载过高。6.2 数据倾斜与长尾问题的解法差异数据倾斜是所有 Shuffle 场景的噩梦。表现是某个 key 的记录特别多导致一个 Reduce/分区拖慢整个作业。MapReduce 的经典解法是加盐两阶段聚合先在 map 端给 key 拼接随机前缀做一次局部聚合再去掉前缀做第二次全局聚合。这种方法在 Spark 里同样适用。但 MapReduce 没有运行期感知能力Reduce 数量一旦确定就无法在运行中动态调整。Spark 3 之后引入 AQEAdaptive Query Execution可以在 Shuffle 结束后根据统计数据动态合并小分区对倾斜的 Join 自动拆分倾斜分区的数据并用多个 Task 并行处理。我曾在一次倾斜比较严重的 Join 作业里靠开启 AQE 把作业时间从四十分钟压到十几分钟。这是 MapReduce 很难做到的运行时自适应能力。如果你还在用 Spark 2.x 且无法升级也可以手动加盐聚合或者用广播变量把小表广播到大表侧避免 Shuffle。这类经验在两类框架里都通用但 Spark 的算子粒度更细处理起来手段更多。6.3 常用参数速查表参数所属框架作用我的使用建议mapreduce.task.io.sort.mbMapReduceMap 输出缓冲区大小默认 100MB大作业调大到 200-400MB减少 spillmapreduce.map.sort.spill.percentMapReduce缓冲区溢写阈值默认 0.8内存充足可适当调高减少溢写次数mapreduce.reduce.shuffle.parallelcopiesMapReduceReduce 端并发拉取线程数默认 5集群网络稳定时可调到 10-20mapreduce.task.io.sort.factorMapReduce一次合并的最大文件数默认 10调大可以减少 merge 轮数spark.sql.shuffle.partitionsSparkSQL 作业 Shuffle 分区数默认 200根据数据量调一般 200-500 足够spark.default.parallelismSparkRDD Shuffle 默认并行度没有配置时容易过小需手动设spark.memory.fractionSparkSpark Memory 占 Executor 堆内存比例默认约 0.6如缓存少可调高spark.shuffle.sort.bypassMergeThresholdSpark分区数小于该值且无聚合时走 bypass默认 200小文件场景可调高减少排序spark.shuffle.compressSparkShuffle 数据压缩开关默认开启CPU 不紧张就保留spark.reducer.maxSizeInFlightSparkReduce 端拉取缓冲上限默认 48MB 或 96MB增大可提速但占内存这个表不需要背但建议在出问题的时候对照着排查比瞎试参数高效很多。7. 选型建议与实操体会7.1 哪些场景继续留在MapReduce更省心虽然 Spark 在很多场景下更快但 MapReduce 在一些特定场景仍有价值。如果你的作业体量很小、调度频率很低不想引入复杂的 DAG 和内存管理MapReduce 的稳定性依然让人放心。尤其是历史遗留系统里依赖大量 MR 作业短期内迁移成本大于性能收益继续使用也是合理选择。另外如果业务要求严格的全局有序输出MapReduce 的 sort-reduce 模型天生支持Spark 想要做到同样效果反而要额外控制分区键范围和 reducer 数量。我见过不少团队为了“Spark 一定更快”而强行迁移这类作业结果在排序细节上调试了很长时间性价比并不高。7.2 哪些场景换成Spark收益最大反过来迭代式计算、多阶段 ETL、复杂 Join、交互式 SQL、机器学习特征工程这些场景Spark 的优势几乎是碾压级的。只要内存资源允许shuffle 次数更少、map 端聚合更强、AQE 还能动态调优整体运维效率会明显提升。如果已经决定迁移建议先做一次“小数据量对比 大数据量压测”不要一上来就全量切。重点观察 Spark UI 中 Shuffle Write/Read 与输入数据量的比例确认聚合生效再对比历史 MR 作业的耗时用数据说话。7.3 我个人总结的三个排查习惯这几年踩过不少 Shuffle 的坑最后沉淀成三个习惯。第一个习惯是拿到一个慢作业先看 Shuffle 数据量与输入数据量的比值。如果比值异常高我会怀疑 map 端聚合失效或者分区逻辑不合理而不是急着加资源。第二个习惯是优先检查算子选型能用reduceByKey就不要用groupByKey能走 SQL 聚合就不要写程序收集 map很多所谓“Spark 慢”的问题其实只是算子选错。第三个习惯是调分区数之前先确认瓶颈是长尾还是磁盘 IO。如果是长尾加分区可能更糟如果是 IO加并行度或者调大 reduce 拉取缓冲才有效。Shuffle 是分布式计算里最容易出性能问题的地方希望这篇文章能让你少走点弯路。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询