map reduce

微軟著名的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容量也使得包括Single-Thread和Multi-Thread程序都能受益。也許這些還能幫助你一段時間,但問題是,我們必須做出改變,面對這個即將到來的變革,你準備好了麼?

Concurrency Programming != Multi-Thread Programming。很多人都會說MultiThreading誰不會,問題是,你是爲什麼使用/如何使用多線程的?我從前做過一個類似AcdSee一樣的圖像查看/處理程序,我通常用它來處理我的數碼照片。我在裏面用了大量的多線程,不過主要目的是在圖像處理的時候不要Block住UI,所以將CPU Intensive的計算部分用後臺線程進行處理。而並沒有把對圖像矩陣的運算並行分開。

我覺得Concurrency Programming真正的挑戰在於Programming Model的改變,在程序員的腦子裏面要對自己的程序怎樣並行化有很清楚的認識,更重要的是,如何去實現(包括架構、容錯、實時監控等等)這種並行化,如何去調試,如何去測試

在Google,每天有海量的數據需要在有限的時間內進行處理(其實每個互聯網公司都會碰到這樣的問題),每個程序員都需要進行分佈式的程序開發,這其中包括如何分佈、調度、監控以及容錯等等。Google的MapReduce正是把分佈式的業務邏輯從這些複雜的細節中抽象出來,使得沒有或者很少並行開發經驗的程序員也能進行並行應用程序的開發。

MapReduce中最重要的兩個詞就是Map(映射)和Reduce(規約)。初看Map/Reduce這兩個詞,熟悉Function Language的人一定感覺很熟悉。FP把這樣的函數稱爲”higher order function”(”High order function”被成爲Function Programming的利器之一哦),也就是說,這些函數是編寫來被與其它函數相結合(或者說被其它函數調用的)。如果說硬要比的化,可以把它想象成C裏面的CallBack函數,或者STL裏面的Functor。比如你要對一個STL的容器進行查找,需要制定每兩個元素相比較的Functor(Comparator),這個Comparator在遍歷容器的時候就會被調用。

拿前面說過圖像處理程序來舉例,其實大多數的圖像處理操作都是對圖像矩陣進行某種運算。這裏的運算通常有兩種,一種是映射,一種是規約。拿兩種效果來說,”老照片”效果通常是強化照片的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),得到最終的結果。

Googler發現這是問題的核心,其它都是共性問題。因此,他們把MapReduce抽象分離出來。這樣,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″)……

實際的執行順序是:

  1. MapReduce Library將Input分成M份。這裏的Input Splitter也可以是多臺機器並行Split
  2. Master將M份Job分給Idle狀態的M個worker來處理;
  3. 對於輸入中的每一個<key, value> pair 進行Map操作,將中間結果Buffer在Memory裏;
  4. 定期的(或者根據內存狀態),將Buffer中的中間信息Dump到本地磁盤上,並且把文件信息傳回給Master(Master需要把這些信息發送給Reduce worker)。這裏最重要的一點是,在寫磁盤的時候,需要將中間文件做Partition(比如R個)。拿上面的例子來舉例,如果把所有的信息存到一個文件,Reduce worker又會變成瓶頸。我們只需要保證相同Key能出現在同一個Partition裏面就可以把這個問題分解。
  5. R個Reduce worker開始工作,從不同的Map worker的Partition那裏拿到數據(read the buffered data from the local disks of the map workers),用key進行排序(如果內存中放不下需要用到外部排序 – external sort)。很顯然,排序(或者說Group)是Reduce函數之前必須做的一步。 這裏面很關鍵的是,每個Reduce worker會去從很多Map worker那裏拿到X(0<X<R) Partition的中間結果,這樣,所有屬於這個Key的信息已經都在這個worker上了。
  6. Reduce worker遍歷中間數據,對每一個唯一Key,執行Reduce函數(參數是這個key以及相對應的一系列Value)。
  7. 執行完畢後,喚醒用戶程序,返回結果(最後應該有R份Output,每個Reduce Worker一個)。

可見,這裏的分(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.

發佈了46 篇原創文章 · 獲贊 4 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章