《Data-intensive Text Processing with MapReduce》读书笔记第2章:MapReduce基础(1)
本读书笔记的目录地址:http://www.cnblogs.com/mdyang/archive/2011/06/29/data-intensive-text-prcessing-with-mapreduce-contents.html
当前处理大规模数据最行之有效的思想就是“分而治之”。
分而治之:将一个大问题划分为相对独立的若干小问题,然后加以解决。由于小问题间相对独立,因此可以以并发/并行的方式分别处理。具体来说,可以是多线程,多进程,多核以至于多处理机(集群)。
如何分治?根据应用场景的不同,处理的方式千差万别。要考虑的问题包括但不限于:
· 如何划分问题?
· 如何将子任务分配给工作者(worker,可以是线程、进程、处理器核心、处理机等)?
· 如何同步?
· 如何在工作者间共享中间结果(例如:worker A需要使用worker B产生的中间结果)?
· 如何处理软/硬件错误?
在以前,程序员需要独自处理所有这些问题。例如对于共享内存的并发/并行程序,程序员需要使用像互斥量这样的同步原语来显式地指定共享数据结构的访问方式,同时还要注意避免死锁和竞争条件。像OpenMP(共享内存的并行/并发编程框架)和MPI(Message Passing Interface)这样的并行编程框架以程序库的形式封装了并行编程底层需要处理的同步与通信机制。即使这样,程序员依然需要考虑很多与问题处理无关的底层细节。
MapReduce最大的优势是它提供了系统层上的抽象,从而在很大程度上对程序员隐藏了底层细节。这样一来,程序员只需要考虑如何解决问题本身,而无需考虑诸多与问题无关的细节(这些细节:通信、同步等通常由并行编程引起)。
除此之外,MapReduce将数据集分布至各个节点,并在节点上直接执行处理程序,提高了效率。
本章主要介绍MapReduce编程模型与分布式文件系统。
2.1节介绍函数式编程FP(Functional Programming),MapReduce设计灵感来源于此;
2.2节介绍mapper和reducer,MapReduce的基本编程模型;
2.3节探讨执行框架(Execution Framework)在执行MapReduce程序(称为job,任务)中的作用;
2.4节partitioner与combiner(划分器与合并器);
2.5节分布式文件系统;
2.6节Hadoop集群架构;
2.7节总结。
2.1 函数式编程(Functional Programming, FP)
FP的基本概念
图2-1 map与fold内置高阶函数
MapReduce的设计灵感起源于FP。FP编程语言有Lisp、ML等。FP语言的一个重要特性是支持高阶函数(一种接受其它函数作为输入参数的函数)。最常见的两个内置高阶函数是map和fold(图2.1所示). map接受一个函数类型的参数f,并对输入列表l的每个元素应用f. fold接受两个输入参数:函数g与初始值i. fold将初始值与l中的第1个元素进行操作后,将结果存入一中间变量,然后再将中间变量与l中第2个元素操作,结果存入中间变量……如此循环,直至最后一个元素计算完毕,输出中间结果。map和fold的伪代码如下所示:
map(f) for each e in l e←f(e) |
fold(g, i) for each e in l i←g(e,i) |
例如:计算l中所有元素的平方和,可以定义f(x)=x2(记作λx.x2). g(x,y)=x+y(记作λxλy.x+y). 计算过程可以表示如下:
map(λx.x2), i←0, fold(λxλy.x+y, i)
map可以看做对数据集的变换(变换方法由f定义);fold可以看做对数据的合并(合并方法由g定义)。
可以直观地看出,map所进行的各步操作(将f应用至l中的各个元素)是相互独立的,可以非常方便地并行化。而fold函数则对数据的局部性有一定要求:执行g前,l中的元素必须首先聚合到一起。但实际上很多fold函数并不要求访问l中的所有元素(例如最初给出的那个fold每次只需要访问中间变量和l的一个元素),因此将fold进行并行化也是可能的。
FP与MapReduce的关系
以上描述刚好概括了MapReduce的工作方式:FP程序中map函数的执行对应MapReduce中的map阶段,fold对应reduce阶段。
MapReduce定义了一个两步处理大规模数据的方法:
1) 对数据集中的每个元素执行用户定义的map函数,产生中间结果;
2) 中间结果通过用户定义的reduce函数进行合并。
程序员所要做的就是定义map和reduce两个函数,类似于定义传入map的f和传入fold的g。
具体的执行过程则由MapReduce执行框架负责。
MapReduce看起来简单,在功能上似乎有很大局限性。但如果能够将复杂算法分解为多步map-reduce过程,用MapReduce设计复杂的并行算法也是可能的(后续章节将会介绍)。
MapReduce到底是什么?
准确地讲,MapReduce代表三个互相联系,但又相互区别的概念:
1) MapReduce编程模型;
2) MapReduce执行框架(亦可称为“运行时环境”,即runtime);
3) MapReduce实现(Google自家的实现与基于Java的Hadoop开源实现)。
除了Hadoop,还有很多其他MapReduce的实现(例如为CELL架构设计的GPGPUs)。Hadoop对MapReduce的实现与Google对MapReduce的设计之间有一些差别,后面将会讲解到。我们将以Hadoop为讲解基准,因为Hadoop是目前开源MapReduce实现中最成熟、拥有开发者最多的。
2.2 Mapper与Reducer
数据结构
MapReduce中的基本数据结构是键-值对(key-value pairs)。key与value可以是基本类型的数据,例如整数、浮点数、字符串或是字节数组,也可以是更复杂的数据类型(线性表、元祖、关联数组等)。复杂结构可以借助Protocol Buffers、Thrift、Avro之类的库自行定义。
MapReduce可用来处理由这样的key-value对构成的数据集。例如:
1) 网页数据集 key:页面URL,value:页面的HTML代码;
2) 图 key:节点ID,value:邻接节点列表。
一些情况下key完全用不着,而另一些情况下key被用来当做标识数据项的ID. 具体内容后面(第3章)会讲到。
定义mapper和reducer
MapReduce中,程序员需要定义mapper和reducer:
map: (k1,v1)→[(k2,v2)]
reduce: (k2,[v2])→[(k3,v3)] (本书用[...]表示列表)
MapReduce程序的输入通常是存储于底层分布式文件系统的key-value数据(见2.5节)。
MapReduce的执行过程
MapReduce的执行过程可以简述如下:
1) 输入数据被逐项传入mapper中执行,生成另一个key-value对的列表(中间结果);
1.5) 中间结果随即按照key进行分组,每一组内的pairs按照key排好序(这一步为隐含操作);
2) 每个组输入一个对应的reducer进行处理,输出结果写入文件系统;
(1) reducer内对一个组按照key顺序处理,reducer之间并行运行
(2) r个reducer将会生成r个输出文件
通常不需要合并这r个文件,因为它们往往是下一个MapReduce程序的输入。
图2.2给出了这个两步过程的演示。
图2.2 简化的MapReduce计算过程
一个简单的例子
一个统计文档中各单词出现次数的程序伪代码如图2.3所示。
1 2 3 4 |
class Mapper method Map(docid a, doc d) for all term t∈doc d do Emit(term t, count 1) |
1 2 3 4 5 6 |
class Reducer method Reduce(term t, counts [c1,c2,...]) sum←0 for all count c∈counts [c1,c2,...] do sum←sum+c Emit(term t, count sum) |
图2.3 使用MapReduce进行单词计数的程序伪代码
2.3中,输入key-value对是(docid,doc),存储于分布式文件系统之中。docid是能够唯一标识一个文档的ID,doc是文档正文。mapper接受一个(docid,doc),将文档docid正文中的单词逐个分离出来(tokenization),并为每个单词产生一个key-value对(word,1),其中key为单词本身,value为1(即一个计数)。(word,1)随后被分组、划分后送至reducer进行处理。reducer将所有具有相同word的(word,1)对计数相加,并将最终结果排序后输出至文件。
MapReduce执行环境能够保证所有具有相同key的key-value对(这里指的是经过mapper处理得到的中间结果,在2.3中即指具有相同word的(word,1)对)都能够被分配到同一个reducer中。
中间结果的分组、划分是由partitioner完成的,partitioner将在2.4节介绍。
Hadoop及其与Google MapReduce的差别
本书中的算法伪代码基于Hadoop程序,因此反映了Hadoop对MapReduce实现上的一些特性。
Hadoop中,mapper/reducer是实现了map/reduce方法的对象。
map阶段,针对每个map任务生成一个mapper对象,由执行框架对每个输入数据项应用mapper中定义的map方法。程序员可以指定同时进行多少个map任务,但执行框架具有最终决定权(取决于输入数据的物理分布),2.5节与2.6节将会详细介绍相关内容。
reduce阶段类似:对于每个reduce任务生成一个reducer对象,reducer对其接受的输入(中间结果)逐一指定reduce操作。与map不同的是,程序员可以完全控制同时进行的reduce任务数。这部分将会在2.6节对任务执行的介绍中做详细讲解。要读懂2.6节,须先读懂2.5节。
Hadoop实现和Google自家对MapReduce的实现之间是有差别的:
1) 在Hadoop中,reducer接受的是一个key-iterator对,其中iterator是一个列表的迭代器,这个列表中存储的是所有key等于当前key值的key-value对中的value值,并且这个列表是乱序的(比较绕,举个例子,(k1,v1),(k1,v2),(k1,v3),(k1,v1)被分配至reducer 1处理,则reducer 1实际接受到是(k1,iterator i of list [v1,v2,v3,v1]))。而Google的实现允许程序员执行二次排序操作(即按照value进行排序)(在划分之前已经对全体key-value按照key进行过排序了。这次排序,是划分完毕后,每个分组内再按照value进行排序,因此叫做二次排序),这样reducer接受的数据不仅是key有序的,对于每个key,其中的value列表也是有序的。如何在Hadoop中实现二次排序将在3.4节介绍;
2) Google实现中不允许改变reducer输入的key值,也就是说,reducer输出的key和输入的key是一样的(例如输入("a",1),("the",1),("the",1),只能输出("a",1),("the",2))。而Hadoop中没有这个限制,可以任意指定输出的key.
注意事项
mapper和reducer有什么限制?
对外部资源的使用要小心,因为会带来访问竞争、性能瓶颈等问题(例如SQL)
MapReduce的活用
1) 没有reducer的MapReduce程序(等价于有一个什么也不做的reducer:它直接将输入原样输出)。这种程序的输出结果几句是mapper的输出结果。应用场景举例:解析大文本集(很多文本构成的集合)、处理大图片集(很多图片构成的集合),其中每个项都可以独立处理,且处理结果无需合并;
2) 相反,没有mapper的MapReduce程序没有什么用处。
其他的数据存储
一般来说MapReduce从分布式文件系统中读取输入数据,并将结果写出至文件。但不仅限于此:
1) BigTable,Google实现的一个分布式稀疏表存储。HBase是它的一个开源实现(可与Hadoop集成);
2) 已有的大型并行关系数据库MPP RDB(massively parallel processing relational database);
3) 一些计算任务完全不需要输入(例如π的计算)。