Files
MIT6.824/lecture-01-introduction/1.8-mapreduce-han-shu.md
2022-01-25 02:41:31 +00:00

14 KiB
Raw Blame History

1.8 Map函数和Reduce函数

Map函数使用一个key和一个value作为参数。我们这里说的函数是由普通编程语言编写例如C++Java等所以这里的函数任何人都可以写出来。入参中key是输入文件的名字通常会被忽略因为我们不太关心文件名是什么value是输入文件的内容。所以对于一个单词计数器来说value包含了要统计的文本我们会将这个文本拆分成单词。之后对于每一个单词我们都会调用emit。emit由MapReduce框架提供并且这里的emit属于Map函数。emit会接收两个参数其中一个是key另一个是value。在单词计数器的例子中emit入参的key是单词value是字符串“1”。这就是一个Map函数。在一个单词计数器的MapReduce Job中Map函数实际就可以这么简单。而这个Map函数不需要知道任何分布式相关的信息不需要知道有多台计算机不需要知道实际会通过网络来移动数据。这里非常直观。

Reduce函数的入参是某个特定key的所有实例Map输出中的key-value对中出现了一次特定的key就可以算作一个实例。所以Reduce函数也是使用一个key和一个value作为参数其中value是一个数组里面每一个元素是Map函数输出的key的一个实例的value。对于单词计数器来说key就是单词value就是由字符串“1”组成的数组所以我们不需要关心value的内容是什么我们只需要关心value数组的长度。Reduce函数也有一个属于自己的emit函数。这里的emit函数只会接受一个参数value这个value会作为Reduce函数入参的key的最终输出。所以对于单词计数器我们会给emit传入数组的长度。这就是一个最简单的Reduce函数。并且Reduce也不需要知道任何有关容错或者其他有关分布式相关的信息。

对于MapReduce的基本框架有什么问题吗

学生提问可以将Reduce函数的输出再传递给Map函数吗

Robert教授在现实中这是很常见的。MapReduce用户定义了一个MapReduce Job接收一些输入生成一些输出。之后可能会有第二个MapReduce Job来消费前一个Job的输出。对于一些非常复杂的多阶段分析或者迭代算法比如说Google用来评价网页的重要性和影响力的PageRank算法这些算法是逐渐向答案收敛的。我认为Google最初就是这么使用MapReduce的他们运行MapReduce Job多次每一次的输出都是一个网页的列表其中包含了网页的价值权重或者重要性。所以将MapReduce的输出作为另一个MapReduce Job的输入这很正常。

学生提问如果可以将Reduce的输出作为Map的输入在生成Reduce函数的输出时需要有什么注意吗

Robert教授是的你需要设置一些内容。比如你需要这么写Reduce函数使其在某种程度上知道应该按照下一个MapReduce Job需要的格式生成数据。这里实际上带出了一些MapReduce框架的缺点。如果你的算法可以很简单的由Map函数、Map函数的中间输出以及Reduce函数来表达那是极好的。MapReduce对于能够套用这种形式的算法是极好的。并且Map函数必须是完全独立的它们是一些只关心入参的函数。这里就有一些限制了。事实上很多人想要的更长的运算流程这涉及到不同的处理。使用MapReduce的话你不得不将多个MapReduce Job拼装在一起。而在本课程后面会介绍的一些更高级的系统中会让你指定完整的计算流程然后这些系统会做优化。这些系统会发现所有你想完成的工作然后有效的组织更复杂的计算。

学生提问MapReduce框架更重要还是Map/Reduce函数更重要

Robert教授从程序员的角度来看只需要关心Map函数和Reduce函数。从我们的角度来看我们需要关心的是worker进程和worker服务器。这些是MapReduce框架的一部分它们与其它很多组件一起调用了Map函数和Reduce函数。所以是的从我们的角度来看我们更关心框架是如何组成的。从程序员的角度来看所有的分布式的内容都被剥离了。

学生提问当你调用emit时数据会发生什么变化emit函数在哪运行

Robert教授首先看这些函数在哪运行。这里可以看MapReduce论文的图1。现实中MapReduce运行在大量的服务器之上我们称之为worker服务器或者worker。同时也会有一个Master节点来组织整个计算过程。这里实际发生的是Master服务器知道有多少输入文件例如5000个输入文件之后它将Map函数分发到不同的worker。所以它会向worker服务器发送一条消息说请对这个输入文件执行Map函数吧。之后MapReduce框架中的worker进程会读取文件的内容调用Map函数并将文件名和文件内容作为参数传给Map函数。worker进程还需要实现emit这样每次Map函数调用emitworker进程就会将数据写入到本地磁盘的文件中。所以Map函数中调用emit的效果是在worker的本地磁盘上创建文件这些文件包含了当前worker的Map函数生成的所有的key和value。

所以Map阶段结束时我们看到的就是Map函数在worker上生成的一些文件。之后MapReduce的worker会将这些数据移动到Reduce所需要的位置。对于一个典型的大型运算Reduce的入参包含了所有Map函数对于特定key的输出。通常来说每个Map函数都可能生成大量key。所以通常来说在运行Reduce函数之前。运行在MapReduce的worker服务器上的进程需要与集群中每一个其他服务器交互来询问说我需要对key=a运行Reduce请看一下你本地磁盘中存储的Map函数的中间输出找出所有key=a并通过网络将它们发给我。所以Reduce worker需要从每一个worker获取特定key的实例。这是通过由Master通知到Reduce worker的一条指令来触发。一旦worker收集完所有的数据它会调用Reduce函数Reduce函数运算完了会调用自己的emit这个emit与Map函数中的emit不一样它会将输出写入到一个Google使用的共享文件服务中。

有关输入和输出文件的存放位置这是我之前没有提到的它们都存放在文件中但是因为我们想要灵活的在任意的worker上读取任意的数据这意味着我们需要某种网络文件系统network file system来存放输入数据。所以实际上MapReduce论文谈到了GFSGoogle 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台服务器能提供的吞吐量。

学生提问:这里的箭头代表什么意思?

Robert教授随着Google这些年对MapReduce系统的改进答案也略有不同。通常情况下如果我们在一个例如GFS的文件系统中存储大的文件你的数据分散在大量服务器之上你需要通过网络与这些服务器通信以获取你的数据。在这种情况下这个箭头表示MapReduce的worker需要通过网络与存储了输入文件的GFS服务器通信并通过网络将数据读取到MapReduce的worker节点进而将数据传递给Map函数。这是最常见的情况。并且这是MapReduce论文中介绍的工作方式。但是如果你这么做了这里就有很多网络通信。 如果数据总共是10TB那么相应的就需要在数据中心网络上移动10TB的数据。而数据中心网络通常是GB级别的带宽所以移动10TB的数据需要大量的时间。在论文发表的2004年MapReduce系统最大的限制瓶颈是网络吞吐。如果你读到了论文的评估部分你会发现当时运行在一个有数千台机器的网络上每台计算机都接入到一个机架机架上有以太网交换机机架之间通过root交换机连接最上面那个交换机

如果随机的选择MapReduce的worker服务器和GFS服务器那么至少有一半的机会它们之间的通信需要经过root交换机而这个root交换机的吞吐量总是固定的。如果做一个除法root交换机的总吞吐除以2000那么每台机器只能分到50Mb/S的网络容量。这个网络容量相比磁盘或者CPU的速度来说要小得多。所以50Mb/S是一个巨大的限制。

在MapReduce论文中讨论了大量的避免使用网络的技巧。其中一个是将GFS和MapReduce混合运行在一组服务器上。所以如果有1000台服务器那么GFS和MapReduce都运行在那1000台服务器之上。当MapReduce的Master节点拆分Map任务并分包到不同的worker服务器上时Master节点会找出输入文件具体存在哪台GFS服务器上并把对应于那个输入文件的Map Task调度到同一台服务器上。所以默认情况下这里的箭头是指读取本地文件而不会涉及网络。虽然由于故障负载或者其他原因不能总是让Map函数都读取本地文件但是几乎所有的Map函数都会运行在存储了数据的相同机器上并因此节省了大量的时间否则通过网络来读取输入数据将会耗费大量的时间。

我之前提过Map函数会将输出存储到机器的本地磁盘所以存储Map函数的输出不需要网络通信至少不需要实时的网络通信。但是我们可以确定的是为了收集所有特定key的输出并将它们传递给某个机器的Reduce函数还是需要网络通信。假设现在我们想要读取所有的相关数据并通过网络将这些数据传递给单台机器数据最开始在运行Map Task的机器上按照行存储例如第一行代表第一个Map函数输出a=1b=1

而我们最终需要这些数据在运行Reduce函数的机器上按照列存储例如Reduce函数需要的是第一个Map函数的a=1和第三个Map函数的a=1

论文里称这种数据转换之为洗牌shuffle。所以这里确实需要将每一份数据都通过网络从创建它的Map节点传输到需要它的Reduce节点。所以这也是MapReduce中代价较大的一部分。

学生提问是否可以通过Streaming的方式加速Reduce的读取

Robert教授你是对的。你可以设想一个不同的定义其中Reduce通过streaming方式读取数据。我没有仔细想过这个方法我也不知道这是否可行。作为一个程序接口MapReduce的第一目标就是让人们能够简单的编程人们不需要知道MapReduce里面发生了什么。对于一个streaming方式的Reduce函数或许就没有之前的定义那么简单了。

不过或许可以这么做。实际上很多现代的系统中会按照streaming的方式处理数据而不是像MapReduce那样通过批量的方式处理Reduce函数。在MapReduce中需要一直要等到所有的数据都获取到了才会进行Reduce处理所以这是一种批量处理。现代系统通常会使用streaming并且效率会高一些。

所以这里的shuffle的重点是这里实际上可能会有大量的网络通信。假设你在进行排序排序的输入输出会有相同的大小。这样如果你的输入是10TB为了能排序你需要将10TB的数据在网络上移动并且输出也会是10TB所以这里有大量的数据。这可能发生在任何MapReduce job中尽管有一些MapReduce job在不同阶段的数据没有那么大。

之前有人提过想将Reduce的输出传给另一个MapReduce job而这也是人们常做的事情。在一些场景中Reduce的输出可能会非常巨大比如排序比如网页索引器。10TB的输入对应的是10TB的输出。所以Reduce的输出也会存储在GFS上。但是Reduce只会生成key-value对MapReduce框架会收集这些数据并将它们写入到GFS的大文件中。所以这里有需要一大轮的网络通信将每个Reduce的输出传输到相应的GFS服务器上。你或许会认为这里会使用相同的技巧就将Reduce的输出存储在运行了Reduce Task的同一个GFS服务器上因为是混部的。或许Google这么做了但是因为GFS会将数据做拆分并且为了提高性能并保留容错性数据会有2-3份副本。这意味着不论你写什么你总是需要通过网络将一份数据拷贝写到2-3台服务器上。所以这里会有大量的网络通信。这里的网络通信是2004年限制MapReduce的瓶颈。在2020年因为之前的网络架构成为了人们想在数据中心中做的很多事情的限制因素现代数据中心中root交换机比过去快了很多。并且你或许已经见过一个典型的现代数据中心网络会有很多的root交换机而不是一个交换机spine-leaf架构。每个机架交换机都与每个root交换机相连网络流量在多个root交换机之间做负载分担。所以现代数据中心网络的吞吐大多了。

我认为Google几年前就不再使用MapReduce了不过在那之前现代的MapReduce已经不再尝试在GFS数据存储的服务器上运行Map函数了它乐意从任何地方加载数据因为网络已经足够快了。

好的我们没有时间聊MapReduce了下周有一个lab你会在lab中实现一个你自己的简单版本的MapReduce。