一、本章概览
MapReduce可以看作是Hadoop中的分布式计算框架,是用于批量数据离线处理的编程模型。基于MapReduce的并行数据处理是Hadoop能够支撑大数据计算的核心。
书中这一章是以一个实际的例子对MapReduce的过程、机制还有Hadoop提供的相关编程模型及借口做了简单的介绍,内容即非常易懂,也能让读者初步地宏观了解MapReduce的计算原理。其中很多细节的地方书中并没有做详细介绍,比如是如何做数据的备份容灾,集群中机器间数据传输的机制,以及partition、shuffle的具体实现等等。这部分可以后续放在进行理论上的深入了解,或者说准备在后面的源码阅读部分去进一步学习。
二、核心内容
这里先把本章的重点内容梳理一遍,下一片博客会介绍下Hadoop环境的配置、基于IDEA的本地开发环境搭建以及气象分析的实际例子+代码。本章开头介绍了Unix脚本进行文件处理,以及利用awk工具来进行数据处理和计算,这里主要的目的是想表达单一进程的程序处理大量的数据存在效率的问题,如果进行多线程并行处理,在没有机制的约束下,又回出现很多问题:
- 比如如何进行合理的文件划分。天气的数据以年为单位存放在多个文件中,不同年份的数据量大小不一,如何保证每个作业(线程)处理的数据量大小是相同的呢?(因为如果分配的大小不一,那么总的计算时间仍然取决于执行最长文件的处理时间)
- 在实现了第一点的基础上,得到了每个作业的中间计算结果。由于需要进行尽量均等的文件划分,那么就不能保证每个作业都能完整处理一年的数据。那么要得到我们想要的结果,即每年的最高气温,那么还需要对结果进行合并处理的过程。
- 即便能解决以上问题,基于多线程的计算面对大规模的数据时,计算力仍然是远远不够的。(单台机器的计算力上限摆在那里)
所以,我们需要Hadoop这样的可以在分布式集群中进行大规模数据处理的框架。MapReduce可以粗分为Map和Reduce两个阶段,在每个阶段都以键值对(k-v)的形式处理输入数据,输出数据。多个Map任务接收输入数据之后,按照定义的Map函数对数据进行处理,得到的中间结果会在集群中通过网络分配到对应执行Reduce任务的机器上,继续进行Reduce任务。每个Reduce任务得到的结果(最终结果)会通过Hdfs持久化到机器的硬盘上。那么,输入数据的形式是什么,Map任务是如何创建的?又是如何分配到对应的Reducer的呢?
MapReduce的详细过程可以分为这样七步:(初步的理解)
- Input:这一步对(按block分块)存储在Hdfs上的数据进行读取并生成键值对,具体的数据类型,Hadoop这里进行了单独的封装,并没有使用java原生的数据类型。这里的键可以是文件名,可以是数据的行偏移量等等。
- Map:Map函数接受键值对形式的输入,并进行数据处理。比如在气温分析的例子中,进行了年份和温度的提取。Map输出的中间结果会被缓存。
- Partition:如果对应有多个Reduce任务,那么数据需要进行分区,分区会被分配到对应的Reducer执行。每个分区可能包括多个键对应的数据。分区后的结果会周期性地写入机器的本地硬盘。
- Shuffle:在这一阶段,不同分区的数据被分配到对应执行Reduce任务的worker上(Reducer从对应Mapper的机器上进行数据读取)
- Sort:Reducer读取完所有的中间数据后,通过key进行sort,使相同key的数据聚合在一起(不同key的数据可能会被分配的同一个Reducer)。
- Reduce:按照定义的Reduce函数对数据进行进一步处理。
- Output:Reduce函数的输出被追加到所属分区的输出文件上(Hdfs)。第一个副本会存储在本地节点,其他副本处于可靠性考虑,分发到不同机架上的节点。
这其中,只有第1、2、7步是必要存在的,一个MapReduce数据流的梳理可以只有Map任务。上面过程中的部分数据传输是非本地的,比如Mapper–》Reducer,即shuffle过程,还有输入、输出的过程(有从Hdfs其他节点上读取存储的数据进行MapReduce的情况)。
这整个过程下来,有几个难点,包括Map任务的数量、输入数据如何分片、节点挂掉(任务失败)、数据跨节点传输需要占用大量网络带宽等等,我们看下Hadoop是如何解决的。
- Map 任务的数量:Map任务的数量由存储在Hdfs上数据块的数量决定,一个数据块对应mapreduce中的一个split,Hadoop会为每一个split创建一个map task。
- 输入数据如何分片:将数据进行分片,并且保持分片的数据量大小较小,是为了能够更好地进行分布式的处理,实现负载平衡(算力更好的机器可以处理更多的分片)。这里其实有两个问题,即合理的分片大小和如何在创建Map任务时尽量避免数据的非本地传输。(1)Hdfs上的数据按照block size被分为等长的块,默认的大小是128m。MapReduce的split大小一般任务跟block大小保持一致为佳。理由是如果split_size > block_size,那么每个分片会跨越多个数据块,执行map任务的worker需要从多个节点去读取数据。如果split_size < block_size,那么同样的道理,一个block会需要被多个mapper读取数据,占用带宽资源。
- 节点挂掉(任务失败):分布式集群比较容易出现的就是某些节点的故障。如果出现节点挂掉的情况,该任务会被置为出事的空闲状态,Hadoop会在另一个节点上重新运行该任务,并通知所有的Reducer从新的节点读取数据(如果是Map任务)。对于集群中master节点挂掉的情况,Hadoop也设计了一套机制来保障容灾,基本原理是进行周期性地进行数据持久化,不在这里进行详细介绍。至于如何发现故障节点,这个在以后进行介绍。
- 数据跨节点传输需要占用大量网络带宽:这个问题跟第二个问题也有关系。Hadoop会优先进行数据本地化优化,即存储该数据块的节点,就在本地创建Map任务,如果该节点正在运行其他Map任务,则会需要从同一机架上寻找空闲的节点。极少的情况下,会出现跨机架的创建Map任务。这样可以尽量保证减少带宽占用。这里的本地化优化只针对Map任务,不适用于Reduce任务(Reducer始终都需要从非本地的Mapper去读取数据----一个Reducer几乎要从所有的Mapper上去读取数据)。
本章还提到了combiner,在我理解这属于一种部分情况下可行的节约Mapper–》Reducer的网络带宽资源的一种机制。这里举个例子说明:在气温分析的场景中,我们在Map函数中提取年份和气温的信息,然后shuffle到Reduce端,对每一个年份(键)进行Max操作求得最高气温。那么我们为什么不在Mapper端就先预进行一次max操作,得到一个中间结果呢。
即,每个map task上都包括数个年份的数据,如果我们在这里通过combiner先调用一次reduce函数,那么一个map task输出的多个键对应的值就只会有一个,而不是一个集合。这样从Map端传输到Reduce端的数据量就会小很多。但是结合实际情况一想,在大多数场景下,可能都不满足max操作这样的可结合性。这里需要结合具体场景进行优化,并不是通用的。