原文发表于 2006 年 11 月 15 日
微软著名的 C++大师 Herb Sutter 在 2005 年初的时候曾经写过一篇重量级的文章——The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software,预言OO之后软件开发将要面临的又一次重大变革——并行计算。
摩尔定律统治下的软件开发时代有一个非常有意思的现象:“Andy giveth, and Bill taketh away.”。不管CPU的主频有多快,我们始终有办法来利用它,而我们也陶醉在机器升级带来的程序性能提高中。
我记着我大二的时候曾经做过一个五子棋的程序,当时的算法就是预先设计一些棋型(有优先级),然后扫描棋盘,对形势进行分析,看看当前走哪部对自己最重要。当然下棋还要堵别人,这就需要互换双方的棋型再计算。如果只算一步,很可能被狡猾的对手欺骗,所以为了多想几步,还需要递归和回朔。在当时的机器上,算 3 步就基本上需要 3 秒左右的时间了。后来大学毕业收拾东西的时候找到这个程序,试了一下,发现算 10 步需要的时间也基本上感觉不出来了。
不知道你是否有同样的经历,我们不知不觉的一直在享受着这样的免费午餐。可是,随着摩尔定律的提前终结,免费的午餐终究要还回去。虽然硬件设计师还在努力:Hyper Threading CPU(多出一套寄存器,相当于一个逻辑CPU)使得 Pipeline(管道) 尽可能满负荷,使多个 Thread 的操作有可能并行,使得多线程程序的性能有 5%-15% 的提升;增加 Cache(缓存) 容量也使得包括 单线程 和 Multi-Thread(多线程) 程序都能受益。也许这些还能帮助你一段时间,但问题是,我们必须做出改变,面对这个即将到来的变革,你准备好了么?
Concurrency Programming != Multi-Thread Programming(并发编程!=多线程程序)。很多人都会说 MultiThreading 谁不会,问题是,你是为什么使用/如何使用多线程的?我从前做过一个类似 AcdSee 一样的图像查看/处理程序,我通常用它来处理我的数码照片。我在里面用了大量的多线程,不过主要目的是在图像处理的时候不要 Block 住 UI,所以将 CPU Intensive(密集) 的计算部分用后台线程进行处理,而并没有把对图像矩阵的运算并行分开。
我觉得 并发编程 真正的挑战在于编程模式的改变,在程序员的脑子里面要对自己的程序怎样并行化有很清楚的认识,更重要的是,如何去实现(包括架构、容错、实时监控等等)这种并行化,如何去调试,如何去测试。
在 Google,每天有海量的数据需要在有限的时间内进行处理(其实每个互联网公司都会碰到这样的问题),每个程序员都需要进行分布式的程序开发,这其中包括如何分布、调度、监控以及容错等等。Google的 MapReduce 正是把分布式的业务逻辑从这些复杂的细节中抽象出来,使得没有或者很少并行开发经验的程序员也能进行并行应用程序的开发。
MapReduce 中最重要的两个词就是Map(映射)和 Reduce(规约)。初看 Map/Reduce 这两个词,熟悉 函数式语言 的人一定感觉很熟悉。FP 把这样的函数称为“higher order function”(“High order function” 被成为 Function Programming 的利器之一哦),也就是说,这些函数是被编写来与其它函数相结合(或者说被其它函数调用的)。如果说硬要比的化,可以把它想象成 C 里面的 CallBack(回调) 函数,或者 STL 里面的 Functor。比如你要对一个 STL 的容器进行查找,需要制定每两个元素相比较的Functor(Comparator),这个 Comparator 在遍历容器的时候就会被调用。(意思是map reduce这两个函数就类似C语言的回调函数.....)
拿前面说过图像处理程序来举例,其实大多数的图像处理操作都是对图像矩阵进行某种运算。这里的运算通常有两种,一种是映射,一种是规约。拿两种效果来说,”老照片”效果通常是强化照片的 G/B 值,然后对每个象素加一些随机的偏移,这些操作在二维矩阵上的每一个元素都是独立的,是 Map 操作。而”雕刻”效果需要提取图像边缘,就需要元素之间的运算了,是一种 Reduce 操作。再举个简单的例子,一个一维矩阵(数组)[0,1,2,3,4] 可以映射为 [0,2,3,6,8](乘2),也可以映射为[1,2,3,4,5](加1)。它可以规约为0(元素求积)也可以规约为10(元素求和)。
面对复杂问题,古人教导我们要“分而治之”,英文中对应的词是”Divide and Conquer“。Map/Reduce 其实就是 Divide/Conquer 的过程,通过把问题 Divide,使这些Divide 后的 Map 运算高度并行,再将 Map 后的结果 Reduce(根据某一个Key),得到最终的结果。(讲了map/reduce源于分治法)
Googler 发现这是问题的核心,其它都是共性问题。因此,他们把 Map/Reduce 抽象分离出来。这样,Google 的程序员可以只关心应用逻辑,关心根据哪些 Key 把问题进行分解,哪些操作是 Map 操作,哪些操作是 Reduce 操作。其它并行计算中的复杂问题诸如分布、工作调度、容错、机器间通信都交给 Map/Reduce Framework 去做,很大程度上简化了整个编程模型。
MapReduce 的另一个特点是,Map 和 Reduce 的输入和输出都是中间临时文件(MapReduce 利用 Google 文件系统来管理和访问这些文件),而不是不同进程间或者不同机器间的其它通信方式。我觉得,这是 Google 一贯的风格,化繁为简,返璞归真。
接下来就放下其它,研究一下 Map/Reduce 操作。(其它比如容错、备份任务也有很经典的经验和实现,论文里面都有详述)
Map的定义:
Map, written by the user, takes an input pair and produces a set of intermediate key/value pairs. The MapReduce library groups together all intermediate values associated with the same intermediate key I and passes them to the Reduce function.
Reduce的定义:
The Reduce function, also written by the user, accepts an intermediate key I and a set of values for that key. It merges together these values to form a possibly smaller set of values. Typically just zero or one output value is produced per Reduce invocation. The intermediate values are supplied to the user’s reduce function via an iterator. This allows us to handle lists of values that are too large to fit in memory.
MapReduce 论文中给出了这样一个例子:在一个文档集合中统计每个单词出现的次数。
Map 操作的输入是每一篇文档,将输入文档中每一个单词的出现输出到中间文件中去。
map(String key, String value): // key: document name // value: document contents for each word w in value: EmitIntermediate(w, “1″);
比如我们有两篇文档,内容分别是
A - “I love programming”
B - “I am a blogger, you are also a blogger”。
B 文档经过 Map 运算后输出的中间文件将会是:
I,1 am,1 a,1 blogger,1 you,1 are,1 a,1 blogger,1
Reduce 操作的输入是单词和出现次数的序列。用上面的例子来说,就是 (“I”, [1, 1]), (“love”, [1]), (“programming”, [1]), (“am”, [1]), (“a”, [1,1]) 等。然后根据每个单词,算出总的出现次数。
reduce(String key, Iterator values): // key: a word // values: a list of counts int result = 0; for each v in values: result += ParseInt(v); Emit(AsString(result));
最后输出的最终结果就会是:(“I”, 2″), (“a”, 2″)……
实际的执行顺序是:
可见,这里的分(Divide)体现在两步,分别是将输入分成 M 份,以及将 Map 的中间结果分成R份。将输入分开通常很简单,Map 的中间结果通常用“hash(key) mod R”这个结果作为标准,保证相同的 Key 出现在同一个 Partition 里面。当然,使用者也可以指定自己的 Partition Function,比如,对于 Url Key,如果希望同一个 Host 的 URL 出现在同一个 Partition,可以用“hash(Hostname(urlkey)) mod R”作为 Partition Function。
对于上面的例子来说,每个文档中都可能会出现成千上万的 (“the”, 1)这样的中间结果,(原因)琐碎的中间文件必然导致传输上的损失。因此,MapReduce还 支持用户提供 Combiner Function。(区别)这个函数通常与 Reduce Function 有相同的实现,不同点在于 Reduce 函数的输出是最终结果,而 Combiner 函数的输出是 Reduce 函数的某一个输入的中间文件。(作用:这样可以大量减少重复成千上万的中间结果。。。。)
Tom White给出了 Nutch[2] 中另一个很直观的例子,分布式Grep。我一直觉得,Pipe 中的很多操作,比如 More、Grep、Cat 都类似于一种 Map 操作,而 Sort、Uniq、wc 等都相当于某种 Reduce 操作。
加上前两天 Google 刚刚发布的 BigTable 论文,现在 Google 有了自己的集群 – Googel Cluster,分布式文件系统 – GFS,分布式计算环境 – MapReduce,分布式结构化存储 – BigTable,再加上 Lock Service。我真的能感觉的到 Google 著名的免费晚餐之外的对于程序员的另一种免费的晚餐,那个由大量的 commodity PC 组成的 large clusters。我觉得这些才真正是Google的核心价值所在。
呵 呵,就像微软老兵 Joel Spolsky(你应该看过他的”Joel on Software”吧?)曾经说过,对于微软来说最可怕的是[1],微软还在苦苦追赶 Google 来完善 Search 功能的时候,Google 已经在部署下一代的超级计算机了。
The very fact that Google invented MapReduce, and Microsoft didn’t, says something about why Microsoft is still playing catch up trying to get basic search features to work, while Google has moved on to the next problem: building Skynet^H^H^H^H^H^H the world’s largest massively parallel supercomputer. I don’t think Microsoft completely understands just how far behind they are on that wave.
注1:其实,微软也有自己的方案 – DryAd。问题是,大公司里,要想重新部署这样一个底层的 InfraStructure,无论是技术的原因,还是政治的原因,将是如何的难。
注2:Lucene 之父Doug Cutting的又一力作,Project Hadoop - 由Hadoop分布式文件系统和一个 Map/Reduce 的实现组成,Lucene/Nutch 的成产线也够齐全的了。
原文地址:http://kb.cnblogs.com/page/170370/
总结.......
介绍了Map/Reduce...粗略的。简单来说,这两个东西就是类似C语言里面的回调函数之类的,因为谷歌公司发现了程序员进行多线程或者说是并发编程时候的一些共性,都是对一些相互独立的东西进行了分离,然后计算....而对于那些同步互斥之类的手段是极其相似的,因而谷歌公司引入了Map/Reduce让初级程序员也能玩起并行编程这样的东西.....
然后粗略介绍了Map/Reduce,简单来说Map就是对输入的分离,将输入分成x份,然后选出x个空闲进程(或者是真实的机器,或者专业叫法是worker)是进行读取操作。读取完然后执行Map操作,Map操作计算后得到的相当于x份区间之中的一个小区间的结果,计算出来结果后当然是存在内存中啦,不可能你这么手残是覆盖它吧.....但是计算机是廉价的,随时可能宕机之类的....你懂得,如果你计算了99%后,worker突然崩溃了,这时候你又得重新开始计算那个小区间的Map函数了。。。。。。所以计算出来的东西会定时刷新到磁盘上,但是不能让x份数据的计算结构都输入到同一个文件吧....这样并行的瓶颈无疑凸显在这里了。。。。所以采取策略是让一系列的具有相同的key的数据放在同一个输入中间文件Partition中,有点像多条单链表数据存放在文件中....呃,这种描述是不准确的,不过也就大概这个意思.....再考虑到x份数据的输出而且是一共分成k个Partition小文件存储 。这时候.....小文件一共有x+k+1份。所以说文件数是巨大的....好吧,先记住文件数是很大的。。。等下再讨论这个,现在先把它忘了
现在经历了划分x份输入,并且经过map计算,得出x*k份结果后,结果一定是凌乱不堪,因而一个叫Reduce的worker跑起来先得对结果进行整理= =或者说是排序......排序完成以后就可以对其进行Reduce函数操作了,注意这里是对Partition文件中每一个唯一的Key执行Reduce操作。- - 如果你还是记住我那个不准确的说法的话,,那么就是对那条单链表进行Reduce函数操作。然后把结果存放在Hash里面。。最后等所有的Key都计算好了,都塞进了Hash里后,就唤醒用户进程,把结果返回给用户进程。。。。。
个人感觉吧,,,,并行算法最重要的特性是要讲究相互独立性,要求分裂后的数据处理不具有相互依赖性。。。。这个也就跟分治思想有点惊人的相似性。。。或者可以理解成是分治法在计算机领域的一个应用吧= =