MapReduce 为何一定要排序
MapReduce 为何一定要排序
MapReduce是一种经典的分布式计算框架,广泛应用于大数据处理领域。其核心机制包括Map和Reduce两个阶段,通过简单的Key-Value数据格式实现复杂的计算任务。本文将深入探讨MapReduce中的排序机制,解释为什么排序对于提高数据处理效率至关重要。
MapReduce 机制
MapReduce 是一个非常经典的计算框架。
从数据格式的角度来说,仅仅用 Key-Value 这样简单的格式,就可以覆盖几乎所有的计算,大道至简。
从分布式计算的角度来说,抽象了 Map 和 Reduce 过程,把单机计算变成多节点的分布式计算,由此打开了大数据计算的大门。
经典的框架值得反复研究,Spark 一开始也想标新立异,抛弃MapReduce的一些实现,结果在不断的优化中仍然和 MapReduce 的理念非常像。
所以,在谷歌发布 MapReduce 论文的20年后,我们站在巨人的肩膀上来回忆一下 MapReduce 的设计理念。
MapReduce 是一种计算引擎,也是一种编程模型。MapReduce 提供了两个编程接口,即 Map 和 Reduce,让用户能够在此基础上编写自己的业务代码,而不用关心整个分布式计算框架的背后工作。
如下图所示为 MapReduce 运行所需要经过的环节。
以上五个环节还可以进一步分解成如图 5.2 所示的形式
以上过程分步骤描述一下:
创建 Split。由于 Map 任务最终是分布式的进程运行在不同的机器上,split 描述了每个 Map 任务该去哪台机器上的整块数据中,读取哪一部分的数据;
读取数据;
数据经过用户编写的 Map 业务处理,输入的是 Key-Value 格式,输出也是 Key-Value 格式。这一步其实是对数据标记的过程,为每条数据标记一个特征(key),相同特征的数据最终会到一起;
数据经过分区后,写入到内存缓冲区中;
内存缓冲区被写满 80%后,在内存中进行排序(先按照分区排序,每个分区内部按照 key 排序);
如果定义了 combiner 的话,进行一次合并;
每个 Map 溢写出一个文件出来;
最终对每个 Map 溢写出的文件合并成一个大文件;
进行一次 combine,写入到本地文件中;
进行到 reduce 阶段,每个 reduce 任务从上游数据中拷贝出属于自己的文件
调用用户定义的 reduce 方法进行计算;
最终结果写入到 hdfs 中
MapReduce 的排序
Map 端在对一个大文件进行计算的时候,首先要计算切片,每个切片分配一个 Map 任务。
这个 Map 任务计算之后,经过一个 partition 分区器,不同的 key 分配到不同的分区中。分区数量由下游的 reduce 数来决定。
经过分区后的数据,进入到一个环形缓冲区中,并且多次溢写之后,生成小文件。每个 Map Task 最终合并成一个文件。
如下图所示:
可以看到 merge 最后的一个文件中是分段的,第一段的数据是给下游的第一个reduce任务的,第二段的数据是给下游的第二个 reduce 的。
也就是下游的 每个 reduce 要来上游的每个 Map 任务的文件中,取到属于自己的那一段文件。
reduce 任务拉取完上游的各个分段数据之后,进行一次合并,合并到同一个文件中。
reduceTask 读取这个合并好的文件,把相同的key分组一次,进行计算即可。
现在问题就在这,如果 reduce 端合并之后的文件是乱序的,如下面这样:
a,1
c,1
a,1
b,1
....
那么reduce任务,如果要读取所有a的数据,就需要把整个文件从头到尾扫描一次,才能获取到所有a的数据。
如果有很多的 key,就得扫描多少次文件,肯定不是 MapReduce 引擎想要达到的效果。
那么如何读取文件一次,就可以把所有的 key 分组聚合好呢?
答案是:只要这个文件是有序的即可。
比如文件内容如下所示:
a,1
a,2
a,1
b,2
b,1
c,1
只要从上往下读取,只要读取到一个 key ,和上一个 key 不一样的时候,表示 a 都读取完了。
这样只要读取文件一次,就可以把各个 key 的特征汇总起来,就可以调用 reduce 的逻辑继续处理了。
既然 Reduce 端的输入需要 文件是有序的,而 reduce 阶段的文件是来自上游每个 map 的,只需要在上游环形缓冲区溢写之前,在内存中做一次快速排序,再溢写即可。
reduce 端拿到每段属于自己的数据之后,做一次归并排序,就可以很高效率的完成对整个文件的排序了。