1. 先厘清概念:为什么Shuffle这个词被Spark和Hadoop都用了
我在面试候选人的时候,经常问一个问题:“Spark的shuffle和Hadoop的shuffle,到底是不是一回事?”
相当多的人第一反应是:都是数据重分区嘛,总不能翻出花来。但真让他们说出个一二三,就开始含糊了。这不能全怪候选人,因为这俩东西名字一样,解决的问题也确实类似,但底层的设计哲学和执行路径差了十万八千里。如果你只是把它们当成同一个东西去理解,遇到实际问题——比如Spark作业莫名OOM、Hadoop任务卡在shuffle阶段——根本没法定位。
Shuffle这个词,直译是“洗牌”。在分布式的语境下,它指的就是数据在集群节点之间重新分布的过程。一个MapReduce任务,Map阶段处理完的数据分散在各节点上,接下来Reduce阶段需要把相同key的数据拉到一个节点处理,这一步就是shuffle。整个过程涉及磁盘读写、网络传输、排序归并,是大数据计算里最耗费资源、也最容易出问题的环节。
Hadoop MapReduce 从2004年论文里就把这套机制定义得很清楚,当年数据库界的大牛David DeWitt甚至说过一句名言:“We love shuffle”(我们爱shuffle),因为shuffle是分布式数据库和分布式计算框架绕不开的核心操作。
Spark之所以沿用shuffle这个词,并不是要从概念上跟Hadoop对齐,而是它面对的问题本质相同:DAG计算中,父RDD的数据经过map后,需要按照目标RDD的分区规则重新分布。既然底层问题一样,词就借过来了。但Spark对shuffle的实现,不是“Hadoop那套换个语言重写一遍”,而是针对内存计算和DAG迭代的场景做了彻底重设计。
这篇文章我就把两边掰开来聊,先讲Hadoop shuffer的完整链路,再讲Spark shuffle的演进和现状,最后给你一张能直接写进面试答案或设计方案里的对照表。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Hadoop MapReduce的Shuffle:一次写盘到底的硬派做法
Hadoop MapReduce的设计目标非常明确:跑超大规模批处理任务,数据量几十TB甚至PB级别,集群节点可能几百上千台,且节点宕机是常态。在这种前提下,它选择了最稳、最保守但绝对可靠的shuffle实现。
2.1 Map端:环形缓冲区、Spill与排序
Map端输出的结果并不是直接写到磁盘上送给Reduce的。MapTask启动之后,会先创建一个环形内存缓冲区(默认大小100MB,由mapreduce.task.io.sort.mb控制),Map的输出会以key、value、partitionId、offset等格式写入这个缓冲区。
当缓冲区的使用率达到阈值(默认80%,由mapreduce.map.sort.spill.percent控制),一个后台线程就会启动“Spill”(溢写)过程。这时候注意一个关键细节:在真正溢写之前,后台线程会把缓冲区里的数据按照partitionId和key做一次排序——这就是为什么Map端也一定要有排序,因为Hadoop的shuffle默认就是“sort-based shuffle”,后续Reduce拉取数据时,必须依赖每个Map输出片段是有序的这个前提。
排序完成后,数据会被写入本地磁盘,形成一个Spill文件。如果整个MapTask的输出量很大,可能会触发多次Spill,那么最终在MapTask结束前,还会把所有Spill文件做一次Merge,合并成一个有序的最终输出文件,同时生成一个索引文件来记录每个分区数据在文件中的偏移量。这里有个细节:合并时如果配置了Combiner,并且Spill文件数量超过阈值(默认3),会在Merge过程中执行Combiner,提前做一轮Map端局部聚合,减少网络传输的数据量。
2.2 Reduce端:拉取、归并与二次排序
ReduceTask启动后,会启动若干个Fetcher线程(默认5个)去各个MapTask节点拉取属于自己的分区数据。这个拉取过程不
