MapReduce 模式、算法和用例
Ilya Katsov
在这篇文章中,我整合了一些MapReduce的模式和算法,以便于读者系统化地认识那些在互联网及科学文献中能够找到的不同技术。同时,我也提供了几个实用的案例学习。所有的描述及代码片段使用了标准Hadoop平台的MapReduce模型,包括:Mappers,Reduce,Combiners,Partitions和Sorting。下图描绘了这个框架。
图1 MapReduce框架
常规的MapReduce模式
计数和总结
问题陈述:从一批含有术语集合的文档中,计算出各个术语总共出现的次数。亦或是,计算出各个术语任意的函数结果。例如:通过一个日志文件的各个记录(每条记录包含一个响应时间)来计算平均响应时间。
解决方案:让我们先从一些简单的问题着手。下面的代码片段展示了这样一个含义:当每个Mapper处理一个术语时,它只发出一个“1”;而Reduceer则遍历整个术语列表并对其求和。
class Mapper
method Map(docid id, doc d)
for all term t in doc d do
Emit(term t, count 1)
class Reducer
method Reduce(term t, counts [c1, c2,...])
sum = 0
for all count c in [c1, c2,...] do
sum = sum + c
Emit(term t, count sum)
这种方法明显的缺陷是:Mapper发出了大量虚假计数。通过对每个文档的计数进行总结,Mapper能够减少较多的计数。
class Mapper
method Map(docid id, doc d)
H = new AssociativeArray
for all term t in doc d do
h{t} = h{t} + 1
for all term t in H do
Emit(term t, count H{t})
为了不仅能够对一个文档累加计数,而且能够对一个Mapper节点处理的所有文档进行累加计数,可以补充使用Combiners。
class Mapper
method Map(docid id, doc d)
for all term t in doc d do
Emit(term t, count 1)
class Combiner
method Combine(term t, [c1, c2,...])
sum = 0
for all count c in [c1, c2,...] do
sum = sum + c
Emit(term t, count sum)
class Reducer
method Reduce(term t, counts [c1, c2,...])
sum = 0
for all count c in [c1, c2,...] do
sum = sum + c
Emit(term t, count sum)
应用:日志分析,数据查询。
比较
问题陈述:根据一个术语集合及一个术语的相关函数,找出所有具有相同函数值的术语并将其保存到文件中,亦或是根据所有能够共同处理的术语执行其它的计算。最典型的例子就是倒排索引的建立。
解决方案:这个解决方案是简单易懂的。Mapper为每个术语计算指定的函数,并将函数值和术语本身作为结果发出。Reducer获取所有依据函数值分类的术语,并对其进行处理或是保存等。在倒排索引例子中,术语就是单词,而函数值是含有该术语的文芳的编号。
应用:倒排索引,ETL(数据抽取、转换、装载)
过滤,分析及验证
问题陈述:从一些记录中,收集所有满足指定条件的记录,或是将每条记录(区别于其它记录)转换另一种表现形式。类似的案例包括诸如文本分析、数值抽取及格式转换等。
解决方案:这个解决方案相当直接。Mapper一条条录入记录,并发出接收的记录或是该记录转换后的格式。
应用:日志分析,数据查询,ETL,数据验证
执行分布式任务
问题陈述:一个大量数据的计算任务可以被划分成许多子任务,通过将所有子任务的结果进行合并,从而获得最终的结果。
解决方案:在划分一类特定事物时(这些事物被存储为Mapper的输入数据),每个Mapper录入一个事物,执行相应的计算并发出结果。Reducer合并所有被发出的部分结果,并形成最终结果。
案例学习:模拟数字通讯系统
类似于WiMAX那样的数字通讯系统的软件模拟器,它能够通过系统模型传递一组随机数据,并计算传输的错误概率。每一个Mapper模拟指定的数据量(规定样本的1/N),发出错误率。Reducer计算平均错误率。
应用:物理和工程模拟,数值分析,性能试验
排序
问题陈述:对一组记录根据指定的规则进行排序,或是让这些记录形成特定的顺序。
解决方案:简单排序是显而易见的。Mapper只要发出所有的元素及排序关键字(由元素函数值组合形成)。然而,实际上排序都在被巧妙地使用,这也就是为什么说排序是MapReduce(或Hadoop)的核心思想之一。尤其是,非常普遍地使用综合的关键字来达到排序和分类的目的。最初,MapReduce中的排序是以根据关键字排序被发出的关键字值对为目的,但是存在着补充使用Hadoop实施细节来完成值排序的技术。如果MapReduce被用来排序原始数据(非中间数据),那将是无意义的,而持续使用BigTable概念维护数据的有序状态才是一个不错的选择。换句话说,在每次插入时排序数据将比在进行MapReduce查询时排序更加有效。
应用:ETL,数据分析
非常规的MapReduce模式
迭代消息传递(图形处理)
问题陈述:从一个实体关系网络结构中,根据其邻接实体的基本属性计算出每个实体的状态。这种状态可以是与其它实体间的距离,或是具备特定属性的邻接实体的表示,或是邻接实体密度的特征等等。
解决方案:上述网络结构被存储为一组节点,每个节点包含一张邻接表。概念上,MapReduce作业以迭代的方式执行,并在每次迭代中发送消息给它的相邻节点。每个邻接点根据接收的消息更新自己的状态。当满足一些特定条件,例如达到固定的最大迭代次数(比如说网络直径),或是两次连续的迭代没有使状态发生改变时,迭代即可终止。从技术角度来说,Mapper通过把邻接点编号作为关键字,来为每个节点发出消息。因此,所有的消息都依据引入的节点进行分类,同时减速器能够根据新状态重新计算状态和节点。下图描绘了这个算法。
class Mapper
method Map(id n, object N)
Emit(id n, object N)
for all id m in N.OutgoingRelations do
Emit(id m, message getMessage(N))
class Reducer
method Reduce(id m, [s1, s2,...])
M = null
messages = []
for all s in [s1, s2,...] do
if IsObject(s) then
M = s
else // s is a message
messages.add(s)
M.State = calculateState(messages)
Emit(id m, item M)
需要着重强调的是,一个节点的状态会迅速在整个网络传播。网络不要太稀疏,因为所有的节点会被它的邻接点影响。下图描绘了这个处理过程
案例学习:树形目录的可用性传播
问题陈述:这个问题的灵感来源于现实生活的电子商务。从大的目录(如男人,女人,儿童)中分裂出小的目录(如男士牛仔裤,女士长裙),并最终形成小的终端目录(如男士蓝色牛仔裤)。终端目录是可获得的,也可以是不可获得的。当子树中至少存在一个可获得的终端目录时,则上层目录也可以获得。
解决方案:这个问题可以通过使用前一节描述的框架来解决。接下来我们定义getMessage和calculateState方法。
class N
State in {True= 2, False = 1, null = 0}, initialized 1 or 2 for
end-of-line categories, 0 otherwise
method getMessage(object N)
return N.State
method calculateState(state s, data [d1, d2,...])
return max([d1, d2,...] )
案例学习:广度优先搜索
问题陈述:从一个图形中,计算出从源点到所有其他节点的距离。
解决方案:源点发出0给所有的邻接点,而这些邻接点传递这个计数时不断增加1。
class N
State is distance, initialized 0 for source node, INFINITY for all othernodes
method getMessage(N)
return N.State + 1
method calculateState(state s, data[d1, d2,...])
min( [d1, d2,...] )
案例学习:网页排名和单侧映射数据汇总
这个算法是由谷歌建议,作为一个函数来计算一个网页的相关性。原本的算法相当复杂,但是在它的核心,它仅仅是传播节点之间的权重,其中每个节点利用平均传入的权重计算其自身的权重。
class N
State isPageRank
method getMessage(object N)
return N.State/ N.OutgoingRelations.size()
method calculateState(state s, data [d1, d2,...])
return ( sum([d1, d2,...]) )
值得提及的是,我们使用的架构太宽泛,以至于我们没有利用状态是数值这一特点。在许多实际的案例中,由于这一事实,我们可以在映射器端执行聚集值。这一优化方法可以从以下代码片段了解(网页排名)。
class Mapper
method Initialize
H = new AssociativeArray
method Map(id n, object N)
s = N.PageRank / s.OutgoingRelations.size()
Emit(id n, object N)
for all id m in N.OutgoingRelations do
H{m} = H{m} + p
method Close
for all id n in H do
Emit(id n, value H{n})
class Reducer
method Reduce(id m, [s1, s2,...])
M = null
for all s in [s1, s2,...] do
if IsObject(s) then
M = s
else
s = s + p
M.PageRank = s
Emit(id m, item M)
应用:图像分析,网站索引
区别重复值(计数独特元素)
问题陈述:从包含字段F和G的记录集合中,为每个有相同G的记录子集(根据G分类),计算出其独特F的个数
这个问题可以稍微被归类概括为多面搜索。
问题陈述:现有一个记录集合,每个记录有字段F及任意个数的种类标签G={G1,G2,…}。为任一标签的子集计算出独特字段F的总数。例如:
Record 1: F=1, G={a, b}
Record 2: F=2, G={a, d, e}
Record 3: F=1, G={b}
Record 4: F=3, G={a, b}
Result:
a -> 3 // F=1, F=2, F=3
b -> 2 // F=1, F=3
d -> 1 // F=2
e -> 1 // F=2
解决方案I:第一种方法分两阶段解决问题。第一阶段:Mapper为每对F、G发出虚拟计数器;Reducer计算每对出现的总次数。这阶段的主要目标F值的唯一性。第二阶段:根据G来分类形成分组,并计算出每个分组中元素总个数。
Phase I:
class Mapper
method Map(null, record [value f, categories [g1, g2,...]])
for all category g in [g1, g2,...]
Emit(record [g, f], count 1)
class Reducer
method Reduce(record [g, f], counts [n1, n2, ...])
Emit(record [g, f], null )
Phase II:
class Mapper
method Map(record [f, g], null)
Emit(value g, count 1)
class Reducer
method Reduce(value g, counts [n1, n2,...])
Emit(value g, sum( [n1, n2,...] ) )
解决方案II:第二种方法只需要一个MapReduce作业,但是它不是可扩展的,并且适应性有限。这个算法十分简单。Mapper发出值和种类,Reducer从每个值的种类列表及每个种类的增量计数器列表中去除重复值。最后一步是对Reducer发出的所有计算器求和。如果具有相同F值的记录数不是非常多,并且种类的总数也是有限的,那么这个方法可以应用。例如:这个方法可用于处理网络日志和用户分类——用户总量很多,但单个用户的事件数及种类数都有限。在这个架构中,若在数据传递给Reducer之前,利用Combiners从种类列表中去除重复值将是无效的。
class Mapper
method Map(null, record [value f, categories [g1, g2,...] )
for all category g in [g1, g2,...]
Emit(value f, category g)
class Reducer
method Initialize
H = new AssociativeArray : category ->count
methodReduce(value f, categories [g1, g2,...])
[g1', g2',..] = ExcludeDuplicates( [g1,g2,..] )
for all category g in [g1', g2',...]
H{g} = H{g} + 1
method Close
for all category g in H do
Emit(category g, count H{g})
应用:日志分析,独特用户计数
互相关
问题陈述:现有一个元组集合,为每对可能的元素计算其共同出现的元组总数。如果元素的总数是N,则报告N*N。这个问题出现在文本分析(那就是说元素是词语,元组说是句子),市场分析(买这类商品的消费者倾向于也买那类商品)。如果N*N非常小并能够存储在单个机器的内存中,那么方案实施起来将是简单明了的。
成对方法:第一种方法是让Mapper发出所有元素对及虚拟计数器,然后利用Reducer对这些计数器求和。缺陷是:一Combiners的益处有限,而且很有可能所有的元素对都不同;二没有在内存中的积累。
class Mapper
method Map(null, items [i1, i2,...] )
for all item i in [i1, i2,...]
for all item j in [i1, i2,...]
Emit(pair [i j], count 1)
class Reducer
method Reduce(pair [i j], counts [c1, c2,...])
s = sum([c1, c2,...])
Emit(pair[i j], count s)
成条方法:第二种方法是根据元素对中的第一个元素来分类,并在对所有邻接元素累加时维护一个关联数组(“条”)。Reducer为向导元素i接收所有的关联数组,合并它们,并发出同样的结果作为对方法。
l 产生较少的中间键,一次减少排序次数
l 最大化利用Combiners
l 执行内存中的累加。如果应用不当将会导致问题
l 等复杂的实现
l 一般来说,成条方法比成对方法更快
class Mapper
method Map(null, items [i1, i2,...] )
for all item i in [i1, i2,...]
H = new AssociativeArray : item ->counter
for all item j in [i1, i2,...]
H{j} = H{j} + 1
Emit(item i, stripe H)
class Reducer
method Reduce(item i, stripes [H1, H2,...])
H = new AssociativeArray : item ->counter
H = merge-sum( [H1, H2,...] )
for all item j in H.keys()
Emit(pair [i j], H{j})
应用:文本分析,市场分析
关系MapReduce 模式
在这一节,我们将仔细研究关系运算并讨论在MapReduce中如何实现它们。
选择:
class Mapper
methodMap(rowkey key, tuple t)
if t satisfiesthe predicate
Emit(tuple t,null)
投影:投影比选择稍微复杂,但我们应该用Reducer来消除可能的重复值。
class Mapper
methodMap(rowkey key, tuple t)
tuple g =project(t) // extract required fields to tuple g
Emit(tuple g,null)
class Reducer
methodReduce(tuple t, array n) // n is an array of nulls
Emit(tuple t,null)
并:Mappers由两个待合并集合的所有记录来反馈。Reducer被用来去除重复值。
class Mapper
methodMap(rowkey key, tuple t)
Emit(tuple t,null)
class Reducer
methodReduce(tuple t, array n) // n is an array of one or two nulls
Emit(tuple t,null)
交:Mappers由两个待求交的集合的所有记录来反馈。Reducer只发出出现两次的记录,当且仅当这两个集合都包含该记录,因为记录包含的主键在一个集合中只出现一次。
class Mapper
methodMap(rowkey key, tuple t)
Emit(tuple t,null)
class Reducer
methodReduce(tuple t, array n) // n is an array of one or two nulls
if n.size() =2
Emit(tuple t,null)
差:现有两个记录集合—R和S,计算差集R-S。Mapper发出所有的元组和标签(记录所属集合的名字)。Reducer只发出R集合中记录。
class Mapper
methodMap(rowkey key, tuple t)
Emit(tuple t,string t.SetName) // t.SetName is either 'R' or 'S'
class Reducer
methodReduce(tuple t, array n) // array n can be ['R'], ['S'], ['R','S'], or ['S','R']
if n.size() =1 and n[1] = 'R'
Emit(tuple t,null)
分组和聚集:按以下方式,分组和聚集能够在一个MapReduce作业中执行。Mapper从每个元组中抽取值进行分类、聚集及发送。Reducer接收已分组的值进行聚集,并计算聚合函数。典型的聚合函数如求和、求最值,能够以流媒体的方式计算,因此不必同时处理所有的值。然而,在一些案例中,会用到双相的MapReduce作业,例如DistinctValues模式。
class Mapper
methodMap(null, tuple [value GroupBy, value AggregateBy, value ...])
Emit(valueGroupBy, value AggregateBy)
class Reducer
methodReduce(value GroupBy, [v1, v2,...])
Emit(valueGroupBy, aggregate( [v1, v2,...] ) ) // aggregate() :
sum(), max(),...
联接:在MapReduce框架中,联接能够非常完美的实现,但是存在着一些面向效率和数据量差异的技术。这一节我们学习一些基本的方法。参考文献中有指向联接技术学习的链接。
再分配联接(缩减联接,排序-合并联接)
这个算法根据键K对集合R和L进行联接。Mapper遍历R和L中的所有元组,从元组中取出值,给元组附上只是所属集合的标签,发出k作为键的标签元组。Reducer为一个特定的键K接收所有的元组,并把它们入两个容器中。当两个容器填满时,Reducer运行嵌套循环,并发出这两个容器中元素的交叉联接。每个被发出的元组由R元组,L元组及键K组成。这种方法有如下的缺陷:Mapper发出所有的数据,即使是只在一个集合出现的键。在内存中,Reducer应该为每个键保存所有的数据。如果数据没有存储,Reducer需要通过交换来解决。然而,再分配联接是最通用的技术,尤其是其它优化技术都不适用的时候。
class Mapper
method Map(null, tuple [join_key k, value v1, value v2,...])
Emit(join_key k, tagged_tuple [set_nametag, values [v1, v2, ...] ] )
class Reducer
method Reduce(join_key k, tagged_tuples [t1, t2,...])
H = new AssociativeArray : set_name ->values
for all tagged_tuple t in [t1,t2,...] // separate values into 2 arrays
H{t.tag}.add(t.values)
for all values r in H{'R'} // produce a cross-join of thetwo arrays
for all values l in H{'L'}
Emit(null, [k r l] )
复制联接(映射联接,哈希联接)
实际上,将小集合和大集合联接是很典型的(那就是说带有日志记录列表的用户列表)。假设我们联接两个集合—R和L,R集合很小。如果是这样的话,集合R能够分发给所有的Mappers,并且每个Mapper能够加载并通过联接键为它们建立索引。最普遍和有效的缩印技术是哈希表。之后,Mapper遍历集合L的所有元组,并和存储在哈希表中集合R的元组联接。这种方法非常有效,因为不需要通过网络对集合L进行排序或是传输,但集合R必须足够小并能够分发给所有的Mapper。
class Mapper
methodInitialize
H = new AssociativeArray: join_key -> tuple from R
R = loadR()
for all [join_key k, tuple [r1, r2,...] ] in R
H{k} =H{k}.append( [r1, r2,...] )
methodMap(join_key k, tuple l)
for all tupler in H{k}
Emit(null,tuple [k r l] )
参考文献:
1. Join Algorithms using Map/Reduce
2. Optimizing Joins in a MapReduce Environment
机器学习和数学MapReduce算法
l C. T. Chu et al provides anexcellent description of machine learning algorithms for
MapReduce in the article Map-Reduce forMachine Learning on Multicore.
l FFT using MapReduce: http://www.slideshare.net/hortonworks/large-scale-math-with-hadoop-mapred
uce
l MapReduce for integer factorization: http://www.javiertordable.com/files/MapreduceForIntegerFactorization.pdf
l Matrixmultiplication with
MapReduce: http://csl.skku.edu/papers/CS-TR-2010-330.pdfhttp://www.norstad.org/matrix-multiply/index.html