上一篇《大数据(3):HDFS》分析了 Hadoop 的分布式存储框架 HDFS,这一篇将分析 Hadoop 的分布式并行计算框架——MapReduce。
〇、起源
MapReduce 源于 Google 一篇论文。
最早,Google 为了解决其搜索引擎中大规模网页数据的并行化处理,提出了一种面向大规模数据处理的并行计算模型和方法。
在 2003 年 和 2004 年,Google 在国际会议上分别发表了两篇关于 Google 分布式文件系统和 MapReduce 的论文,公布了 GFS 和 MapReduce 的基本原理和主要设计思想。
其中,《MapReduce: Simplified Data Processing on Large Clusters》这篇论文标志着 MapReduce 超大规模数据处理的第一次革命。
MapReduce 充分借鉴了“分而治之”的思想,将一个数据处理过程拆分为主要的 Map (映射) 与 Reduce (归约) 两步。简单地说,MapReduce 就是“任务的分解与结果的汇总”。
但到了 2014 年左右,Google 内部已经几乎没人写新的 MapReduce 了。
你可能有一个疑问 :为什么 MapReduce 会被取代?
事实是:MapReduce 是一种编程范式,更是一种思维方式。某个软件没有人用了,可以说这个软件被淘汰了,但是你不能说实现这个软件的思想被淘汰了。
可以说,原来用 Hadoop MapReduce 做的一些事情可以用其它引擎来做了,并不是 MapReduce 被淘汰了。所以,关于 MapReduce,我们更关注的应该是实现思想。
一、MapReduce 流程
先考虑一个最简单的 MapReduce 流程,主要分为 4 步:
Input:从数据源提取数据,输入到 Map Task 中。
Map Task:对输入片中的数据按照一定的规则映射成
(k, v)
形式。Shuffle:这是一个比较核心的过程,shuffle 有洗牌的意思,eg. 把红桃都发给 A Reduce,黑桃都发给 B Reduce,诸如此类。
Reduce Task:聚合,全部任务进行合并,即把分散的数据合并成一个大的数据,再对合并后的数据排序。
例如使用 MapReduce 进行单词统计。
在 Input 阶段:inputformat 按行读取文本,形成 (k, v) 形式,其中 k 是文本偏移量,v 是每行文本。
在 Map 阶段:将上一步的 v,也就是每行文本,按空格分隔,分隔后形成新的 (k, v),这时 k 是每个单词,v 是单词数量,目前每个单词数量都是 1,也就是 (word, 1) 这种形式。
在 Shuffle 阶段,把相同的 k 分组,例如所有的 (hello, 1) 将被分到同一组。
在 Reduce 阶段,将每一组的结果聚合,例如某一组有 3 个 (tom, 1),经过 reduce 聚合后变成 (tom, 3)。
形象的说:你需要统计一个文本里面有多少个单词。
- 把文本按行分割,每一行交个不同的人(Input)。
- 每人计算自己手里有多少个单词(Map)。
- 将相同的单词分到同一个组中(Shuffle)。
- 你将所有组的结果综合起来,得到总的单词数量(Reduce)。
但是这有几个问题,如果文本有一千行,而只有 3 个人,该怎么分配呢?在综合结果时,单词总数太多,你一个人忙不过来,怎么办?
二、InputFormat
在单词统计时,并不是帮你统计的人越多越好,也就是说在 MapReduce 程序的运行中,并不是 MapTask 越多就越好,需要考虑数据量的多少及机器的配置。
那么需要多少个 MapTask 呢?这取决于输入 InputFormat 的切片数量。InputFormat 默认的实现是按照 HDFS 的 block 大小 (128M) 进行切分 (split)。一个切片就对应一个 MapTask 实例。
假如我们有一个 300M 的文件,它会在 HDFS 中被切成 3 块:0 ~ 128M,128 ~ 256M,256 ~ 300M,然后将这 3 块放置到不同的节点上去(前提是节点数量大于 3,否则将以多线程方式运行)分别启动 MapTask。
也就是说,每一个 split 分配一个 MapTask 并行实例处理
这是由于在实际过程中,如果 MapTask 读取的数据不在运行的本机,则须通过网络进行数据传输,这对性能的影响非常大。所以常采取的策略是就按照 block 块的存储切分 MapTask,使得每个 MapTask 尽可能读取本机的数据。
当文件进过 InputFormat 切分后,再通过 Reader 读入到 MapTask 中,这是由于 MapTask 的输入输出形式为 (k, v),所以 Reader 的任务是将输入文件转成 (k, v)。
例如在单词统计时,输入是文本,Reader 将文本按行切割后,转成 (k, v),其中 k 是文本偏移量,v 是每行文本。
三、MapTask
MapTask 主要分为以下几部分组成:map 函数、Partitioner、Combiner。其中 map 函数是必须的,Partitioner 和 Combiner 是可选的。
1. map 函数
在 map 函数中,接受 Reader 传来的每行文本,编程将每行文本按照空格分隔,形成新的 (k, v),例如 (word, 1)。
2. Partitioner
分区。按照一定规则,把数据分成不同的区。
Partitioner 决定 MapTask 输出的数据交由哪个 ReduceTask 处理,也就是说 Partitioner 分区数 = Reduce 个数。
分区规则可通过编程自定义,默认是按照 key 的 hashcode 进行分区,相同 hash 值的 k 将被分到相同的区。同一个分区中的 (k, v) 最后将进入同一个 reduce。
但如果某个 k 大量存在,则它们的 hashcode 也相同,那么这些数据将会进入同一个 reduce 中,而其他的 reduce 中的数据很少,这将造成数据倾斜。
例如,有一万个单词需要统计,其中九千个是 hello,如果使用默认的分区方法,那么这九千个 hello 都将进入同一个 reduce 中,而其他 reduce 中的单词很少很少。
解决数据倾斜的一个常用办法是重写分区函数。
5. Combiner
Combiner 的作用顾名思义,就是聚合。不是 reduce 的作用就是聚合吗,怎么 Combiner 也聚合?
没错,Combiner 和 reduce 的作用很接近,都是聚合。举个例子,如果在某个 map 中,有一千个 (hello, 1)
,那么这一千个都需要经过网络 shuffle 后,进入 reduce,这样大大增加了网络压力。
如果在网络 shuffle 之前,先在 map 中局部聚合,把一千个 (hello, 1)
变成一个 (hello, 1000)
,这样一来,大大减小了网络压力。
但是 Combiner 不是万能的,例如在统计单词,或者统计最大最小值时,Combiner 是非常有效的,例如:
max(0, 20, 10, 25, 15) = max(max(0, 20, 10), max(25, 15)) = max(20, 25) = 25
但在统计平均值时,就不能使用 Combiner,例如:mean(0, 20, 10, 25, 15) = 14
但是:mean(mean(0, 20, 10), mean(25, 15)) = mean(10, 20) = 15
四、shuffle
将 map 阶段的输出,传入 reduce 阶段的输入,这个过程称为 shuffle。
map 函数产生输出时,并不是简单的将数据写入磁盘,每个 map 任务都有个 buffer,buffer 的默认大小为 100M,当 buffer 内容达到阈值 (默认80%) 时,一个后台线程便开始把内容溢出 (spill) 到磁盘。
在溢出写磁盘的过程中,map 的输出继续写入 buffer,如果在此期间 buffer 被填满,map 会被阻塞,直到写磁盘过程完成。
若有 combiner 函数,则会使 map 的输出更紧凑,因此减少写到磁盘的数据和传递给 reducer 的数据。
五、ReduceTask
ReduceTask 的任务是将 MapTask 的输出进行聚合。主要包括分组 (group) 和调用 reduce() 函数两部分。
分组,默认使用的是 WritableComparator 比较器进行对 key 值的比较,key 值相同的会被分在一组。而 reduce() 函数是按照组为操作对象进行统计的,也就是有多少个组,则调用几次 reduce() 函数。
一个完整的 MapReduce 流程如下:
六、MapReduce on YARN任务调度流程
第 1 步:客户端 Client 发起一个作业请求,即执行我们的代码,在哪台机器执行,哪台机器就是客户端。
第 2 步:Client 会向 ResourceManager 申请一个 ApplicationID,作为作业的唯一标识。
第 3 步:标识成功后,客户端会将作业上传到 HDFS,因为 slave 在跑作业的时候,会直接从集群里面拿这个计算程序,这也是分布式计算原则的体现,尽可能移动计算而不是移动数据。
第 4 步:Client 上传作业到 HDFS 后,会真正提交作业给 ResourceManager,ResourceManager 此时,会得到此作业的很多信息,包括需要多少内存、多少个CPU等等。
第 5a 步:Client 提交作业后,ResourceManager 会根据集群资源情况,找一个合适的从节点机器启动一个 Container。
第 5b 步:在此 Container 里面启动一个 MRAppMater 进程。
第 6 步:MRAppMater 进行一些初始化操作。
第 7 步:MRAppMater 查询 HDFS 上的作业中有多少 input splits,一个 input splits 对应一个 MapTask,需要知道执行多少个 MapTask,多少个 ReduceTask,这些需要计算的块分布在哪里等信息。
第 8 步:MRAppMater 根据上一步衡量了作业需要多少资源,应该怎么分配资源后,向 ResourceManager 申请资源。
第 9a 步:MRAppMater 申请到资源后,与有资源的 NodeManager 通信
第 9b 步:NodeManager 启动相应的 Container,在这里面会启动一个执行作业的进程,进程里面会跑 Map Task 与 Reduce Task,即 MapReduce 的两个阶段。
第 10 步:执行作业的进程会去 HDFS 上拉去相应的作业。
第 11 步:拉去到作业后,执行相应的MapTask、ReduceTask。
至此,一个完整的 mapreduce 流程执行完成。