MapReduce研究报告
1 MapReduce简介
在过去的数年里,Google的许多员工实现了很多基于特殊应用的计算,用来处理海量的原始数据,比如文档爬虫、Web请求日志等。为了计算各种类型的数据,比如倒排索引,Web文档的图结构的各种表示,每天被请求数量最多的搜索集合等等。这样的计算在概念上很容易理解,但是,输入的数据量极大,只有计算被分布在成百上千的机器上才能在可以接受的时间内完成。怎么样并行计算,分发数据,处理错误,所有这些问题综合在一起,使得原本很简单的计算,因为要处理大量的分布式计算的问题,而变得极其复杂。为了解决这个问题,Google公司的Jeffery Dean设计了一个新的抽象模型,使我们只要执行简单的计算,而隐藏并行化、容错、数据分布、负载均衡那些杂乱的细节。此类抽象模型的灵感来自Lisp和许多其他函数语言的Map和Reuce的原始表示。事实上,很多计算都包含这样的操作:在我们的输入数据上应用map操作,来计算出一个中间的key/value对;在所有具有相同key的value上应用reduce操作,来适当的合并数据。功能模型的使用,再结合用户指定的map和reduce操作,使得我们可以非常容易的实现大规模并行化计算,同时使用重启机制可以很容易的实现容错。这个工作的主要贡献是通过简单有力的接口来实现自动的并行化合大规模分布式计算。结合这个接口的实现能在大量普通的pc机上实现高性能计算。
2 MapReduce编程模型
MapReduce框架的运作完全基于<key,value>对,即数据的输入是一批<key,value>对,生成的结果也是一批<key,value>对。只是有时候他们的类型不一样而已。
Map任务由用户编写,Map任务的输入是一个键值对,输出一些中间的键值对集。MapReduce库会将键相同的值组合到一起,然后传递给Reduce函数。Reduce函数也由用户自己编写,Reduce函数接受Map产生的中间键以及该键对应的一个值的集合。
2.1 示例
WordCound是Hadoop自带的一个例子,目标是统计文本文件中单词的个数。假设有如下两个文本文件来运行WordCount程序:
File1: Hello World ByeWorld
File2: Hello Hadoop GoodBye Hadoop
下面是程序的简单实现:
l Map数据输入
Hadoop针对文本文件缺省使用LineRecordReader类来实现读取,一行一个key/value对,key取偏移量,value为行内容。
如下是map1的输入数据:
Key1 Value1
0 Hello World Bye World
如下是map2的输入数据:
Key2 Value2
0 Hello Hadoop GoodBye Hadoop
l Map输出
如下是map1的输出结果:
Hello 1
World 1
Bye 1
World 1
如下是map2的输出结果:
Hello 1
Hadoop 1
GoodBye 1
Hadoop 1
如果只有一个Reduce任务,那么Map的输出不会分区,所有的键值对都会存储在一个文件块中;如果有多个reduce任务,则每个map任务都会对其输出进行分区,即为每一个reduce任务建一个分区。每个分区都有许多键(对应的值),但是每个键对应的键/值对记录在同一个分区中。分区由用户定义的分区函数控制,但通常默认的分区器(partitioner)通过哈希函数来分区,这种方法很高效。由于每个map任务都使用同一个分区函数,因此,相同的键对应的存储的分区是一样的。举个例子:假设上面的程序设置了两个reduce任务,而两个输入文件中都有Hello单词,而Hello键对应的键值对都会存到同一个分区。
对于有两个reduce任务的情况:
Map1的输出有两个分区,分区一和分区二:
我们假设Hello键在进行哈希分区时会映射到一分区,而Bye 和World会映射到二分区,那么,一分区的存储的内容如下:
<Hello, 1>
<Bye, 1>
二分区存储的内容如下:
<World, 1>
<World, 1>
Map2的输出也有两个分区,分区一和分区二.
分区一的内容:
<Hello, 1>
<GoodBye,1>
分区二的内容:
<Hadoop,1>
<Hadoop,1>
l Reduce输入
Reduce任务的处理过程总共有三个阶段,即拷贝阶段,排序合并阶段,用户定义的reduce阶段。拷贝阶段用于从map节点拷贝数据到reduce节点端。排序阶段,系统会自动将所有的键分组,然后将键相同的值排序。最后的用户定义reduce函数才是真正的执行用户定义的操作。
因此,如果只有一个reduce任务,那么该任务先从两个map节点将所有的键值对拷贝到reduce端,然后所有的键相同的值合并到一起,最后执行reduce操作。
一个Reduce
因此,从map1得到的输入是:
<Hello, 1>
<Bye, 1>
<World, 1,1>
从map2得到的输入是:
<Hello, 1>
<GoodBye,1>
<Hadoop, 1,1>
如果有两个reduce任务,那么从reduce1得到的是来自分区一的数据,
Map1 分区一
<Hello, 1>
<Bye,1>
Map2 分区二
<Hello, 1>
<GoodBye,1>
合并成
<Hello, 1,1>
<Bye,1>
<GoodBye,1>
上面就是reduce1的输入
Reduce2得到的来自分区二的数据:
Map1, 分区一
<World, 1>
<World, 1>
Map2 分区二
<Hadoop,1>
<Hadoop,1>
合并成
<World,1,1>
<Hadoop, 1,1>
上面就是reduce2的输入
l Reduce输出
如果只有一个reduce任务,那么输出比较简单,就是所有单词的计数
如果有两个reduce任务,那么每个reduce任务输出不一样
Reduce1
<Hello, 2>
<Bye,1>
<GoodBye, 1>
Reduce2
<World, 2>
<Hadoop,2>
2.2 其他示例
分布式的Grep(Unix工具程序,用于文件内部的字符串查找):如果输入行匹配给定的样式,map函数就输出这一行。Reduce函数就是把中间爱你数据复制到输出。
计算URL访问频率:map函数处理web页面的请求记录,输出(URL, 1). Reduce函数把相同的URL的value都加起来,产生一个(URL, 记录总数)的对。
倒排索引:map函数分析每个分档,然后产生一个(词,文档号)对的序列。Reduce函数接收一个给定词的所有对,排序相应的文档的所有ID,并产生一个(词,文档ID列表)对。所有的输出对集形成一个简单的倒排索引。它可以优化对词位置的查找。
3 MapReduce的实现
MapReduce框架可以在很多中环境中实现。可以在单机单核共享内存的环境中实现,也可以在单机多核共享内存的环境中实现,同样的,还可以在分布式的集群中实现。下面给出的实现主要是基于分布式集群的环境的mapreduce实现。
下面是google的一个常用的集群环境:
1. 机器是x86双核处理器,linux系统,2-4GB内存
2. 网络传输带宽通常是100M/s
3. 集群通常由成百上千的机器组成,因此,机器故障是很常见的事情。
4. 存储通常使用便宜的IDE驱动的磁盘,并且使用分布式文件系统来管理磁盘。在不可靠的集群中,分布式文件系统利用文件拷贝来实现文件存储的可靠性和稳定性。
Map任务的输入文件通常被分成M块,这些文件块能被很多机器并行的处理。Map产生的中间键值对被分区函数(例如hash(key) mod R)分成R块. 分成多少块以及使用什么分区函数通常由用户指定。分区函数通常默认的就是哈希函数。
3.1MapReduce执行过程
图1 MapReduce执行过程预览
- MapReduce库将输入文件分割成M块(通常每块16-64M,由用户通过参数控制)。然后开启集群上各个节点的拷贝程序。
- 程序的一个特殊的拷贝是Master节点的拷贝。其他的节点用来分配Map任务和Reduce任务。Master节点挑选空闲的工作节点分配Map任务或者Reduce任务。
- 被分配了Map任务的节点读取相应的输入分块。工作节点将输入文件的内容转换为键值对,Map函数产生的键值对缓存在内存中。
- 存在内存中的键值对会被周期性的写入本地磁盘,并且这些键值对会被分成R份分开存储。不同的键值对的存储位置会被传送到Master节点。然后,Master节点会根据各个节点的使用情况选定进行Reduce工作的节点。
- 当键值对存储的位置由Master节点传送给Reduce节点之后,Reduce节点通过远程调用读取Map节点的磁盘上的键值对。当一个Reduce节点读取到了所有的Map产生的中间键值对之后,Reduce节点利用键值进行分组,将所有的键相同的数据存到一起。这是一个必须的过程,是因为通常许多键Map到同一个Reduce任务上。如果Map任务产生的中间数据太大以至于不能全部存到内存中,那么就要使用到外部排序,会影响处理的效率。
- Reduce工作节点不断的重复对Map产生的键值对数据的排序过程,并且将每一个检测到的键对应的值的集合传递给用户定义的Reduce函数。Reduce函数的最终的输入被添加到一个输入文件。
- 当所有的Map任务以及Reduce任务都已经完成,MapReduce程序执行产生的输出文件存在R个输出文件文件中(每个Reduce任务的输出文件都可以由用户指定)。一般来说,用户并不需要将这R个文件整合成一个大的文件,因为,这些文件通常作为下一个MapReduce任务的输入。
3.2 Master节点的数据结构
Master保存了一些数据结构。它为每一个map和reduce任务存储他们的状态(空闲,工作中,完成)。Master节点像一个管道,通过它,map任务产生的中间文件区域的位置传递给reduce任务。对于每个完成的map任务,master存储由map任务产生的R歌中间文件区域的大小和位置。当map任务完成的时候,位置和大小的被传递给master节点,然后master节点将这些信息传递给reduce任务。
3.3 容错
因为MapReduce库被设计用来使用成百上千的机器集群,以处理海量的数据,因此,要能够很好的处理机器故障。
Worker故障
Master节点会周期性的pingworker节点,如果积累到一定的次数发现worker节点没有响应,那么就认为worker节点失败了。任何被这个失败的工作节点完成的map任务会被重新调度至其他工作节点重新运行。同样的,任何正在执行的mapreduce任务也会被重新调度至其他工作节点重新运行。
出现故障的机器的已经完成的map任务需要被重新执行,因为map任务存储在该工作节点的本地磁盘,但是该输出已经不能访问到,因此,需要重新执行。完成的reduce任务不需要被重新执行因为他们的输出存储在HDFS中。
如果一个map任务首先被节点A执行了,然后又被节点B执行了一次(因此A出现故障),所有的正在执行的reduce任务都会被通知出现此故障。任何没有执行完成的从节点A读取数据的reduce任务,会选择从节点B读取数据。
Master故障
Master节点故障是一种最严重的情况。目前,没有比较好的处理这种失败的机制—因为它是一个单点故障---因此,在这种情况下,任务注定要失败。然而,这种失败发生的概率很小,因为具体某台机器失败的几率很小。未来的版本的Hadoop可能通过运行多个master节点来解决这个问题,任何时候都只有一个master主节点。可以使用Zookeeper作为多个master的协调机制来决定哪个master作为主节点。
3.4 本地化
网络带宽一直是计算领域的稀缺资源。在分布式计算领域,网络传输经常成为分布式计算的瓶颈。
MapReduce的输入文件分布式的存储在集群的各个机器的本地磁盘上。Hadoop文件系统会将输入文件分成很多大小一定的块(通常是64M),并且会拷贝一定数量(通常是3个,也可以自己设定)分块存储到不同的机器上。考虑到这个特点,Master节点在分配Map任务的时候,会尽量把Map任务分配到存有输入文件分块的拷贝的节点上。如果前面这种分配任务不成功,也就是说,存有输入文件的节点正处于忙碌状态,那么,Master节点会将Map任务分配在离存有输入文件较近的节点上,以降低网络传输数据的影响。
4 MapReduce的执行
4.1 推测执行
MapReduce模型将作业分解成任务,然后并行地运行任务以使作业的整体执行时间少于各个任务顺序执行的时间。这使作业执行时间对运行缓慢的任务很敏感,因为只运行一个缓慢的任务会使整个作业所使用的时间远远长于执行其他任务的时间。当一个作业由几百个或者几千个任务组成时,可能出现少数“拖后腿”任务是很常见的。
任务执行缓慢有很多种原因,包括硬件老化或软件配置错误,但是,检测具体原因很困难,因为任务总能够成功完成,尽管会比预期的执行时间长。Hadoop不会尝试诊断或修复执行慢的任务,相反,在一个任务运行比预期慢的时候,它会尽量检测,并启动另一个相同的任务作为备份。这就是所谓的任务的“推测执行”(speculative execution).
当集群中出现了空闲节点时,Hadoop选择尽量充分利用资源,选择其他任务在此节点上运行:
l 选择失败的具有最高优先级的任务
l 选择没有运行的任务。对于map任务,会选择数据存在本地磁盘的节点优先运行。
l 如果上面两种可能没有出现,Hadoop会执行推测式任务。也就是说,Hadoop会推测出运行比较慢的节点,然后拷贝任务以及数据,在这个空闲的新的节点运行,而同时,运行比较慢的节点上的任务会继续同步运行。
为了选择一个“拖后腿”任务拷贝运行,Hadoop监控每个任务的运行进度,通过进度值来比较任务的运行状况,进度值的范围是0-1.
对于Map任务,进度值就是Map任务读取的输入文件的百分比,而对于Reduce任务,执行被分成三个阶段,每个阶段所占的比值是1/3.
在拷贝阶段,reduce任务获取Map的输出
排序阶段,reduce任务根据键对map输入排序
Reduce阶段,用户定义的函数的执行阶段。
在reduce任务的每一个阶段,进度值就是数据被处理的百分比。例如:在reduce阶段,如果拷贝任务拷贝了一半数据,那么进度值就是1/2 * 1/3 =1/6; 如果在reduce阶段,处理的数据量是一半,那么进度值是:1/3 + 1/3 +(1/2*1/3) = 5/6
Hadoop算出所有任务的平均执行进度,并且定义一个推测任务的临界条件:当一个任务的如果一个任务的执行进度小于平均执行进度减去0.2的值,并且这个任务已经执行了至少一分钟,那么这个任务就被标记为“拖后腿”的任务。
Hadoop调度器执行良好的前提:
- 所有节点工作频率几乎一样
- 所有任务在运行过程中几乎保持一样的执行速率
- 不考虑推测式任务执行的开销(从一个空闲节点执行拖后腿任务的开销)
- 任务的执行进度由处理的文件的百分比来表现。特别是,在reduce任务中,拷贝任务,排序任务以及用户定义的reduce任务分别占执行进度的1/3
然后,我们思考一下上述前提条件存在的问题:
1. 不可能所有节点的工作速率都一样
2. 每个节点可能开始的时间不一样
3. 在reduce任务中,由于copy阶段涉及到网络传输,某些节点copy阶段所占的时间可能是reduce阶段的一半以上,这样就很容易出现某些节点的完成进度小于平均进度的20%,导致产生很多所谓的“拖后腿”任务。
如何改进:
关于运行进度的度量:之前,Hadoop假设所有的任务几乎都是同时开始的,所以计算任务的运行进度采用的绝对的方式,但是,有可能并不是所有的任务都是同时开始,所以计算时间并不能用绝对的时间。
那么可以采用相对的时间来计算运行进度:
ProgressScore/T,代表运行进度, T代表某个节点的总运行时间。
关于运行拷贝任务的节点选择:之前,我们在进行调度时,总是利用第一个可用的节点来重新执行失败的或者运行较慢的任务,但是,如果第一个可用的节点运行速度较慢,那么结果只是会出现一个新的straggler. 解决方案是:我们可以设定一个运行速率临界值,一旦低于这个临界值的节点被判定为运行速率较低,我们认为这种节点为SlowNode, 在分配时,尽量不考虑这种节点。(Master在运行过程中可以跟踪节点的运行速率)
关于拷贝份数:之前,我们在进行推测式任务时,只拷贝一份某个节点的任务,为了达到更短的运行时间,可以考虑增加拷贝的份数。
限定同时运行的推测任务的个数:如果同时检测到多个节点是straggler,并马上运行这些straggler的推测任务,可能会影响整体任务的运行性能。比如说,同时运行这些推测任务,涉及到网络传输部分,可能会导致网络阻塞。
判定为Straggler的临界值:我们定义一个临界值,只有当任务运行进度低于高于临界值时,才认为此节点可能是Straggler节点。主要是考虑到,如果一个节点开始的比较慢,所以它的运行的进度比较小,但是它并不是一个慢节点,我们不能立即认为它是慢节点。
新的算法:
当Hadoop集群中,出现一个空闲的节点,并且此时运行推测任务的低于临界值时,我们再考虑新的推测任务:
1. 如果节点的运行任务的进度低于临界值,忽略。
2. 根据任务运行的剩余的时间,将正在运行的不是推测式的任务进行排序。
3. 在空闲节点上,开始运行剩余时间最长的任务
4.2 坏记录
大型数据集十分庞杂。他们经常有损坏的记录;也经常会有不同格式的记录;还会有缺失的字段。在理想情况下,用户代码可以很好地处理这些情况。但是,时间情况中,忽略这些坏的记录只是权宜之计。取决于正在执行的分析,如果只有一小部分记录受影响,那么忽略他们不会显著影响结果。然后,如果一个任务由于遇到一个坏的记录而发生问题—通过抛出一个运行时异常—任务就会失败。失败的任务将重新运行,但是如果一个任务失败四次,那么整个作业会呗标记为失败。如果数据是导致任务抛出异常的“元凶”,那么重新运行任务也无济于事,因为它每次都会因相同的原因而失败。
处理坏记录的最佳位置位于mapper和reducer代码。我们可以检测出坏记录并忽略它,或通过抛出异常来终止作业运行。还可以使用计数器来计算作业中总的坏记录数,看问题影响的范围有多广。