MIT 6.824 Distributed System 学习笔记 - MapReduce

131 阅读9分钟

MIT 6.824 Distributed System - MapReduce

背景

MapReduce是由Google设计,开发和使用的一个系统,相关的论文在2004年发表。

Google当时面临的问题是,他们需要在TB级别的数据上进行大量的计算,比如对整个互联网数十TB的数据构建索引,构建索引基本上等同于对整个数据做排序,用一台计算机的话可能要几周,几个月,甚至几年。当时Google非常希望能将对大量数据的大量运算并行跑在几千台计算机上,这样才能快速完成计算。

Google需要一种框架,可以让它的工程师能够进行任意的数据分析,例如排序,网络索引器,链接分析器以及任何的运算。工程师只需要实现应用程序的核心,就能将应用程序运行在数千台计算机上,而不用考虑如何将运算工作分发到数千台计算机,如何组织这些计算机,如何移动数据,如何处理故障等等这些细节。所以,当时Google需要一种框架,使得普通工程师也可以很容易的完成并运行大规模的分布式运算。

思想

应用程序设计人员和分布式运算的使用者,只需要写简单的Map函数Reduce函数,而不需要知道任何有关分布式的事情,MapReduce框架会处理剩下的事情。

第一阶段 - 执行Map函数

MapReduce假设有一些输入,这些输入被分割成大量的不同的文件或者数据块。所以,我们假设现在有输入文件1,输入文件2和输入文件3,这些输入可能是从网上抓取的网页,更可能是包含了大量网页的文件。

MapReduce启动时,会查找Map函数。之后,MapReduce框架会为每个输入文件运行Map函数。这里很明显有一些可以并行运算的地方,比如说可以并行运行多个只关注输入和输出的Map函数。

Map函数的输入输出:输入 —— Map函数以文件作为输入,文件又是整个输入数据的一部分;输出 —— key-value对的列表,论文中称之为中间输出(intermediate output),也就是每个Map函数输出的key-value对。

假设我们在实现一个最简单的MapReduce Job:单词计数器。它会统计每个单词出现的次数。在这个例子中,Map函数会输出key-value对,其中key是单词,而value是1。Map函数会将输入中的每个单词拆分,并输出一个key-value对,key是该单词,value是1。最后需要对所有的key-value进行计数,以获得最终的输出。所以,假设输入文件1包含了单词a和单词b,Map函数的输出将会是key=a,value=1和key=b,value=1。第二个Map函数只从输入文件2看到了b,那么输出将会是key=b,value=1。第三个输入文件有一个a和一个c。

第二阶段 - 执行Reduce函数

MapReduce框架会为所有Map函数输出的每一个key,调用一次Reduce函数。

还是上面单词统计的例子,MapReduce框架会收集所有Map函数输出的每一个单词的统计。比如说,MapReduce框架会先收集每一个Map函数输出的key为a的key-value对,将它们提交给Reduce函数。之后会收集所有的b,并将它们提交给另一个Reduce函数。对c也是一样。

在我们这个简单的单词计数器的例子中,Reduce函数只需要统计传入参数的长度,甚至都不用查看传入参数的具体内容,因为每一个传入参数代表对单词加1,而我们只需要统计个数。最后,每个Reduce都输出与其关联的单词和这个单词的数量。所以第一个Reduce输出a=2,第二个Reduce输出b=2,第三个Reduce输出c=1。

术语补充

  • Job:整个MapReduce计算称为Job。
  • Task:每一次MapReduce调用称为Task。 一个完整的MapReduce Job,由一些Map Task和一些Reduce Task组成

Map函数

入参:key,value。key是输入文件的名字,(通常会被忽略,我们不太关心文件名是什么),value是输入文件的内容。

对于一个单词计数器来说,value包含了要统计的文本,我们会将这个文本拆分成单词。

之后对于每一个单词,我们都会调用emit。emit由MapReduce框架提供,并且这里的emit属于Map函数。emit会接收两个参数,其中一个是key,另一个是value。在单词计数器的例子中,emit入参的key是单词,value是字符串“1”。

Map(String key, Stringvalue):
    //key: 文件名, value: 文件内容
    split value into words
    for word in words:
        EmitIntermediate(word, "1")

Reduce函数

入参:key,value。key是某个特定key(Map输出中的key-value对中,出现了至少一次的所有key都可以作为Reduce函数的key参数),value是一个数组,里面每一个元素都是Map函数输出的key的一个实例的value。

对于单词计数器来说,key就是单词,value就是由字符串“1”组成的数组,所以,这个例子中我们不需要关心value的内容是什么,我们只需要关心value数组的长度。

Reduce函数也有一个属于自己的emit函数。这里的emit函数只会接受一个参数value,这个value会作为Reduce函数入参的key的最终输出。所以,对于单词计数器,我们会给emit传入数组的长度。

Reduce(String key, Iterator values):
    //key: a word, values: a list of "1"
    int result = 0;
    for v in values:
        result += 1
    Emit(result)

执行过程

  1. 用户程序的MapReduce Library:

    1.1 首先将input files分成M个部分,每部分16~64MB

    1.2 在一些机器上fork此程序,其中一个fork是master,其余是worker

    master给workers分配task(共有M个map task和R个reduce task),master会选取idle状态的worker,然后给它们分配map task或reduce task

  2. 若一个worker被分配了一个map task:

    2.1 它会读取对应的input split,从输入中解析出key/value pair(即文件名/文件内容),把每个key/value pair输入给map function,map function会输出intermediate keu/value pairs,缓存在这个worker的内存中

    2.2 被缓存的intermediate key/value pairs会周期性地被写入local disk,被partitioning function划分成R个regions

     每次Map函数调用emit,worker进程就会将数据写入到本地磁盘的文件中。
    

    2.3 worker把这R个文件的文件名会被传递给mastermaster会存储下这个map task对应的这R个文件名,之后会把这些位置传递给reduce workers

     如果master收到的completion message对应的是一个已经completed的map task,则直接忽略
    
  3. 若一个worker被分配了一个reduce task(处理R个regions中的一个):

    3.1 它会根据master传递给它的intermediate key/value pairs在map worker磁盘上的位置,使用remote procedure call从map worker的磁盘上的指定位置处读取intermediate key/value pairs

    通常来说,在运行Reduce函数之前。运行在MapReduce的worker服务器上的进程需要与集群中每一个其他服务器交互来询问说,看,我需要对key=a运行Reduce,请看一下你本地磁盘中存储的Map函数的中间输出,找出所有key=a,并通过网络将它们发给我。所以,Reduce worker需要从每一个worker获取特定key的实例。

    3.2 当reduce worker读去了所有intermediate key/value pairs,则先对这些intermediate keys排序,把所有相同key的values聚集起来。

    3.3 reduce worker遍历每一个intermediate key及其对应的values,调用reduce function,输出会被添加到这个reduce partition的final output file

     Reduce函数调用emit,将输出写入到一个Google使用的共享文件服务中
    
  4. 所有map tasks和reduce tasks都完成之后,会得到R个output file,master会唤醒用户程序

输入和输出文件的存放位置

它们都存放在文件中,但是因为我们想要灵活的在任意的worker上读取任意的数据,这意味着我们需要某种网络文件系统(network file system)来存放输入数据。

所以实际上,MapReduce论文谈到了GFS(Google File System)。GFS是一个共享文件服务,并且它也运行在MapReduce的worker集群的物理服务器上。GFS会自动拆分你存储的任何大文件,并且以64MB的块存储在多个服务器之上

所以,如果你有了10TB的网页数据,你只需要将它们写入到GFS,甚至你写入的时候是作为一个大文件写入的,GFS会自动将这个大文件拆分成64MB的块,并将这些块平均的分布在所有的GFS服务器之上,而这正是我们所需要的。如果我们接下来想要对刚刚那10TB的网页数据运行MapReduce Job,数据已经均匀的分割存储在所有的服务器上了。如果我们有1000台服务器,我们会启动1000个Map worker,每个Map worker会读取1/1000输入数据。这些Map worker可以并行的从1000个GFS文件服务器读取数据,并获取巨大的读取吞吐量,也就是1000台服务器能提供的吞吐量。

master的数据结构

存储:

  • 每个map task和reduce task的状态idle, in-progress, completed
  • 每个worker machine的身份
  • 对于每个completed的map task,存储这个map task产生的R个intermediate file regions

容错 Fault Tolerance

worker failure

master会周期性地ping每个worker,如果在一定时间内没收到worker的响应,master就会把这个worker标记为failed。

任何在failed worker上in-progress的map task或reduce task,以及任何在failed worker上completed的map task都会被置为idle状态,从而可以被重新调度执行。

failed worker上的completed reduce task不用被置为idle状态重新调度执行,因为它们的输出已经存储在GFS上了。

如果一个map task最先被workerA执行,但workerA fail了,转而让workerB重新执行,那么所有正在执行reduce task的worker都会被通知,没有从workerA读取中间数据的将会从workerB读取。

master failure

master周期性的对master中存储的数据结构进行写checkpoint。如果master task失败了,可以从最近的一次checkpoint新起一个副本。

但由于只有一个master,它出错的概率很低,也可以在master出错时直接退出程序,用户可以再重试。