谷歌技術"三寶"之MapReduce

江湖傳說永流傳:谷歌技術有"三寶",GFS、MapReduce和大表(BigTable)!

谷歌在03到06年間連續發表了三篇很有影響力的文章,分別是03年SOSP的GFS,04年OSDI的MapReduce,和06年OSDI的BigTable。SOSP和OSDI都是操作系統領域的頂級會議,在計算機學會推薦會議裏屬於A類。SOSP在單數年舉辦,而OSDI在雙數年舉辦。

那麼這篇博客就來介紹一下MapReduce。

1. MapReduce是幹啥的

因爲沒找到谷歌的示意圖,所以我想借用一張Hadoop項目的結構圖來說明下MapReduce所處的位置,如下圖。

Hadoop實際上就是谷歌三寶的開源實現,Hadoop MapReduce對應Google MapReduce,HBase對應BigTable,HDFS對應GFS。HDFS(或GFS)爲上層提供高效的非結構化存儲服務,HBase(或BigTable)是提供結構化數據服務的分佈式數據庫,Hadoop MapReduce(或Google MapReduce)是一種並行計算的編程模型,用於作業調度。

GFS和BigTable已經爲我們提供了高性能、高併發的服務,但是並行編程可不是所有程序員都玩得轉的活兒,如果我們的應用本身不能併發,那GFS、BigTable也都是沒有意義的。MapReduce的偉大之處就在於讓不熟悉並行編程的程序員也能充分發揮分佈式系統的威力。

簡單概括的說,MapReduce是將一個大作業拆分爲多個小作業的框架(大作業和小作業應該本質是一樣的,只是規模不同),用戶需要做的就是決定拆成多少份,以及定義作業本身。

下面用一個貫穿全文的例子來解釋MapReduce是如何工作的。

2. 例子:統計詞頻

如果我想統計下過去10年計算機論文出現最多的幾個單詞,看看大家都在研究些什麼,那我收集好論文後,該怎麼辦呢?

方法一:我可以寫一個小程序,把所有論文按順序遍歷一遍,統計每一個遇到的單詞的出現次數,最後就可以知道哪幾個單詞最熱門了。

這種方法在數據集比較小時,是非常有效的,而且實現最簡單,用來解決這個問題很合適。

方法二:寫一個多線程程序,併發遍歷論文。

這個問題理論上是可以高度併發的,因爲統計一個文件時不會影響統計另一個文件。當我們的機器是多核或者多處理器,方法二肯定比方法一高效。但是寫一個多線程程序要比方法一困難多了,我們必須自己同步共享數據,比如要防止兩個線程重複統計文件。

方法三:把作業交給多個計算機去完成。

我們可以使用方法一的程序,部署到N臺機器上去,然後把論文集分成N份,一臺機器跑一個作業。這個方法跑得足夠快,但是部署起來很麻煩,我們要人工把程序copy到別的機器,要人工把論文集分開,最痛苦的是還要把N個運行結果進行整合(當然我們也可以再寫一個程序)。

方法四:讓MapReduce來幫幫我們吧!

MapReduce本質上就是方法三,但是如何拆分文件集,如何copy程序,如何整合結果這些都是框架定義好的。我們只要定義好這個任務(用戶程序),其它都交給MapReduce。

在介紹MapReduce如何工作之前,先講講兩個核心函數map和reduce以及MapReduce的僞代碼。

3. map函數和reduce函數

map函數和reduce函數是交給用戶實現的,這兩個函數定義了任務本身。

  • map函數:接受一個鍵值對(key-value pair),產生一組中間鍵值對。MapReduce框架會將map函數產生的中間鍵值對裏鍵相同的值傳遞給一個reduce函數。
  • reduce函數:接受一個鍵,以及相關的一組值,將這組值進行合併產生一組規模更小的值(通常只有一個或零個值)。

統計詞頻的MapReduce函數的核心代碼非常簡短,主要就是實現這兩個函數。

map(String key, String value):
	// key: document name
	// value: document contents
	for each word w in value:
		EmitIntermediate(w, "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));

在統計詞頻的例子裏,map函數接受的鍵是文件名,值是文件的內容,map逐個遍歷單詞,每遇到一個單詞w,就產生一箇中間鍵值對<w, "1">,這表示單詞w咱又找到了一個;MapReduce將鍵相同(都是單詞w)的鍵值對傳給reduce函數,這樣reduce函數接受的鍵就是單詞w,值是一串"1"(最基本的實現是這樣,但可以優化),個數等於鍵爲w的鍵值對的個數,然後將這些“1”累加就得到單詞w的出現次數。最後這些單詞的出現次數會被寫到用戶定義的位置,存儲在底層的分佈式存儲系統(GFS或HDFS)。

4. MapReduce是如何工作的


上圖是論文裏給出的流程圖。一切都是從最上方的user program開始的,user program鏈接了MapReduce庫,實現了最基本的Map函數和Reduce函數。圖中執行的順序都用數字標記了。

  1. MapReduce庫先把user program的輸入文件劃分爲M份(M爲用戶定義),每一份通常有16MB到64MB,如圖左方所示分成了split0~4;然後使用fork將用戶進程拷貝到集羣內其它機器上。
  2. user program的副本中有一個稱爲master,其餘稱爲worker,master是負責調度的,爲空閒worker分配作業(Map作業或者Reduce作業),worker的數量也是可以由用戶指定的。
  3. 被分配了Map作業的worker,開始讀取對應分片的輸入數據,Map作業數量是由M決定的,和split一一對應;Map作業從輸入數據中抽取出鍵值對,每一個鍵值對都作爲參數傳遞給map函數,map函數產生的中間鍵值對被緩存在內存中。
  4. 緩存的中間鍵值對會被定期寫入本地磁盤,而且被分爲R個區,R的大小是由用戶定義的,將來每個區會對應一個Reduce作業;這些中間鍵值對的位置會被通報給master,master負責將信息轉發給Reduce worker。
  5. master通知分配了Reduce作業的worker它負責的分區在什麼位置(肯定不止一個地方,每個Map作業產生的中間鍵值對都可能映射到所有R個不同分區),當Reduce worker把所有它負責的中間鍵值對都讀過來後,先對它們進行排序,使得相同鍵的鍵值對聚集在一起。因爲不同的鍵可能會映射到同一個分區也就是同一個Reduce作業(誰讓分區少呢),所以排序是必須的。
  6. reduce worker遍歷排序後的中間鍵值對,對於每個唯一的鍵,都將鍵與關聯的值傳遞給reduce函數,reduce函數產生的輸出會添加到這個分區的輸出文件中。
  7. 當所有的Map和Reduce作業都完成了,master喚醒正版的user program,MapReduce函數調用返回user program的代碼。

所有執行完畢後,MapReduce輸出放在了R個分區的輸出文件中(分別對應一個Reduce作業)。用戶通常並不需要合併這R個文件,而是將其作爲輸入交給另一個MapReduce程序處理。整個過程中,輸入數據是來自底層分佈式文件系統(GFS)的,中間數據是放在本地文件系統的,最終輸出數據是寫入底層分佈式文件系統(GFS)的。而且我們要注意Map/Reduce作業和map/reduce函數的區別:Map作業處理一個輸入數據的分片,可能需要調用多次map函數來處理每個輸入鍵值對;Reduce作業處理一個分區的中間鍵值對,期間要對每個不同的鍵調用一次reduce函數,Reduce作業最終也對應一個輸出文件。

我更喜歡把流程分爲三個階段。第一階段是準備階段,包括1、2,主角是MapReduce庫,完成拆分作業和拷貝用戶程序等任務;第二階段是運行階段,包括3、4、5、6,主角是用戶定義的map和reduce函數,每個小作業都獨立運行着;第三階段是掃尾階段,這時作業已經完成,作業結果被放在輸出文件裏,就看用戶想怎麼處理這些輸出了。

5. 詞頻是怎麼統計出來的

結合第四節,我們就可以知道第三節的代碼是如何工作的了。假設咱們定義M=5,R=3,並且有6臺機器,一臺master。


這幅圖描述了MapReduce如何處理詞頻統計。由於map worker數量不夠,首先處理了分片1、3、4,併產生中間鍵值對;當所有中間值都準備好了,Reduce作業就開始讀取對應分區,並輸出統計結果。

6. 用戶的權利

用戶最主要的任務是實現map和reduce接口,但還有一些有用的接口是向用戶開放的。
  • an input reader。這個函數會將輸入分爲M個部分,並且定義瞭如何從數據中抽取最初的鍵值對,比如詞頻的例子中定義文件名和文件內容是鍵值對。
  • a partition function。這個函數用於將map函數產生的中間鍵值對映射到一個分區裏去,最簡單的實現就是將鍵求哈希再對R取模。
  • a compare function。這個函數用於Reduce作業排序,這個函數定義了鍵的大小關係。
  • an output writer。負責將結果寫入底層分佈式文件系統。
  • a combiner function。實際就是reduce函數,這是用於前面提到的優化的,比如統計詞頻時,如果每個<w, "1">要讀一次,因爲reduce和map通常不在一臺機器,非常浪費時間,所以可以在map執行的地方先運行一次combiner,這樣reduce只需要讀一次<w, "n">了。
  • map和reduce函數就不多說了。

7. MapReduce的實現

目前MapReduce已經有多種實現,除了谷歌自己的實現外,還有著名的hadoop,區別是谷歌是c++,而hadoop是用java。另外斯坦福大學實現了一個在多核/多處理器、共享內存環境內運行的MapReduce,稱爲Phoenix(介紹),相關的論文發表在07年的HPCA,是當年的最佳論文哦!

參考文獻

[1] MapReduce : Simplified Data Processing on Large Clusters. In proceedings of OSDI'04.
[4] Evaluating MapReduce for Multi-core and Multiprocessor Systems. In proceedings of HPCA'07.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章