《Data-intensive Text Processing with MapReduce》读书笔记第3章:MapReduce算法设计(5)
本读书笔记的目录地址:http://www.cnblogs.com/mdyang/archive/2011/06/29/data-intensive-text-prcessing-with-mapreduce-contents.html
因为最近工作比较忙,没有时间继续写这本书的读书笔记,所以本系列将会暂停一段时间。
3.5 关系连接
相关wiki:
Join: http://en.wikipedia.org/wiki/Join_(SQL)
Nested Loop Join: http://en.wikipedia.org/wiki/Nested_loop_join
(译者:整个3.5都更像是数据库教程,而不是MapReduce算法设计导引)
Hadoop的另一个重要应用就是数据仓库。企业的数据仓库存储了产品销售记录、库存等各种各样的信息。通过对这些数据的分析,可以为企业的商务智能(business
intelligence)与决策制定(decision making)提供重要参考。
传统的数据仓库通常使用关系数据库(relational
database)实现。这些数据库通常为在线分析型处理(online analytical process, OLAP,与OLTP对应。详见wiki:
OLTP http://en.wikipedia.org/wiki/OLTP
OLAP http://en.wikipedia.org/wiki/OLAP)进行了优化。许多数据库厂商都提供了并行数据库产品,但由于昂贵的成本(每TB上万美元),很多这些数据库的用户无法将他们扩展到所需的规模。例如Hamerbacher在谈到Facebook在这方面的处理时提到,由于太贵,Facebook弃用了Oracle的产品,转而投奔Hadoop.
他们基于Hadoop开发了一个名为Hive的框架(现在是Apache旗下的一个开源项目)。Pig(现在也是Apache旗下的一个开源项目)由Yahoo开发,也是一个基于Hadoop的数据分析工具,可以用来分析大规模半结构化数据。
基于以上成功的Hadoop数据处理案例,我们认为有必要对使用MapReduce进行关系数据处理的算法展开讨论。本节关注使用MapReduce进行关系数据上的连接(join)操作。但与此同时需要强调的一点是:Hadoop不是数据库。
考虑两个关系(你可以理解为关系数据库中的表)S和T,其中S中具有如下形式的数据条目(数据表中的行):
k为我们希望与T进行连接的键,sn为标识元组唯一性的ID,sn后的Sn则表示关系S中其他的属性(由于这些属性跟连接操作无关,因此简写为Sn)。
而T中的数据为:
k为连接键,tn为元组ID,Tn表示T中其他属性。
为了更形象地说明,我们假设S是一个用户信息表,其中k是主键(用户ID),其他属性可能包括用户的年龄、性别、收入等。另一个表T中则记录了用户的访问记录信息,包括访问的URL,停留时间,产出收益等信息,T中的键k为外键S.k,将S和T在k上做连接,可以得到每个用户的访问记录信息。
3.5.1 reduce端连接
参考wiki: Sort-merge Join http://en.wikipedia.org/wiki/Merge_join
基本思路
reduce端连接的思路很简单:mapper扫描两个表中的所有行,将每个元组[k,s,S](或[k,t,T])转换成形如(k,[s,S])(或(k,[t,T]))这样的中间结果key-value对后直接输出。由于在进入reduce前,所有这样的中间结果会按照key值进行排序/分组,因此所有具有相同k值的元组都会进入同一个reducer,这保证每个reducer仅处理自己得到的数据就可以得到正确结果(不会丢失结果)。而且输入reducer的数据是按照key有序的,因此可以在reducer内进行类似归并连接的连接操作(Sort-merge
Join)。
这就是reduce端连接的大致思路,接下来还有一些细节需要考虑。
1) 一对一连接:这种情况考虑的是S和T之中都至多有一个元组拥有相同的key.
如下图所示(仅画出了属性k):
此时reducer拿到的数据如下所示:
(k23, [(s64,S64),(t84,T84)])
(k37, [(s68,S68)])
(k59, [(t97,T97),(s81,S81)])
(k61, [(t99,T99)])
…
在这种情况下,如果一个key对应列表有两个元素,那么这两个元素一定有一个来自于S,另一个来自于T.
此时就需要对这两个元组进行连接。注意Hadoop中输入reducer的value是无序的(看看上面的k23和k59就会发现,S和T不一定哪个在前面)。
2) 一对多连接:如果S中的连接键k是唯一的,而T中的k不唯一,此时的连接就是一(S)对多(T)连接。例如上面的例子,S为用户表,T为用户操作记录,S与T的连接就是一个一对多连接。1)
中算法的mapper仍然可用,但到了reducer,因为value是无序的,因此不能保证S中的元组先出现,而只有S中的元组先出现时reducer才可以进行连接,具体做法如下面reducer伪代码所示(这段代码假设进入reducer的元组都是按照先S后T的顺序排好序的):
Reducer (JoinKey k, TupleList tuple[t1,t2,tn]) IF t1
is from relation S FOR i
= 2 TO n DO Emit join(t1,
ti) |
算法3.5.1.1
解决这个问题的一个简单的办法是在reducer遍历到来自S中的元组前先将已遍历过的元组存入内存,待至来自S中出现后再进行连接。伪代码如下:
Reducer (JoinKey k, TupleList tuple[t1,t2,tn]) Initialize l
as an empty list encountered←FALSE FOR
EACH t IN tuple DO IF t
is from relation S tS←t encountered←TRUE FOR EACH tT
IN l DO Emit join(tS,tT) CONTINUE ELSE IF encountered = TRUE Emit
join(tS,t) ELSE add t
to l |
算法3.5.1.2
但这个做法有内存容量上的瓶颈。
另一个做法是使用二次排序,通过应用值键转换,我们可以让mapper构造类似下面这样格式的中间结果:
((k82,s105),[(S105)])
((k82,t98),[(T98)])
((k82,t101),[(T101)])
((k82,t137),[(T137)])
这样一来,就可以通过自定义排序保证同一个k值下的所有key-value对都按照先S后T的顺序排列,这样就可以在reducer中使用最简单的算法3.5.1.1了。
3) 多对多连接:用2)
中的值键转换、二次排序法处理这样的数据。如果S的规模比T小,我们倾向于让S排在T前面,因此将会得到如下中间结果:
((k82,s105),[(S105)])
((k82,s124),[(S124)])
…
((k82,t98),[(T98)])
((k82,t101),[(T101)])
((k82,t137),[(T137)])
…
对于这种连接,可以先将来自S的元组存入一个列表l,然后遇到一个来自T中的元组tT,就将tT与l中的所有元组连接,伪代码如下:
Reducer (JoinKey k, TupleList tuple[t1,t2,tn]) Initialize l
as an empty list i←1 WHILE ti
is from S DO add
ti to l i←i+1 WHILE i
≤ n DO FOR EACH
tT IN l
DO Emit
join (tT, ti) i←i+1 |
算法3.5.1.3
因为需要将前半个输入序列的内容存在内存中,算法3.5.1.3也有内存瓶颈。因此为了尽量减小内存开销,在实际处理时通常需要根据具体的数据特点设计排序规则,使得来自规模较小的表的元组排在前面。
reduce端连接总结
可以看出,1) 2) 3)
是一个一般化的过程,即2)是3)的特殊情况,1)又是2)的特殊情况。我们从一对一连接开始,通过对连接情况的不断扩展,探讨了reduce端连接的实现方法。reduce端连接通过对两个数据表按照连接键进行再划分实现。因为连接在reduce操作中进行,因此需要将数据集通过网络传输(mapper扫描数据集,产生中间结果,中间结果排序、划分后通过网络送入reducer),这种连接操作的效率并不高。
3.5.2 map端连接
如果需要连接的两个表都已经按照连接键排好序了(序应该是相同的,即同为升序/降序),那么可以通过归并连接法连接它们。这个过程可以通过以相同规则排序、划分两个数据表实现并行化。举个例子,将S与T都按连接键使用相同的划分规则分为10份(在MapReduce中可能就是10个输入文件),且每个文件中的元组也是按照连接键有序的。有了这样的输入数据,我们就可以使用mapper实现并行连接操作(在mapper内读入数据文件并连接其中元组,过程参照算法3.5.1.3),这样的算法无需reducer.
由于算法完全在mapper内进行,无需将大量数据通过网络传输,因此map端连接效率高于reduce端连接。
map端连接与reduce端连接的比较
但map端连接对输入数据的要求较高,在现实中有应用的可能吗?答案是肯定的。实际的数据分析通常包含多步操作,下一步操作的输入往往依赖上一步操作的输出。而分析流程往往是可预知的(通常分析流程都是预先制定的,具有相对静态性),因此可以通过仔细的设计使得上一步的输出满足下一步输入的要求,来实现类似map端连接这样的操作。
而reduce端连接虽然效率较低,但更具有一般性。因为它本质上只需要将输入数据全部读入,然后按照连接键划分即可。所以reduce端连接对输入数据没有特殊要求,对各种不同的输入具有更好的适应性。
map端连接在Hadoop上的问题
还有一个关于map端连接的问题是与Hadoop相关的。Hadoop中输入的key与输出的key可以不一致。但随意修改key格式将会破坏处理格式的一致性。而map端连接依赖于上一步分析操作(很可能也是一个连接操作)产生的输出数据,因此在使用Hadoop设计map端连接算法时,应当特别注意key格式的一致性。
3.5.3 基于内存的连接
参考wiki: Hash Join http://en.wikipedia.org/wiki/Hash_join
(这一小节很水,而且跟MapReduce没什么关系)
进行连接的另一个方法基于hash表。对于两个要进行连接的表,先对其中较小的表在连接键上建立hash表,然后遍历另一个表,每遍历一个元组,使用该元组的连接键查找hash表,如果找到匹配元素,则进行连接。Hash
Join的伪代码如下:
// join S and T on key k, and S is the smaller table HashJoin (table S, table T) initialize an
empty hash table H{key=>list} FOR EACH
tS IN S DO add tS
to H{tS.k}
// H{tS.k} is a list FOR EACH
tT IN T DO IF H{tT.k}
is not empty FOR EACH
tS IN H{tT.k} DO Emit
join(tS,tT) |
算法3.5.3.1
由于需要在内存中建立hash表H,因此算法具有内存瓶颈(如果较小的表S也很大,那么这个hash表可能在内存放不下)。当放不下的时候,最简单的解决办法就是将S划分为n份:S=S1∪S2∪...∪Sn.
通过调整n的大小,几乎总能使得一次处理需要建立的hash表小到足以放入内存。划分S后Hash Join从HashJoin(S,T)分解为HashJoin(S1,T),HashJoin(S2,T)…
HashJoin(Sn,T). 显然,这个方案需要对T遍历n次。
还有一个解决方法:使用分布式的key-value存储(例如memcached,通过把key-value分布至多机内存构建一个逻辑上统一的、容量巨大的key-value存储。介绍可见http://en.wikipedia.org/wiki/Memcached)代替基于本机内存的hash表。这样一来可供使用的“内存”空间大了很多,也就可以进行大表之间的连接操作了。
3.6 本章总结
本章对MapReduce算法设计的基础进行了介绍。介绍了几种MapReduce算法设计中几种常见的设计模式(design
pattern):
1.
mapper内合并(in-mapper combining)模式。这种模式中combiner的工作被移至mapper内完成。应用mapper内合并后,mapper不再为每个输入key-value对产生一个输出key-value对,而是将其进行局部合并,最后统一输出。
2.
对(pair)模式与带(stripe)模式。这两种模式可用于从观察数据中追踪协同事件。在pair算法中,每对一起发生的事件被分别记录;而在stripe算法中,与某个事件同时发生的所有事件被记录在一起。虽然stripe算法具有很高的效率,但它受到内存容量的制约,存在可扩展性问题。
3.
反序(order-inversion)模式。该模式的核心思想是将算法操作序问题转化为数组排序问题。经过仔细组织,我们可以计算统计结果,并马上将统计结果应用于接下来的计算。与此同时,几乎不花费任何额外空间用来记录统计结果。
4. 键值转换(value-to-key conversion)模式。该模式提供了在Hadoop上进行二次排序的方法,由于利用了Hadoop内置的排序功能,该模式具有很好的可扩展性。
最后再对本章涉及到的MapReduce编程技巧进行一下总结:
1.
使用自定义的数据结构作为key/value:有效组织所需数据,在以上模式中均有应用。
2.
自定义的mapper/reducer的预处理/后处理操作。例如:mapper内合并就是通过自定义的mapper后处理操作实现的。
3.
在mapper/reducer对象内维持状态记录。在mapper内合并、反序、值键转换等模式中有应用。
4.
自定义排序规则。反序与键值转换模式中有应用。
5. 自定义partitioner. 反序与键值转换模式中有应用。
至此,MapReduce算法设计基础概览完毕。可以看出,虽然MapReduce要求将算法表示为定义严格的map-reduce操作序列,操作序列之间的输入/输出也有着严格的规约,但仍然有很多设计技巧可以利用,使得我们能够用MapReduce表达复杂算法。
从下一章开始,我们开始关注特定领域的算法:第4章关注倒排索引(inverted
indexing),第5章关注图(graph)算法,第6章关注期望最大化(expectation-maximization)算法。