大資料計算的基石——MapReduce

獨孤風發表於2020-09-01

MapReduce

Google File System提供了大資料儲存的方案,這也為後來HDFS提供了理論依據,但是在大資料儲存之上的大資料計算則不得不提到MapReduce。

雖然現在通過框架的不斷髮展,MapReduce已經漸漸的淡出人們的視野,越來越多的框架提供了簡單的SQL語法來進行大資料計算。但是,MapReduce所提供的程式設計模型為這一切奠定了基礎,所以Google的這篇MapReduce 論文值得我們去認真的研讀。

摘要

MapReduce 是一個程式設計模型,也是一個處理和生成超大資料集的演算法模型的相關實現。使用者首先建立一個 Map 函式處理一個基於 key/value pair 的資料集合,輸出中間的基於 key/value pair 的資料集合;然後再建立一個 Reduce 函式用來合併所有的具有相同中間 key 值的中間 value 值。現實世界中有很多滿足上述處理模型的例子,本論文將詳細描述這個模型。
MapReduce 架構的程式能夠在大量的普通配置的計算機上實現並行化處理。這個系統在執行時只關心:
如何分割輸入資料,在大量計算機組成的叢集上的排程,叢集中計算機的錯誤處理,管理叢集中計算機之間必要的通訊。採用 MapReduce 架構可以使那些沒有平行計算和分散式處理系統開發經驗的程式設計師有效利用分散式系統的豐富資源。我們的 MapReduce 實現執行在規模可以靈活調整的由普通機器組成的叢集上:一個典型的 MapReduce計算往往由幾千臺機器組成、處理以 TB 計算的資料。程式設計師發現這個系統非常好用:已經實現了數以百計的 MapReduce 程式,在 Google 的叢集上,每天都有 1000 多個 MapReduce 程式在執行。

1 介紹

在過去的 5 年裡,包括本文作者在內的 Google 的很多程式設計師,為了處理海量的原始資料,已經實現了數以百計的、專用的計算方法。這些計算方法用來處理大量的原始資料,比如,文件抓取(類似網路爬蟲的程式)、Web 請求日誌等等;也為了計算處理各種型別的衍生資料,比如倒排索引、Web 文件的圖結構的各種表示形勢、每臺主機上網路爬蟲抓取的頁面數量的彙總、每天被請求的最多的查詢的集合等等。大多數這樣的資料處理運算在概念上很容易理解。然而由於輸入的資料量巨大,因此要想在可接受的時間內完成運算,只有將這些計算分佈在成百上千的主機上。如何處理平行計算、如何分發資料、如何處理錯誤?所有這些問題綜合在一起,需要大量的程式碼處理,因此也使得原本簡單的運算變得難以處理。
為了解決上述複雜的問題,我們設計一個新的抽象模型,使用這個抽象模型,我們只要表述我們想要執行的簡單運算即可,而不必關心平行計算、容錯、資料分佈、負載均衡等複雜的細節,這些問題都被封裝在了一個庫裡面。設計這個抽象模型的靈感來自 Lisp 和許多其他函式式語言的 Map 和 Reduce 的原語。我們意識到我們大多數的運算都包含這樣的操作:在輸入資料的“邏輯”記錄上應用 Map 操作得出一箇中間 key/value pair 集合,然後在所有具有相同 key 值的 value 值上應用 Reduce 操作,從而達到合併中間的資料,得到一個想要的結果的目的。使用 MapReduce 模型,再結合使用者實現的 Map 和 Reduce 函式,我們就可以非常容易的實現大規模並行化計算;通過 MapReduce 模型自帶的“再次執行”(re-execution)功能,也提供了初級的容災實現方案。
這個工作(實現一個 MapReduce 框架模型)的主要貢獻是通過簡單的介面來實現自動的並行化和大規模的分散式計算,通過使用 MapReduce 模型介面實現在大量普通的 PC 機上高效能運算。
第二部分描述基本的程式設計模型和一些使用案例。
第三部分描述了一個經過裁剪的、適合我們的基於叢集的計算環境MapReduce 實現。
第四部分描述我們認為在 MapReduce 程式設計模型中一些實用的技巧。
第五部分對於各種不同的任務,測量我們 MapReduce 實現的效能。
第六部分揭示了在 Google 內部如何使用 MapReduce 作為基礎重寫我們的索引系統產品,包括其它一些使用 MapReduce 的經驗。
第七部分討論相關的和未來的工作。

2 程式設計模型

MapReduce 程式設計模型的原理是:利用一個輸入 key/value pair 集合來產生一個輸出的 key/value pair 集合。
MapReduce 庫的使用者用兩個函式表達這個計算:Map 和 Reduce。
使用者自定義的 Map 函式接受一個輸入的 key/value pair 值,然後產生一箇中間 key/value pair 值的集合。
MapReduce 庫把所有具有相同中間 key 值 I 的中間 value 值集合在一起後傳遞給 reduce 函式。
使用者自定義的 Reduce 函式接受一箇中間 key 的值 I 和相關的一個 value 值的集合。Reduce 函式合併這些value 值,形成一個較小的 value 值的集合。一般的,每次 Reduce 函式呼叫只產生 0 或 1 個輸出 value 值。通常我們通過一個迭代器把中間 value 值提供給 Reduce 函式,這樣我們就可以處理無法全部放入記憶體中的大量
的 value 值的集合。

2.1 例子

例如,計算一個大的文件集合中每個單詞出現的次數,下面是虛擬碼段:

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 函式輸出文件中的每個詞、以及這個詞的出現次數(在這個簡單的例子裡就是 1)。Reduce 函式把 Map函式產生的每一個特定的詞的計數累加起來。
另外,使用者編寫程式碼,使用輸入和輸出檔案的名字、可選的調節引數來完成一個符合 MapReduce 模型規範的物件,然後呼叫 MapReduce 函式,並把這個規範物件傳遞給它。使用者的程式碼和 MapReduce 庫連結在一起(用 C++實現)。附錄 A 包含了這個例項的全部程式程式碼。

2.2 型別

儘管在前面例子的虛擬碼中使用了以字串表示的輸入輸出值,但是在概念上,使用者定義的Map和Reduce函式都有相關聯的型別:

map(k1,v1) ->list(k2,v2)
reduce(k2,list(v2)) ->list(v2)

比如,輸入的 key 和 value 值與輸出的 key 和 value 值在型別上推導的域不同。此外,中間 key 和 value
值與輸出 key 和 value 值在型別上推導的域相同。 2
我們的 C++中使用字串型別作為使用者自定義函式的輸入輸出,使用者在自己的程式碼中對字串進行適當
的型別轉換。

2.3 更多的例子

這裡還有一些有趣的簡單例子,可以很容易的使用 MapReduce 模型來表示:
分散式的 Grep:Map 函式輸出匹配某個模式的一行,Reduce 函式是一個恆等函式,即把中間資料複製到輸出。
計算 URL 訪問頻率:Map 函式處理日誌中 web 頁面請求的記錄,然後輸出(URL,1)。Reduce 函式把相同URL 的 value 值都累加起來,產生(URL,記錄總數)結果。
倒轉網路連結圖:Map 函式在源頁面(source)中搜尋所有的連結目標(target)並輸出為(target,source)。
Reduce 函式把給定連結目標(target)的連結組合成一個列表,輸出(target,list(source))。
每個主機的檢索詞向量:檢索詞向量用一個(詞,頻率)列表來概述出現在文件或文件集中的最重要的一些詞。Map 函式為每一個輸入文件輸出(主機名,檢索詞向量),其中主機名來自文件的 URL。Reduce 函式接收給定主機的所有文件的檢索詞向量,並把這些檢索詞向量加在一起,丟棄掉低頻的檢索詞,輸出一個最終的(主機名,檢索詞向量)。
倒排索引:Map 函式分析每個文件輸出一個(詞,文件號)的列表,Reduce 函式的輸入是一個給定詞的所有(詞,文件號),排序所有的文件號,輸出(詞,list(文件號))。所有的輸出集合形成一個簡單的倒排索引,它以一種簡單的演算法跟蹤詞在文件中的位置。
分散式排序:Map 函式從每個記錄提取 key,輸出(key,record)。Reduce 函式不改變任何的值。這個運算依賴分割槽機制(在 4.1 描述)和排序屬性(在 4.2 描述)。

3 實現

​ MapReduce 模型可以有多種不同的實現方式。如何正確選擇取決於具體的環境。例如,一種實現方式適用於小型的共享記憶體方式的機器,另外一種實現方式則適用於大型 NUMA 架構的多處理器的主機,而有的實現方式更適合大型的網路連線叢集。
本章節描述一個適用於 Google 內部廣泛使用的運算環境的實現:用乙太網交換機連線、由普通 PC 機組成的大型叢集。在我們的環境裡包括:

  1. x86 架構、執行 Linux 作業系統、雙處理器、2-4GB 記憶體的機器。

  2. 普通的網路硬體裝置,每個機器的頻寬為百兆或者千兆,但是遠小於網路的平均頻寬的一半。

  3. 叢集中包含成百上千的機器,因此,機器故障是常態。

  4. 儲存為廉價的內建 IDE 硬碟。一個內部分散式檔案系統用來管理儲存在這些磁碟上的資料。檔案系
    統通過資料複製來在不可靠的硬體上保證資料的可靠性和有效性。

  5. 使用者提交工作(job)給排程系統。每個工作(job)都包含一系列的任務(task),排程系統將這些任
    務排程到叢集中多臺可用的機器上。

3.1 執行概括

通過將 Map 呼叫的輸入資料自動分割為 M 個資料片段的集合,Map 呼叫被分佈到多臺機器上執行。輸入的資料片段能夠在不同的機器上並行處理。使用分割槽函式將 Map 呼叫產生的中間 key 值分成 R 個不同分割槽(例如,hash(key) mod R),Reduce 呼叫也被分佈到多臺機器上執行。分割槽數量(R)和分割槽函式由使用者來指定。

圖 1 展示了我們的 MapReduce 實現中操作的全部流程。當使用者呼叫 MapReduce 函式時,將發生下面的一系列動作(下面的序號和圖 1 中的序號一一對應):

  1. 使用者程式首先呼叫的 MapReduce 庫將輸入檔案分成 M 個資料片度,每個資料片段的大小一般從16MB 到 64MB(可以通過可選的引數來控制每個資料片段的大小)。然後使用者程式在機群中建立大量的程式副本。

  2. 這些程式副本中的有一個特殊的程式–master。副本中其它的程式都是 worker 程式,由 master 分配任務。有 M 個 Map 任務和 R 個 Reduce 任務將被分配,master 將一個 Map 任務或 Reduce 任務分配給一個空閒的 worker。

  3. 被分配了 map 任務的 worker 程式讀取相關的輸入資料片段,從輸入的資料片段中解析出 key/valuepair,然後把 key/value pair 傳遞給使用者自定義的 Map 函式,由 Map 函式生成並輸出的中間 key/valuepair,並快取在記憶體中。

  4. 快取中的 key/value pair 通過分割槽函式分成 R 個區域,之後週期性的寫入到本地磁碟上。快取的key/value pair 在本地磁碟上的儲存位置將被回傳給 master,由 master 負責把這些儲存位置再傳送給Reduce worker。

  5. 當 Reduce worker 程式接收到 master 程式發來的資料儲存位置資訊後,使用 RPC 從 Map worker 所在主機的磁碟上讀取這些快取資料。當 Reduce worker 讀取了所有的中間資料後,通過對 key 進行排序
    後使得具有相同 key 值的資料聚合在一起。由於許多不同的 key 值會對映到相同的 Reduce 任務上,因此必須進行排序。如果中間資料太大無法在記憶體中完成排序,那麼就要在外部進行排序。

  6. Reduce worker 程式遍歷排序後的中間資料,對於每一個唯一的中間 key 值,Reduce worker 程式將這個 key 值和它相關的中間 value 值的集合傳遞給使用者自定義的 Reduce 函式。Reduce 函式的輸出被追加到所屬分割槽的輸出檔案。

  7. 當所有的 Map 和 Reduce 任務都完成之後,master 喚醒使用者程式。在這個時候,在使用者程式裡的對MapReduce 呼叫才返回。

在成功完成任務之後,MapReduce 的輸出存放在 R 個輸出檔案中(對應每個 Reduce 任務產生一個輸出檔案,檔名由使用者指定)。一般情況下,使用者不需要將這 R 個輸出檔案合併成一個檔案–他們經常把這些檔案作為另外一個MapReduce 的輸入,或者在另外一個可以處理多個分割檔案的分散式應用中使用。

3.2 Master 資料結構

​ Master 持有一些資料結構,它儲存每一個 Map 和 Reduce 任務的狀態(空閒、工作中或完成),以及 Worker機器(非空閒任務的機器)的標識。
Master 就像一個資料管道,中間檔案儲存區域的位置資訊通過這個管道從 Map 傳遞到 Reduce。因此,對於每個已經完成的 Map 任務,master 儲存了 Map 任務產生的 R 箇中間檔案儲存區域的大小和位置。當 Map任務完成時,Master 接收到位置和大小的更新資訊,這些資訊被逐步遞增的推送給那些正在工作的 Reduce 任務。

3.3 容錯

因為 MapReduce 庫的設計初衷是使用由成百上千的機器組成的叢集來處理超大規模的資料,所以,這個庫必須要能很好的處理機器故障。

3.3.1 worker 故障

master 週期性的 ping 每個 worker。如果在一個約定的時間範圍內沒有收到 worker 返回的資訊,master 將把這個 worker 標記為失效。所有由這個失效的 worker 完成的 Map 任務被重設為初始的空閒狀態,之後這些任務就可以被安排給其他的 worker。同樣的,worker 失效時正在執行的 Map 或 Reduce 任務也將被重新置為空閒狀態,等待重新排程。
當 worker 故障時,由於已經完成的 Map 任務的輸出儲存在這臺機器上,Map 任務的輸出已不可訪問了,因此必須重新執行。而已經完成的 Reduce 任務的輸出儲存在全域性檔案系統上,因此不需要再次執行。
當一個 Map 任務首先被 worker A 執行,之後由於 worker A 失效了又被排程到 worker B 執行,這個“重新執行”的動作會被通知給所有執行 Reduce 任務的 worker。任何還沒有從 worker A 讀取資料的 Reduce 任務將從 worker B 讀取資料。
MapReduce 可以處理大規模 worker 失效的情況。比如,在一個 MapReduce 操作執行期間,在正在執行的叢集上進行網路維護引起 80 臺機器在幾分鐘內不可訪問了,MapReduce master 只需要簡單的再次執行那些不可訪問的worker 完成的工作,之後繼續執行未完成的任務,直到最終完成這個 MapReduce 操作。

3.3.2 master 失敗

​ 一個簡單的解決辦法是讓 master 週期性的將上面描述的資料結構的寫入磁碟,即檢查點(checkpoint)。如果這個 master 任務失效了,可以從最後一個檢查點(checkpoint)開始啟動另一個master 程式。然而,由於只有一個 master 程式,master 失效後再恢復是比較麻煩的,因此我們現在的實現是如果 master 失效,就中止 MapReduce 運算。客戶可以檢查到這個狀態,並且可以根據需要重新執行 MapReduce操作。

3.3.3 在失效方面的處理機制

​ 當使用者提供的 Map 和 Reduce 操作是輸入確定性函式(即相同的輸入產生相同的輸出)時,我們的分散式實現在任何情況下的輸出都和所有程式沒有出現任何錯誤、順序的執行產生的輸出是一樣的。
我們依賴對 Map 和 Reduce 任務的輸出是原子提交的來完成這個特性。每個工作中的任務把它的輸出寫到私有的臨時檔案中。每個 Reduce 任務生成一個這樣的檔案,而每個 Map 任務則生成 R 個這樣的檔案(一個 Reduce 任務對應一個檔案)。當一個 Map 任務完成的時,worker 傳送一個包含 R 個臨時檔名的完成訊息給 master。如果 master 從一個已經完成的 Map 任務再次接收到到一個完成訊息,master 將忽略這個訊息;否則,master 將這 R 個檔案的名字記錄在資料結構裡。
當 Reduce 任務完成時,Reduce worker 程式以原子的方式把臨時檔案重新命名為最終的輸出檔案。如果同一個 Reduce 任務在多臺機器上執行,針對同一個最終的輸出檔案將有多個重新命名操作執行。我們依賴底層檔案系統提供的重新命名操作的原子性來保證最終的檔案系統狀態僅僅包含一個 Reduce 任務產生的資料。
使用 MapReduce 模型的程式設計師可以很容易的理解他們程式的行為,因為我們絕大多數的 Map 和 Reduce操作是確定性的,而且存在這樣的一個事實:我們的失效處理機制等價於一個順序的執行的操作。當 Map 和 Reduce 操作是不確定性的時候,我們提供雖然較弱但是依然合理的處理機制。當使用非確定操作的時候,
一個 Reduce 任務 R1 的輸出等價於一個非確定性程式順序執行產生時的輸出。但是,另一個 Reduce 任務 R2的輸出也許符合一個不同的非確定順序程式執行產生的 R2 的輸出。
考慮 Map 任務 M 和 Reduce 任務 R1、R2 的情況。我們設定 e(Ri)是 Ri 已經提交的執行過程(有且僅有一個這樣的執行過程)。當 e(R1)讀取了由 M 一次執行產生的輸出,而 e(R2)讀取了由 M 的另一次執行產生的輸出,導致了較弱的失效處理。

3.4 儲存位置

​ 在我們的計算執行環境中,網路頻寬是一個相當匱乏的資源。我們通過儘量把輸入資料(由 GFS 管理)儲存在叢集中機器的本地磁碟上來節省網路頻寬。GFS 把每個檔案按 64MB 一個 Block 分隔,每個 Block 儲存在多臺機器上,環境中就存放了多份拷貝(一般是 3 個拷貝)。MapReduce 的 master 在排程 Map 任務時會考慮輸入檔案的位置資訊,儘量將一個 Map 任務排程在包含相關輸入資料拷貝的機器上執行;如果上述努力失敗了,master 將嘗試在儲存有輸入資料拷貝的機器附近的機器上執行 Map 任務(例如,分配到一個和包含輸入數
據的機器在一個 switch 裡的 worker 機器上執行)。當在一個足夠大的 cluster 叢集上執行大型 MapReduce 操作的時候,大部分的輸入資料都能從本地機器讀取,因此消耗非常少的網路頻寬。

3.5 任務粒度

​ 如前所述,我們把 Map 拆分成了 M 個片段、把 Reduce 拆分成 R 個片段執行。理想情況下,M 和 R 應當比叢集中 worker 的機器數量要多得多。在每臺 worker 機器都執行大量的不同任務能夠提高叢集的動態的負載均衡能力,並且能夠加快故障恢復的速度:失效機器上執行的大量 Map 任務都可以分佈到所有其他的 worker機器上去執行。
但是實際上,在我們的具體實現中對 M 和 R 的取值都有一定的客觀限制,因為 master 必須執行 O(M+R)次排程,並且在記憶體中儲存 O(MR)個狀態(對影響記憶體使用的因素還是比較小的:O(MR)塊狀態,大概每對 Map 任務/Reduce 任務 1 個位元組就可以了)。更進一步,R 值通常是由使用者指定的,因為每個 Reduce 任務最終都會生成一個獨立的輸出檔案。實際使用時我們也傾向於選擇合適的 M 值,以使得每一個獨立任務都是處理大約 16M 到 64M 的輸入資料(這樣,
上面描寫的輸入資料本地儲存優化策略才最有效),另外,我們把 R 值設定為我們想使用的 worker 機器數量的小的倍數。我們通常會用這樣的比例來執行 MapReduce:M=200000,R=5000,使用 2000 臺 worker 機器。

3.6 備用任務

影響一個 MapReduce 的總執行時間最通常的因素是“落伍者”:在運算過程中,如果有一臺機器花了很長的時間才完成最後幾個 Map 或 Reduce 任務,導致 MapReduce 操作總的執行時間超過預期。出現“落伍者”的原因非常多。比如:如果一個機器的硬碟出了問題,在讀取的時候要經常的進行讀取糾錯操作,導致讀取資料的速度從 30M/s 降低到 1M/s。如果 cluster 的排程系統在這臺機器上又排程了其他的任務,由於 CPU、記憶體、本地硬碟和網路頻寬等競爭因素的存在,導致執行 MapReduce 程式碼的執行效率更加緩慢。我們最近遇到的一個問題是由於機器的初始化程式碼有 bug,導致關閉了的處理器的快取:在這些機器上執行任務的效能和正常情況相差上百倍。

我們有一個通用的機制來減少“落伍者”出現的情況。當一個 MapReduce 操作接近完成的時候,master排程備用(backup)任務程式來執行剩下的、處於處理中狀態(in-progress)的任務。無論是最初的執行程式、還是備用(backup)任務程式完成了任務,我們都把這個任務標記成為已經完成。我們調優了這個機制,通常只會佔用比正常操作多幾個百分點的計算資源。我們發現採用這樣的機制對於減少超大 MapReduce 操作的總處理時間效果顯著。例如,在 5.3 節描述的排序任務,在關閉掉備用任務的情況下要多花 44%的時間完成排序任務。

4 技巧

雖然簡單的 Map 和 Reduce 函式提供的基本功能已經能夠滿足大部分的計算需要,我們還是發掘出了一些有價值的擴充套件功能。本節將描述這些擴充套件功能。

4.1 分割槽函式

MapReduce 的使用者通常會指定 Reduce 任務和 Reduce 任務輸出檔案的數量(R)。我們在中間 key 上使用分割槽函式來對資料進行分割槽,之後再輸入到後續任務執行程式。一個預設的分割槽函式是使用 hash 方法(比如,hash(key) mod R)進行分割槽。hash 方法能產生非常平衡的分割槽。然而,有的時候,其它的一些分割槽函式對 key值進行的分割槽將非常有用。比如,輸出的 key 值是 URLs,我們希望每個主機的所有條目保持在同一個輸出檔案中。為了支援類似的情況,MapReduce庫的使用者需要提供專門的分割槽函式。

例如使用“hash(Hostname(urlkey)) mod R”作為分割槽函式就可以把所有來自同一個主機的 URLs 儲存在同一個輸出檔案中。

4.2 順序保證

​ 我們確保在給定的分割槽中,中間 key/value pair 資料的處理順序是按照 key 值增量順序處理的。這樣的順序保證對每個分成生成一個有序的輸出檔案,這對於需要對輸出檔案按 key 值隨機存取的應用非常有意義,對在排序輸出的資料集也很有幫助。

4.3 Combiner 函式

在某些情況下,Map 函式產生的中間 key 值的重複資料會佔很大的比重,並且,使用者自定義的 Reduce 函式滿足結合律和交換律。在 2.1 節的詞數統計程式是個很好的例子。由於詞頻率傾向於一個 zipf 分佈(齊夫分佈),每個 Map 任務將產生成千上萬個這樣的記錄<the,1>。所有的這些記錄將通過網路被髮送到一個單獨的Reduce 任務,然後由這個 Reduce 任務把所有這些記錄累加起來產生一個數字。我們允許使用者指定一個可選的 combiner 函式,combiner 函式首先在本地將這些記錄進行一次合併,然後將合併的結果再通過網路傳送出去。

Combiner 函式在每臺執行 Map 任務的機器上都會被執行一次。一般情況下,Combiner 和 Reduce 函式是一樣的。Combiner 函式和 Reduce 函式之間唯一的區別是 MapReduce 庫怎樣控制函式的輸出。Reduce 函式的輸出被儲存在最終的輸出檔案裡,而 Combiner 函式的輸出被寫到中間檔案裡,然後被髮送給 Reduce 任務。
部分的合併中間結果可以顯著的提高一些 MapReduce 操作的速度。附錄 A 包含一個使用 combiner 函式的例子。

4.4 輸入和輸出的型別

​ MapReduce 庫支援幾種不同的格式的輸入資料。比如,文字模式的輸入資料的每一行被視為是一個key/value pair。key 是檔案的偏移量,value 是那一行的內容。另外一種常見的格式是以 key 進行排序來儲存的 key/value pair 的序列。每種輸入型別的實現都必須能夠把輸入資料分割成資料片段,該資料片段能夠由單獨的 Map 任務來進行後續處理(例如,文字模式的範圍分割必須確保僅僅在每行的邊界進行範圍分割)。雖然大多數 MapReduce 的使用者僅僅使用很少的預定義輸入型別就滿足要求了,但是使用者依然可以通過提供一
個簡單的 Reader 介面實現就能夠支援一個新的輸入型別。
​ Reader 並非一定要從檔案中讀取資料,比如,我們可以很容易的實現一個從資料庫裡讀記錄的 Reader,或者從記憶體中的資料結構讀取資料的 Reader。
類似的,我們提供了一些預定義的輸出資料的型別,通過這些預定義型別能夠產生不同格式的資料。使用者採用類似新增新的輸入資料型別的方式增加新的輸出型別。

4.5 副作用

​ 在某些情況下,MapReduce 的使用者發現,如果在 Map 和/或 Reduce 操作過程中增加輔助的輸出檔案會比較省事。我們依靠程式 writer 把這種“副作用”變成原子的和冪等的 3 。通常應用程式首先把輸出結果寫到一個臨時檔案中,在輸出全部資料之後,在使用系統級的原子操作 rename 重新命名這個臨時檔案。
如果一個任務產生了多個輸出檔案,我們沒有提供類似兩階段提交的原子操作支援這種情況。因此,對於會產生多個輸出檔案、並且對於跨檔案有一致性要求的任務,都必須是確定性的任務。但是在實際應用過程中,這個限制還沒有給我們帶來過麻煩。

4.6 跳過損壞的記錄

​ 有時候,使用者程式中的 bug 導致 Map 或者 Reduce 函式在處理某些記錄的時候 crash 掉,MapReduce 操作無法順利完成。慣常的做法是修復 bug 後再次執行 MapReduce 操作,但是,有時候找出這些 bug 並修復它們不是一件容易的事情;這些 bug 也許是在第三方庫裡邊,而我們手頭沒有這些庫的原始碼。而且在很多時候,忽略一些有問題的記錄也是可以接受的,比如在一個巨大的資料集上進行統計分析的時候。我們提供了一種執行模式,在這種模式下,為了保證保證整個處理能繼續進行,MapReduce會檢測哪些記錄導致確定性的crash,並且跳過這些記錄不處理。
​ 每個 worker 程式都設定了訊號處理函式捕獲記憶體段異常(segmentation violation)和匯流排錯誤(bus error)。
​ 在執行 Map 或者 Reduce 操作之前,MapReduce 庫通過全域性變數儲存記錄序號。如果使用者程式觸發了一個系統訊號,訊息處理函式將用“最後一口氣”通過 UDP 包向 master 傳送處理的最後一條記錄的序號。當 master看到在處理某條特定記錄不止失敗一次時,master 就標誌著條記錄需要被跳過,並且在下次重新執行相關的Map 或者 Reduce 任務的時候跳過這條記錄。

4.7 本地執行

​ 除錯 Map 和 Reduce 函式的 bug 是非常困難的,因為實際執行操作時不但是分佈在系統中執行的,而且通常是在好幾千臺計算機上執行,具體的執行位置是由 master 進行動態排程的,這又大大增加了除錯的難度。
​ 為了簡化除錯、profile 和小規模測試,我們開發了一套 MapReduce 庫的本地實現版本,通過使用本地版本的MapReduce 庫,MapReduce 操作在本地計算機上順序的執行。使用者可以控制 MapReduce 操作的執行,可以把操作限制到特定的 Map 任務上。使用者通過設定特別的標誌來在本地執行他們的程式,之後就可以很容易的使用本地除錯和測試工具(比如 gdb)。

4.8 狀態資訊

​ master 使用嵌入式的 HTTP 伺服器(如 Jetty)顯示一組狀態資訊頁面,使用者可以監控各種執行狀態。狀態資訊頁面顯示了包括計算執行的進度,比如已經完成了多少任務、有多少任務正在處理、輸入的位元組數、中間資料的位元組數、輸出的位元組數、處理百分比等等。頁面還包含了指向每個任務的 stderr 和 stdout 檔案的連結。使用者根據這些資料預測計算需要執行大約多長時間、是否需要增加額外的計算資源。這些頁面也可以用來分析什麼時候計算執行的比預期的要慢。
另外,處於最頂層的狀態頁面顯示了哪些 worker 失效了,以及他們失效的時候正在執行的 Map 和 Reduce任務。這些資訊對於除錯使用者程式碼中的 bug 很有幫助。

4.9 計數器

​ MapReduce 庫使用計數器統計不同事件發生次數。比如,使用者可能想統計已經處理了多少個單詞、已經索引的多少篇 German 文件等等。
​ 為了使用這個特性,使用者在程式中建立一個命名的計數器物件,在 Map 和 Reduce 函式中相應的增加計數器的值。例如:

Counter* uppercase;
uppercase = GetCounter(“uppercase”);
map(String name, String contents):
	for each word w in contents:
		if (IsCapitalized(w)):
			uppercase->Increment();
			EmitIntermediate(w, “1″);

​ 這些計數器的值週期性的從各個單獨的worker機器上傳遞給master (附加在ping的應答包中傳遞)。master把執行成功的 Map 和 Reduce 任務的計數器值進行累計,當 MapReduce 操作完成之後,返回給使用者程式碼。
​ 計數器當前的值也會顯示在 master 的狀態頁面上,這樣使用者就可以看到當前計算的進度。當累加計數器的值的時候,master 要檢查重複執行的 Map 或者 Reduce 任務,避免重複累加(之前提到的備用任務和失效後重新執行任務這兩種情況會導致相同的任務被多次執行)。
​ 有些計數器的值是由 MapReduce 庫自動維持的,比如已經處理的輸入的 key/value pair 的數量、輸出的key/value pair 的數量等等。
​ 計數器機制對於 MapReduce 操作的完整性檢查非常有用。比如,在某些 MapReduce 操作中,使用者需要確保輸出的 key value pair 精確的等於輸入的 key value pair,或者處理的 German 文件數量在處理的整個文件數量中屬於合理範圍。

5 效能

​ 本節我們用在一個大型叢集上執行的兩個計算來衡量 MapReduce 的效能。一個計算在大約 1TB 的資料中進行特定的模式匹配,另一個計算對大約 1TB 的資料進行排序。
這兩個程式在大量的使用 MapReduce 的實際應用中是非常典型的 — 一類是對資料格式進行轉換,從一種表現形式轉換為另外一種表現形式;另一類是從海量資料中抽取少部分的使用者感興趣的資料。

5.1 叢集配置

​ 所有這些程式都執行在一個大約由 1800 臺機器構成的叢集上。每臺機器配置 2 個 2G 主頻、支援超執行緒的 Intel Xeon 處理器,4GB 的實體記憶體,兩個 160GB 的 IDE 硬碟和一個千兆乙太網卡。這些機器部署在一個兩層的樹形交換網路中,在 root 節點大概有 100-200GBPS 的傳輸頻寬。所有這些機器都採用相同的部署(對等部署),因此任意兩點之間的網路來回時間小於 1 毫秒。
​ 在 4GB 記憶體裡,大概有 1-1.5G 用於執行在叢集上的其他任務。測試程式在週末下午開始執行,這時主機的 CPU、磁碟和網路基本上處於空閒狀態。

5.2 GREP

​ 這個分散式的 grep 程式需要掃描大概 10 的 10 次方個由 100 個位元組組成的記錄,查詢出現概率較小的 3個字元的模式(這個模式在 92337 個記錄中出現)。輸入資料被拆分成大約 64M 的 Block(M=15000),整個輸出資料存放在一個檔案中(R=1)。

圖 2 顯示了這個運算隨時間的處理過程。其中 Y 軸表示輸入資料的處理速度。處理速度隨著參與MapReduce 計算的機器數量的增加而增加,當 1764 臺 worker 參與計算的時,處理速度達到了 30GB/s。當Map 任務結束的時候,即在計算開始後 80 秒,輸入的處理速度降到 0。整個計算過程從開始到結束一共花了大概 150 秒。這包括了大約一分鐘的初始啟動階段。初始啟動階段消耗的時間包括了是把這個程式傳送到各個 worker 機器上的時間、等待 GFS 檔案系統開啟 1000 個輸入檔案集合的時間、獲取相關的檔案本地位置優化資訊的時間。

5.3 排序

排序程式處理 10 的 10 次方個 100 個位元組組成的記錄(大概 1TB 的資料)。這個程式模仿 TeraSort benchmark[10]。

排序程式由不到 50 行程式碼組成。只有三行的 Map 函式從文字行中解析出 10 個位元組的 key 值作為排序的key,並且把這個 key 和原始文字行作為中間的 key/value pair 值輸出。我們使用了一個內建的恆等函式作為Reduce 操作函式。這個函式把中間的 key/value pair 值不作任何改變輸出。最終排序結果輸出到兩路複製的GFS 檔案系統(也就是說,程式輸出 2TB 的資料)。
如前所述,輸入資料被分成 64MB 的 Block(M=15000)。我們把排序後的輸出結果分割槽後儲存到 4000個檔案(R=4000)。分割槽函式使用 key 的原始位元組來把資料分割槽到 R 個片段中。
在這個 benchmark 測試中,我們使用的分割槽函式知道 key 的分割槽情況。通常對於排序程式來說,我們會增加一個預處理的 MapReduce 操作用於取樣 key 值的分佈情況,通過取樣的資料來計算對最終排序處理的分割槽點。

圖三(a)顯示了這個排序程式的正常執行過程。左上的圖顯示了輸入資料讀取的速度。資料讀取速度峰值會達到 13GB/s,並且所有 Map 任務完成之後,即大約 200 秒之後迅速滑落到 0。值得注意的是,排序程式輸入資料讀取速度小於分散式 grep 程式。這是因為排序程式的 Map 任務花了大約一半的處理時間和 I/O 頻寬把中間輸出結果寫到本地硬碟。相應的分散式 grep 程式的中間結果輸出幾乎可以忽略不計。
左邊中間的圖顯示了中間資料從 Map 任務傳送到 Reduce 任務的網路速度。這個過程從第一個 Map 任務完成之後就開始緩慢啟動了。圖示的第一個高峰是啟動了第一批大概 1700 個 Reduce 任務(整個 MapReduce分佈到大概 1700 臺機器上,每臺機器 1 次最多執行 1 個 Reduce 任務)。排序程式執行大約 300 秒後,第一批啟動的 Reduce 任務有些完成了,我們開始執行剩下的 Reduce 任務。所有的處理在大約 600 秒後結束。

​ 左下圖表示 Reduce 任務把排序後的資料寫到最終的輸出檔案的速度。在第一個排序階段結束和資料開始寫入磁碟之間有一個小的延時,這是因為 worker 機器正在忙於排序中間資料。磁碟寫入速度在 2-4GB/s 持續一段時間。輸出資料寫入磁碟大約持續 850 秒。計入初始啟動部分的時間,整個運算消耗了 891 秒。這個速度和 TeraSort benchmark[18]的最高紀錄 1057 秒相差不多。

​ 還有一些值得注意的現象:輸入資料的讀取速度比排序速度和輸出資料寫入磁碟速度要高不少,這是因為我們的輸入資料本地化優化策略起了作用 — 絕大部分資料都是從本地硬碟讀取的,從而節省了網路頻寬。排序速度比輸出資料寫入到磁碟的速度快,這是因為輸出資料寫了兩份(我們使用了 2 路的 GFS 檔案系統,寫入複製節點的原因是為了保證資料可靠性和可用性)。我們把輸出資料寫入到兩個複製節點的原因是因為這是底層檔案系統的保證資料可靠性和可用性的實現機制。如果底層檔案系統使用類似容錯編碼[14](erasure
coding)的方式而不是複製的方式保證資料的可靠性和可用性,那麼在輸出資料寫入磁碟的時候,就可以降低網路頻寬的使用。

5.4 高效的 backup 任務

​ 圖三(b)顯示了關閉了備用任務後排序程式執行情況。執行的過程和圖 3(a)很相似,除了輸出資料寫磁碟的動作在時間上拖了一個很長的尾巴,而且在這段時間裡,幾乎沒有什麼寫入動作。在 960 秒後,只有5 個 Reduce 任務沒有完成。這些拖後腿的任務又執行了 300 秒才完成。整個計算消耗了 1283 秒,多了 44%的執行時間。

5.5 失效的機器

​ 在圖三(c)中演示的排序程式執行的過程中,我們在程式開始後幾分鐘有意的 kill 了 1746 個 worker 中的 200 個。叢集底層的排程立刻在這些機器上重新開始新的 worker 處理程式(因為只是 worker 機器上的處理程式被 kill 了,機器本身還在工作)。
​ 圖三(c)顯示出了一個“負”的輸入資料讀取速度,這是因為一些已經完成的 Map 任務丟失了(由於相應的執行 Map 任務的 worker 程式被 kill 了),需要重新執行這些任務。相關 Map 任務很快就被重新執行了。
​ 整個運算在 933 秒內完成,包括了初始啟動時間(只比正常執行多消耗了 5%的時間)。

6 經驗

​ 我們在 2003 年 1 月完成了第一個版本的 MapReduce 庫,在 2003 年 8 月的版本有了顯著的增強,這包括了輸入資料本地優化、worker 機器之間的動態負載均衡等等。從那以後,我們驚喜的發現,MapReduce 庫能廣泛應用於我們日常工作中遇到的各類問題。它現在在 Google 內部各個領域得到廣泛應用,包括:

  1. 大規模機器學習問題

  2. Google News 和 Froogle 產品的叢集問題

  3. 從公眾查詢產品(比如 Google 的 Zeitgeist)的報告中抽取資料。

  4. 從大量的新應用和新產品的網頁中提取有用資訊(比如,從大量的位置搜尋網頁中抽取地理位置信
    息)。

  5. 大規模的圖形計算。

​ 圖四顯示了在我們的原始碼管理系統中,隨著時間推移,獨立的 MapReduce 程式數量的顯著增加。從 2003年早些時候的0個增長到2004年9月份的差不多900個不同的程式。MapReduce的成功取決於採用MapReduce
庫能夠在不到半個小時時間內寫出一個簡單的程式,這個簡單的程式能夠在上千臺機器的組成的叢集上做大規模併發處理,這極大的加快了開發和原形設計的週期。另外,採用 MapReduce 庫,可以讓完全沒有分散式和/或並行系統開發經驗的程式設計師很容易的利用大量的資源,開發出分散式和/或並行處理的應用。

在每個任務結束的時候,MapReduce 庫統計計算資源的使用狀況。在表 1,我們列出了 2004 年 8 月份
MapReduce 執行的任務所佔用的相關資源。

6.1 大規模索引

​ 到目前為止,MapReduce 最成功的應用就是重寫了 Google 網路搜尋服務所使用到的 index 系統。索引系統的輸入資料是網路爬蟲抓取回來的海量的文件,這些文件資料都儲存在 GFS 檔案系統裡。這些文件原始內容 4 的大小超過了 20TB。索引程式是通過一系列的 MapReduce 操作(大約 5 到 10 次)來建立索引。使用MapReduce(替換上一個特別設計的、分散式處理的索引程式)帶來這些好處:
實現索引部分的程式碼簡單、小巧、容易理解,因為對於容錯、分散式以及平行計算的處理都是 MapReduce庫提供的。比如,使用 MapReduce 庫,計算的程式碼行數從原來的 3800 行 C++程式碼減少到大概 700 行程式碼。
MapReduce 庫的效能已經足夠好了,因此我們可以把在概念上不相關的計算步驟分開處理,而不是混在一起以期減少資料傳遞的額外消耗。概念上不相關的計算步驟的隔離也使得我們可以很容易改變索引處理方式。比如,對之前的索引系統的一個小更改可能要耗費好幾個月的時間,但是在使用 MapReduce 的新系統上,這樣的更改只需要花幾天時間就可以了。
索引系統的操作管理更容易了。因為由機器失效、機器處理速度緩慢、以及網路的瞬間阻塞等引起的絕大部分問題都已經由 MapReduce 庫解決了,不再需要操作人員的介入了。另外,我們可以通過在索引系統叢集中增加機器的簡單方法提高整體處理效能。

7 相關工作

很多系統都提供了嚴格的程式設計模式,並且通過對程式設計的嚴格限制來實現平行計算。例如,一個結合函式可以通過把 N 個元素的陣列的字首在 N 個處理器上使用並行字首演算法,在 log N 的時間內計算完[6,9,13] 5 。

MapReduce 可以看作是我們結合在真實環境下處理海量資料的經驗,對這些經典模型進行簡化和萃取的成果。
更加值得驕傲的是,我們還實現了基於上千臺處理器的叢集的容錯處理。相比而言,大部分併發處理系統都只在小規模的叢集上實現,並且把容錯處理交給了程式設計師。

Bulk Synchronous Programming[17]和一些 MPI 原語[11]提供了更高階別的並行處理抽象,可以更容易寫出並行處理的程式。MapReduce 和這些系統的關鍵不同之處在於,MapReduce 利用限制性程式設計模式實現了使用者程式的自動併發處理,並且提供了透明的容錯處理。
我們資料本地優化策略的靈感來源於 active disks[12,15]等技術,在 active disks 中,計算任務是儘量推送到資料儲存的節點處理 6 ,這樣就減少了網路和 IO 子系統的吞吐量。我們在掛載幾個硬碟的普通機器上執行我們的運算,而不是在磁碟處理器上執行我們的工作,但是達到的目的一樣的。
我們的備用任務機制和 Charlotte System[3]提出的 eager 排程機制比較類似。Eager 排程機制的一個缺點是如果一個任務反覆失效,那麼整個計算就不能完成。我們通過忽略引起故障的記錄的方式在某種程度上解決了這個問題。
MapReduce 的實現依賴於一個內部的叢集管理系統,這個叢集管理系統負責在一個超大的、共享機器的叢集上分佈和執行使用者任務。雖然這個不是本論文的重點,但是有必要提一下,這個叢集管理系統在理念上和其它系統,如 Condor[16]是一樣。
MapReduce 庫的排序機制和 NOW-Sort[1]的操作上很類似。讀取輸入源的機器(map workers)把待排序的資料進行分割槽後,傳送到 R 個 Reduce worker 中的一個進行處理。每個 Reduce worker 在本地對資料進行排序(儘可能在記憶體中排序)。當然,NOW-Sort 沒有給使用者自定義的 Map 和 Reduce 函式的機會,因此不具備MapReduce 庫廣泛的實用性。
River[2]提供了一個程式設計模型:處理程式通過分散式佇列傳送資料的方式進行互相通訊。和 MapReduce類似,River 系統嘗試在不對等的硬體環境下,或者在系統顛簸的情況下也能提供近似平均的效能。River 是通過精心排程硬碟和網路的通訊來平衡任務的完成時間。MapReduce 庫採用了其它的方法。通過對程式設計模型進行限制,MapReduce 框架把問題分解成為大量的“小”任務。這些任務在可用的 worker 叢集上動態的排程,這樣快速的 worker 就可以執行更多的任務。通過對程式設計模型進行限制,我們可用在工作接近完成的時候排程備用任務,縮短在硬體配置不均衡的情況下縮小整個操作完成的時間(比如有的機器效能差、或者機器被某些操作阻塞了)。
BAD-FS[5]採用了和 MapReduce 完全不同的程式設計模式,它是面向廣域網的。

不過,這兩個系統有兩個基礎功能很類似。

(1)兩個系統採用重新執行的方式來防止由於失效導致的資料丟失。

(2)兩個都使用資料本地化排程策略,減少網路通訊的資料量。
TACC[7]是一個用於簡化構造高可用性網路服務的系統。和 MapReduce 一樣,它也依靠重新執行機制來實現的容錯處理。

8 結束語

MapReduce 程式設計模型在 Google 內部成功應用於多個領域。我們把這種成功歸結為幾個方面:首先,由於MapReduce 封裝了並行處理、容錯處理、資料本地化優化、負載均衡等等技術難點的細節,這使得 MapReduce庫易於使用。即便對於完全沒有並行或者分散式系統開發經驗的程式設計師而言;

其次,大量不同型別的問題都可以通過 MapReduce 簡單的解決。比如,MapReduce 用於生成 Google 的網路搜尋服務所需要的資料、用來
排序、用來資料探勘、用於機器學習,以及很多其它的系統;第三,我們實現了一個在數千臺計算機組成的大型叢集上靈活部署執行的 MapReduce。這個實現使得有效利用這些豐富的計算資源變得非常簡單,因此也適合用來解決 Google 遇到的其他很多需要大量計算的問題。
我們也從 MapReduce 開發過程中學到了不少東西。首先,約束程式設計模式使得並行和分散式計算非常容易,也易於構造容錯的計算環境;其次,網路頻寬是稀有資源。大量的系統優化是針對減少網路傳輸量為目的的:
本地優化策略使大量的資料從本地磁碟讀取,中間檔案寫入本地磁碟、並且只寫一份中間檔案也節約了網路頻寬;

第三,多次執行相同的任務可以減少效能緩慢的機器帶來的負面影響同時解決了由於機器失效導致的資料丟失問題。

更多Flink,Kafka,Spark等相關技術博文,科技資訊,歡迎關注實時流式計算 公眾號後臺回覆 “電子書” 下載300頁Flink實戰電子書

相關文章