Mengdong的技术博客

学习,记录,分享

导航

《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不是数据库

考虑两个关系(你可以理解为关系数据库中的表)ST,其中S中具有如下形式的数据条目(数据表中的行):

k为我们希望与T进行连接的键,sn为标识元组唯一性的ID,sn后的Sn则表示关系S中其他的属性(由于这些属性跟连接操作无关,因此简写为Sn)。

T中的数据为:

k为连接键,tn为元组ID,Tn表示T中其他属性。

为了更形象地说明,我们假设S是一个用户信息表,其中k是主键(用户ID),其他属性可能包括用户的年龄、性别、收入等。另一个表T中则记录了用户的访问记录信息,包括访问的URL,停留时间,产出收益等信息,T中的键k为外键S.k,将STk上做连接,可以得到每个用户的访问记录信息。

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) 一对一连接:这种情况考虑的是ST之中都至多一个元组拥有相同的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是无序的(看看上面的k23k59就会发现,ST不一定哪个在前面)。

2) 一对多连接:如果S中的连接键k是唯一的,而T中的k不唯一,此时的连接就是一(S)对多(T)连接。例如上面的例子,S为用户表,T为用户操作记录,ST的连接就是一个一对多连接。1) 中算法的mapper仍然可用,但到了reducer,因为value是无序的,因此不能保证S中的元组先出现,而只有S中的元组先出现时reducer才可以进行连接,具体做法如下面reducer伪代码所示(这段代码假设进入reducer的元组都是按照先ST的顺序排好序的):

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

  encounteredFALSE

  FOR EACH t IN tuple DO

    IF t is from relation S

      tSt

      encounteredTRUE

      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对都按照先ST的顺序排列,这样就可以在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,就将tTl中的所有元组连接,伪代码如下:

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

    ii+1

  WHILE in DO

    FOR EACH tT IN l  DO

      Emit join (tT, ti)

    ii+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端连接

如果需要连接的两个表都已经按照连接键排好序了(序应该是相同的,即同为升序/降序),那么可以通过归并连接法连接它们。这个过程可以通过以相同规则排序、划分两个数据表实现并行化。举个例子,将ST都按连接键使用相同的划分规则分为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=S1S2∪...∪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)算法。

posted on 2011-07-23 21:19  mdyang  阅读(1536)  评论(1编辑  收藏  举报