MapReduce:大型集羣上的簡單數據處理

MapReduce:大型集羣上的簡單數據處理

摘要

MapReduce是一個編程模型和一個處理和生成大數據集的相關實現。用戶指定一個map函數處理一個key-value對來生成一組中間key-value對;指定一個reduce函數合併所有和同一中間key值相聯繫的中間value值。許多現實世界中的任務以這個模型展現,就像文中展示的那樣。

以這種函數類型編寫的程序在一羣日常機器上自動並行化並執行。運行時系統關心劃分輸入數據的細節,在一組機器間調度程序的執行,處理機器失效,管理內部機器需要的通信。這使得那些沒有任何並行和分佈式系統經驗的程序員可以容易地使用大型分佈式系統的資源。

我們MapReduce的實現運行在一大羣日常機器上並且高度可擴張:一個典型的MapReduce計算在成千上萬臺機器上處理數TB的數據。程序員發現系統很易用:成百上千的MapReduce程序已經被實現,每天都有多餘1000MapReduce作業在Google集羣上執行。

1.介紹

在過去的5年裏,作者以及Google裏的其他程序員已經實現了數以百計的,特殊目的的計算。這些計算處理海量原始數據,比如,文檔抓取(shijin:類似網絡爬蟲的程序)、web請求日誌等;或者計算各種各樣的派生數據,比如倒排索引、web文檔的圖結構的各種表示形勢、每臺主機上網絡爬蟲抓取的頁面數量的彙總、列舉一天中一組最頻繁的查詢等。大多數這種計算概念上直截了當。然而輸入的數據量巨大,並且爲了在合理的時間內完成,計算不得不分佈到數百或數千臺機器上。如何並行計算、分發數據、處理失效的問題湊在一起使原本簡單的計算晦澀難懂,需要大量複雜的代碼來處理這些問題。

作爲對上述複雜性的應對,我們設計一個新的抽象模型,其使我們可以表達我們試圖執行的簡單運算,但是將並行、容錯、數據分佈和負載均衡等散亂的細節隱藏在了一個庫裏面。我們抽象模型的靈感來自Lisp和許多其他函數式語言的mapreduce原語。我們意識到大多數我們的計算都涉及這樣的操作:在我們輸入中的每個邏輯“記錄”上應用map操作,以便計算出一組中間key/value對,然後在所有享有相同key值的value值應用reduce操作,以便合適地合併派生的數據。我們使用帶有用戶指定mapreduce操作的函數模型,就可以輕易地並行化大規模計算;並且可以使用“再次執行”(re-execution)作爲基礎的容錯機制。

這項工作的主要貢獻是一個簡單強大的接口,該接口使自動地並行化和分佈大規模計算成爲了可能,接口連同該接口的實現,實現了在大羣日常PC機上的高性能。

第二節描述基本的編程模型給出一些例子。第三節描述了爲我們基於集羣的計算環境定做的MapReduce接口的實現。第四節描述一些我們發現有用的編程模型優化。第五節對我們各種不同任務實現的性能進行了測量。第六節探索了MapReduceGoogle內部的使用,包含我們在使用其作爲我們生產索引系統的重寫操作的基礎時的經驗。第七節討論相關的和未來的工作。

2.編程模型

計算取出一組輸入key/value對,產生一組輸出key/value對。使用MapReduce庫的用戶用兩個函數表達這個計算:MapReduce

用戶自己編寫的Map接受一個輸入對,然後產生一組中間key/value對。MapReduce庫把所有和相同中間keyI關聯的中間value值聚集在一起後傳遞給Reduce函數。

也是由用戶編寫的Reduce函數接受一箇中間keyI和一組那個key值的value值。Reduce函數將這些value值合併在一起,形成一個可能更小的value值的集合。通常每次Reduce調用僅產生01個輸出value值。中間value值通過一個迭代器提供給用戶的Reduce函數,這樣我們就可以處理因太大而無法適應內存的value值列表。

2.1 例子

考慮這麼一個問題,在一個大的文檔集合中對每個單詞出現的次數進行計數,用戶可能要類似下面僞代碼的代碼:

map(String key, String value):

    // key: document name

    // value: documen t 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 value s:

        result += Parse Int(v);

    Emit(AsString(result));

Map函數emit每個詞加上一個相關的出現計數(在這個簡單的例子裏就是1)Reduce函數把爲一個特定的詞emit的所有計數加起來。

另外,用戶編寫代碼用輸入和輸出文件的名字以及可選的調節參數填充一個mapreduce說明對象,用戶之後調用MapReduce函數,並把這個說明對象傳遞給它。用戶的代碼和MapReduce庫鏈接在一起(C++實現)。附錄A包含了這個例子的全部程序文本。

2.2 類型

儘管在前面的僞代碼按照字符串輸入輸出書寫,但是在概念上,用戶提供的mapreduce函數有相關的類型:

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

reduce   (k2,list(v2))   ->list(v2)

比如,輸入的key值和value值與輸出的key值和value值從不同的類型域得到。並且,中間key值和value值與輸出key值和value值來自同一個類型域。(alex注:原文中這個domain的含義不是很清楚,我參考HadoopKFS等實現,mapreduce都使用了泛型,因此,我把domain翻譯成類型域)。

我們的C++實現使用字符串類型作爲用戶自定義函數的輸入輸出,並將字符串與適當類型的轉換工作交給了客戶代碼。

2.3 更多的例子

這裏有一些有趣程序的簡單例子,可以很容易地作爲MapReduce計算來表示:

分佈式的Grep:如果與提供的模式串匹配,Map函數emit一行,Reduce函數是一個恆等函數,即僅僅把提供的中間數據複製到輸出。

URL訪問頻率計數:Map函數處理web頁面請求的日誌,然後輸出(URL,1)Reduce函數把相同URLvalue值加在一起,emit一個(URL, 總數)對。

倒轉網絡鏈接圖:Map函數爲每個鏈接輸出(target,source)對,每個鏈接連接到在名字叫源的頁面中發現的目標URLReduce函數把與給定目標URL相關的所有源URL連接成列表,emit(target,list(source))對。

每個主機的檢索詞向量:檢索詞向量將一個文檔或者一組文檔中出現的嘴重要的詞彙總爲一個(詞,頻)對列表。Map函數爲每個輸入文檔emit(主機名, 檢索詞向量),其中主機名來自文檔的URLReduce函數爲一個給定主機接收所有每文檔檢索詞向量,其將這些檢索詞向量加在一起,丟棄低頻的檢索詞,然後emit一個最終的(主機名, 檢索詞向量)對。

倒排索引:Map函數解析每個文檔,emit一系列(, 文檔號)列表,Reduce函數接受一個給定詞的所有(, 文檔號)對,排序相關的文檔號,emit(,list(文檔號))。所有的輸出對集合形成一個簡單的倒排索引,增加計算來跟蹤詞在文檔中的位置很簡單。

分佈式排序:Map函數從每個記錄提取key值,emit(key,record)對。Reduce 函數原封不動地emit所有對。這個運算依賴4.1描述的分區設備和4.2節描述的排序屬性。

3.實現

MapReduce有多種不同的可能實現。正確的選擇取決於環境。例如,一種實現可能適用於小型共享內存的機器,另外一種則適用於大型NUMA多處理器,然而還有的適合大型的網絡集羣。

本章節描述一個針對Google內部廣泛使用的運算環境的實現:大羣日常機器用交換以太網連接在一起。在我們環境中:

1. 機器通常是x86雙核處理器、運行Linux系統、每臺機器2-4GB內存。

2. 使用日常網絡硬件,通常在機器級別帶寬爲百兆每分或者千兆每分,但是平均遠小於網絡整體帶寬的一半。(averaging considerably less in overall bisection bandwidth

3. 集羣包含數百或數千臺機器,因此機器失效是常態。

4. 存儲由直接附屬在個體機器上的廉價IDE硬盤提供。一個內部開發的分佈式文件系統用來管理存儲在這些磁盤上的數據。文件系統使用副本來提供不可靠的硬件上的可用性和可靠性。

5. 用戶向一個調度系統提交作業。每個作業包含一組任務,調度系統將這些任務映射到一組集羣內部可用的機器上。

3.1 執行概覽

Map調用通過將輸入數據自動分爲M個片段(a set of M splits)的方式被分佈到多臺機器上。輸入片段能夠被不同的機器並行處理。Reduce調用使用分區函數將中間key值空間分成R份(例如,hash(key) mod R),繼而被分佈到多臺機器上執行。分區數量(R)和分區函數由用戶指定。

1展示了我們的實現中MapReduce操作的整體流程。當用戶調用MapReduce函數時,下列動作發生(圖一中的數字標籤對應下面列表中的序號):

1. 用戶程序中的MapReduce庫首先將輸入文件分成M份,每份通常在16MB64MB之間(可以通過可選參數由用戶控制)。然後在機器集羣中啓動許多程序副本。

2. 程序副本中有一個是特殊-master。其它的是worker,由master分配工作。有Mmap任務和Rreduce任務將被分配,master選擇空閒的worker然後爲每一個worker分配map任務或reduce任務。

3. 被分配了map任務的worker讀取相關輸入片段的內容,它從輸入的數據片段中解析出key/value對,然後把key/value對傳遞給用戶自定義的Map函數,由Map函數生成的中間key/value對緩存在內存中。

4. 緩存對被定期地寫入本地磁盤,被分區函數分成R個域。緩存對在本地磁盤上的位置被傳回mastermaster負責將這些位置轉寄給reduce worker

5. 當一個reduce workermaster告知位置信息後,它使用遠程過程調用從map worker的本地磁盤讀取緩存數據。當一個reduce worker讀取了所有的中間數據後,它通過中間key值對緩衝數據排序,以便相同key值的出現組織在一起。由於通常許多不同的key值映射到同一reduce任務上,因此排序是需要的。如果中間數據量太大而無法適應內存,那麼就使用外部排序。

6.Reduce worker迭代排序後的中間數據,對於每一個遇到的唯一的中間key 值,Reduce worker將這個key值和與它相關的中間value值的集合傳遞給用戶的Reduce函數。Reduce函數的輸出被追加到這個reduce分區的一個最終輸出文件。

7. 當所有的mapreduce任務完成之後,master喚醒用戶程序。此時此刻,用戶程序裏的對MapReduce調用返回用戶代碼。

成功完成之後,mapreduce執行的輸出可以在R個輸出文件中得到(每個文件對應一個reduce任務,文件名由用戶指定)。通常,用戶不需要將這R個輸出文件合併成一個文件-他們經常把這些文件作爲輸入傳遞給另外一個MapReduce調用,或者在另外一個分佈式應用中使用它們,這種分佈式應用能夠處理分成多個文件的輸入。

3.2 Master數據結構

Master保持一些數據結構,對每一個mapreduce任務,它保存其狀態(空閒、進行中或已完成),以及Worker機器(對於非空閒任務)的身份(identity)。

Master是一個管道,通過它中間文件域的位置信息從map任務傳播到reduce任務。因此,對於每個已經完成的map任務,master存儲了map任務產生的R箇中間文件域的位置和大小。當map任務完成時,接收到了位置和大小信息的更新,這些信息被遞進地推送給那些正在運行的reduce任務。

3.3 容錯

因爲MapReduce庫是設計用來協助使用數百數千的機器處理超大規模數據的,這個庫必須優雅地處理機器故障。

worker故障

master週期性地ping每個worker。如果在一個確定的時間段內沒有收到worke的迴應,master將這個worker標記爲失效。任何由這個worker完成的map任務被重置回它們初始的空閒狀態,因此變得可以被調度到其它worker。同樣,在一個失效的worker上正在運行的mapreduce任務被重置爲空閒狀態,變得可被重新調度。

故障時已完成的map任務必須重新執行是因爲它們的輸出被存儲在失效機器的本地磁盤上,因此不可訪問了。已經完成的reduce任務不需要再次執行,因爲它們的輸出存儲在全局文件系統。

當一個map任務首先被worker A執行,之後被worker B執行(因爲A失效),所有執行reduce任務的worker會接到重新執行的通知。還沒有從worker A讀取數據的任何reduce任務將從worker B讀取數據。

MapReduce對規模worker失效很有彈性。例如,在一次MapReduce操作執行期間,在正在運行的集羣上進行的網絡維護一次造成一組80臺機器在幾分鐘內無法訪問,MapReduce master只需簡單德再次執行那些不可訪問的worker完成的工作,然後繼續執行,直終完成這個MapReduce操作。

master失敗

master定期對上面描述的master數據結構作檢查點很簡單。如果這個master任務失效了,一個新的備份可以從上一個檢查點狀態啓動。然而,考慮到只有一個單獨mastermaster失效是不太可能的,因此我們現在的實現是如果master失效,就中止MapReduce運算。客戶可以檢查這個情況,並且如果需要可以重試MapReduce操作。

在失效面前的語義

semantics in the presence of failures

當用戶提供的mapreduce操作是輸入值的確定性函數,我們的分佈式實現產生相同的輸出,就像沒有錯誤、順序執行整個程序產生的一樣。

我們依賴對mapreduce任務的輸出是原子提交來完成這個特性。每個工作中的任務把它的輸出寫到私有的臨時文件中。一個reduce任務生成一個這樣的文件,並且一個map任務生成R個這樣的文件(一個reduce任務對應一個)。當一個map任務完成時,workermaster發送一個消息,消息中包含R個臨時文件名。如果master收到一個已經完成的map任務的完成消息,它將忽略這個消息;否則,master將這R個文件的名字記錄在一個master數據結構裏。

當一個reduce任務完成時,reduce worker原子性地將臨時文件重命名爲最終的輸出文件。如果同一個reduce任務在多臺機器上執行,針對同一個最終輸出文件將有多個重命名調用執行。我們依賴底層文件系統提供的原子重命名操作來保證最終的文件系統狀態僅僅包含reduce任務一次執行產生的數據。

絕大多數map和人reduce操作是確定的,而且存在這樣的一個事實:我們的語義等同於順序的執行,在這種情況下,程序員可以很容易推斷他們程序的行爲。當map/reduce操作是不確定性的時候,我們提供較弱但是依然合理的語義。在非確定性操作面前,一個特定reduce任務R1的輸出等價於一個非確定性程序順序執行產生的R1的輸出。然而,一個不同reduce任務R2的輸出可能相當於一個不同的非確定行程序順序執行產生的R2的輸出。

考慮map任務Mreduce任務R1R2。設e(Ri)Ri提交的執行過程(只有一個這樣的執行過程)。由於e(R1)可能讀取了由M一次執行產生的輸出,而e(R2)可能讀取了由M的不同執行產生的輸出,較弱的語義隨之而來。

3.4 存儲位置

在我們的計算環境中,網絡帶寬是一個相當稀缺的資源。我們通過充分利用輸入數據(GFS管理)存儲組成集羣的機器的本地磁盤上這樣一個事實來節省網絡帶寬。GFS 把每個文件分成64MB的塊,並且在不同機器上存儲每個塊的一些拷貝(通常是3個拷貝)。考慮到輸入文件的位置信息,MapReducemaster試圖將一個map任務調度到包含相關輸入數據拷貝的機器上;嘗試失敗的話,它將嘗試調度map任務到靠近任務輸入數據副本的機器上(例如,一個包含數據並在同一網關上的worker機器)。當在一個集羣的大部分worker上運行大型MapReduce操作的時候,大部分輸入數據從本地機器讀取,並且不消耗帶寬。

3.5 任務粒度

如上所述,我們把map階段細分成M個片段、把reduce 階段細分成R個片段。理想情況下,MR應當比worker機器的數量要多得多。在每臺worker上執行許多不同的任務提高動態負載平衡,並且在worker故障時加速恢復:失效機器上完成的很多map任務可以分佈到所有其他的worker機器。

但是在我們的實現中對MR的取值有實際的限制,因爲master必須制定O(M+R)調度策略,並且如上所述在內存中保存O(M*R)個狀態(然而內存使用的常數因子還是比較小的:O(M*R)片狀態大約包含每對map任務/reduce任務對1個字節)。

此外,R值通常是被用戶制約的,因爲每個reduce任務的輸出最終在一個獨立的輸出文件中。實際上,我們傾向於選擇M值以使每個獨立任務有大約16M64M的輸入數據(這樣如上所述的本地最優化最有效),我們把R值設置爲我們想使用的worker機器數量的一個小的倍數。我們通常用,使用2000worker機器,以M=200000R=5000來執行MapReduce計算。

3.6 備用任務

延長一個MapReduce操作花費的總時間的常見因素之一是“落伍者”:一臺機器花費了不同尋常的長時間才完成計算中最後幾個mapreduce任務之一,出現“落伍者”的原因非常多。比如:一個有壞磁盤的機器可能經歷頻繁的糾錯以致將其讀性能從30M/s降低到1M/s。集羣調度系統可能已經降其他任務調度到這臺機器上,由於CPU、內存、本地磁盤和網絡帶寬的競爭導致執行MapReduce代碼更慢。我們最近遇到的一個問題是機器初始化代碼中的bug,導致處理器緩存失效:受影響機器上的計算減慢了超過百倍。

我們有一個通用的機制來減輕“落伍者”的問題。當一個MapReduce操作接近完成的時候,master調度剩餘正在運行任務的備份執行、無論主執行還是備份執行完成,任務被標記爲已完成。我們調優了這個機制,以便其通常增加操作不多於幾個百分點的計算資源。作爲示例,5.3節描述的排序程序在關掉備用任務機制時要多花44%的時間完成。

4.精細化

儘管簡單地書寫MapReduce函數提供的基本功能能夠滿足大多數需求,我們還是發現了一些有用的擴展。在本節做了描述。

4.1 分區函數

MapReduce的使用者指定他們需要的reduce任務/輸出文件的數量(R)。我們在中間key值上使用分區函數將數據在這些任務間分開。一個默認的分區函數是使用哈希(比如,hash(key) mod R)提供的。它傾向於導致相當均衡的分區。然而,在某些情況下,通過key值的其它函數分割數據是有用的。比如,輸出的key值是URLs,我們希望單個主機的所有條目以結束在同一個輸出文件中。爲了支持類似的情況,MapReduce庫的用戶可以提供一個專門的分區函數。例如,使用“hash(Hostname(urlkey)) mod R”作爲分區函數就可以把所有來自同一個主機的URL結束在同一個輸出文件中。

4.2 順序保證

我們保證在給定的分區中,中間key/value對是以key值遞增的順序處理的。這個順序保證每個分成生成一個有序的輸出文件很容易,這在下列情況時很有用:輸出文件的格式需要支持按key值高效地隨機訪問查找,或者輸出的用戶有序的數據很方便。

4.3 合併函數

某些情況下,每個map任務產生的中間key值有顯著的重複,並且用戶指定的Reduce函數滿足結合律和交換律。這個情況很好的例子是2.1節中的詞數統計示例。由於詞頻傾向於滿足zipf分佈,每個map任務將產生數百數千形如<the,1>的記錄。所有這些記錄將通過網絡被髮送到一個單獨的reduce任務,然後被Reduce函數累加起來產生一個數。我們允許用戶指定一個可選的合併函數,其在通過網絡發送數據之前對數據進行部分合並。

合併函數在每臺執行map任務的機器上執行。通常使用相同的代碼實現合併函數和reduce函數。合併函數和reduce函數唯一的區別就是MapReduce庫如何處理函數的輸出。Reduce函數的輸出被寫入在最終的輸出文件,合併函數的輸出被寫到中間文件裏,該文件被髮送給reduce任務。

部分合並顯著加速了MapReduce操作中的某些類。附錄A包含一個使用合併函數的例子。

4.4 輸入輸出類型

MapReduce庫支持讀取一些不同的格式的輸入數據。比如,文本模式輸入將每一行視爲一個key/value對。key是文件中的偏移,value是那一行的內容。另外一種通常支持的格式以key值排序存儲了一系列key/value對。每種輸入類型的實現都懂得如何把自己分割成有意義的範圍,以便作爲獨立的map處理(例如,文本模式的範圍分割確保分割只在行邊界發生)。雖然大多數用戶僅僅使用少量預定義輸入類型之一,但是用戶可以通過提供一個簡單的Reader接口的實現支持一個新的輸入類型。

需要提供數據的reader不必從文件中讀取,比如,我們可以容易地定義一個從數據庫裏讀記錄的reader,或者從映射在內存中的數據結構讀。

類似的,我們爲生產不同格式的數據提供一組輸出類型,用戶代碼可以容易地爲新的輸出類型添加支持。

4.5 副作用

某些情況下,MapReduce的使用者發現從map/reduce操作中產生作爲附加輸出的輔助文件比較方便。我們依賴程序writer把這種“副作用”變成原子的和冪等的(alex注:冪等的指一個總是產生相同結果的數學運算)。通常應用程序首先寫到一個臨時文件,在全部生成之後,原子地將這個文件重新命名。

我們不爲單個任務產生的多個輸出文件的原子兩步提交提供支持。因此,產生多個輸出文件、並且具有跨文件一致性要求的任務,必須是確定性的。實際這個限制還沒有成爲問題。

4.6 跳過受損記錄

有時候,用戶代碼中的bug導致map或者reduce函數在某些記錄上確定性地崩潰。這樣的bug阻止MapReduce操作完成。應對的通常過程是修復bug,但是有時不可行;可能這個bug在第三方庫裏,其源碼是得不到的。而且有時忽略一些記錄是可以接受的,比如在一個大數據集上進行統計分析。我們提供了一種可選的執行模式,這種模式下,爲了保證繼續進行,MapReduce庫探測哪些記錄導致確定性的崩潰,並且跳過這些記錄。

每個worker進程都安裝了一個捕獲段例外和總線錯誤的信號句柄。在調用用戶的MapReduce操作之前,MapReduce庫在一個全局變量中保存了參數的序號。如果用戶代碼產生了一個信號,信號句柄向MapReducemaster發送包含序列號的“奄奄一息”的UDP包。當master在特定記錄上看到多餘一次失敗時,當master發佈相關Map或者Reduce任務的下次重新執行時,它就指出這條記錄應該跳過。

4.7 本地執行

調試MapReduce函數的是非常棘手的,因爲實際的執行發生在分佈式系統中,通常是在數千臺機器上,由master動態地制定工作分配策略。爲了促進調試、性能剖析和小規模測試,我們開發了一套可選的MapReduce庫的實現, MapReduce操作在本地計算機上順序地執行所有工作。爲用戶提供了控制以便把計算限制到特定的map任務上。用戶通過特殊的標誌來調用他們的程序,然後可以容易地使用他們覺得有用的調試和測試工具(比如gdb)。

4.8 狀態信息

Master運行着內部的HTTP服務器並且輸出一組供人消費的狀態頁面。狀態頁面顯示包括計算的進展,比如已經完成了多少任務、有多少任務正在進行、輸入的字節數、中間數據的字節數、輸出的字節數、進行的百分比等等。頁面還包含指向每個任務產生的stderrstdout文件的鏈接。用戶可以使用這些數據預測計算需要執行多長時間、是否需要增加向計算增加更多的資源。這些頁面也可以用來搞清楚什麼時候計算比預期的要慢。

另外,最頂層的狀態頁面顯示了哪些worker失效了,以及他們失效的時候正在運行的mapreduce任務是哪些。這些信息在嘗試診斷用戶代碼中bug的時候很有用。

4.9 計數器

MapReduce庫提供了一個計數器促進統計各種事件發生次數。比如,用戶代碼可能想統計處理的總詞數、或者已經索引的德文文檔數等等。

爲了使用這個附加功能,用戶代碼創建一個名叫計數器的對象,然後在在MapReduce函數中適當地增加計數。例如:

Counter* uppercase;

uppercase = GetCounter(“uppercase”);

map(String name, String contents):

       for each word w in c ontents:

              if (IsCapitalized(w)):

                     uppercase->Increment();

       EmitIntermediate(w , 1);

這些來自單個worker機器的計數值週期性傳播到master(附加在ping的應答包中)。master把來自成功執行的mapreduce任務的計數器值進行累加,當MapReduce操作完成後,返回給用戶代碼。當前的計數器值也會顯示在master的狀態頁面上,這樣人們可以觀看當前計算的進度。當累加計數器值的時候,master排除同一map或者reduce任務重複執行的影響,避免重複計數(重複執行額可以起因於我們使用備用任務以及失效後任務的重新執行)。

有些計數器的值由MapReduce庫自動維護,比如已經處理的輸入key/value對的數量、已經產生的輸出key/value對的數量。

用戶發現計數器附加功能對MapReduce操作行爲的完整性檢查有用。比如,在一些MapReduce操作中,用戶代碼可能需要確保產生的輸出對的數量精確的等於處理的輸入對的數量,或者處理的German文檔的比例在處理文檔的整體數量中在可以容忍的比例。

5.性能

本節我們用在一大羣機器上運行的兩個計算來測量MapReduce的性能。一個計算搜尋大約1TB的數據查找特定的模式匹配,另一個計算排序大約1TB的數據。

這兩個程序是由MapReduce用戶書寫的實際程序子集的代表-一類程序是將從一種表現形式打亂爲另外一種表現形式;另一類是從大數據集中抽取少量用戶感興趣的數據。

5.1 集羣配置

所有程序都運行在一個大約由1800臺機器組成的集羣上。每臺機器有兩個2G主頻、支持超線程的Intel Xeon處理器,4GB的內存,兩個160GBIDE硬盤和一個千兆以太網。這些機器被安排在一個兩層樹形交換網絡中,在root節點大約支持100-200GBPS的合計帶寬。所有機器使用相同主機設備,因此任意對之間的往返時間小於1毫秒。

4GB的內存中,大約1-1.5G被運行在集羣上的其他任務預訂。程序在週末下午執行,這時CPU、磁盤和網絡大多數空閒。

5.2 查找

這個grep程序從頭到尾掃描1010100字節的記錄,查找相當稀少的3個字符的模式(這個模式出現在92337個記錄中)。輸入數據被分割成大約64M的塊(M=15000shijin1010*100/(64*1024*1024)),整個輸出被放在一個文件中(R=1)。

2 顯示了運算隨時間的進展。Y軸表示輸入數據的掃描速度。這個速度隨着更多的機器被分配到MapReduce計算中而增加,當1764worker被分配,速度達到超過30GB/s的峯值。當Map任務結束時,速度開始降低並在計算到80秒時達到0。整個計算從開始到結束大約花了150秒。這包括大約一分鐘的啓動開銷。開銷起因於程序傳播到所有worker機器、與GFS交互打開1000個輸入文件集合的延遲、獲取文件優化所需信息的時間。

5.3 排序

排序程序對1010100字節的記錄(大約1TB的數據)排序。這個程序模仿TeraSort基準測試程序[10]

排序程序由不到50行代碼組成。一個三行Map函數從一個文本行中提取出10字節的排序key值,並且將key值和原始文本行作爲中間key/valueemit。我們使用了一個內置的恆等函數作爲Reduce操作。這個函數將中間key/value對作爲輸出輸出key/value對不加修改地傳送。最終已排序的輸出寫入到一組雙備份(2-way replicated)GFS文件(也就是說,作爲程序輸出,2TB 的數據被寫入)。

像以前一樣,輸入數據被分割成64MB的片(M=15000)。我們把已排序的輸分到4000個文件(R=4000)。分區函數使用key值的原始字節將其分離到R個片段其中之一。

我們這個基準測試的分區函數具有key值分佈的內置knowledge。在一個普通排序程序中,我們會增加一個預處理的MapReduce操作,用於key值的樣本並使用key值樣本的分佈計算最終排序過程的分割點。

圖三(a)顯示了這個排序程序的正常執行過程。左上的圖顯示了輸入數據的讀取速度。速度達到大約13GB/s的峯值,並且自從所有map任務完成後,在200秒消逝前速度下降的相當快。值得注意的是,輸入速度小於grep。這是因爲排序map任務花了大約一半的時間和I/O帶寬把中間輸出寫到本地硬盤。Grep的相應中間輸出大小可以忽略不計。

左邊中間的圖顯示了數據通過網絡從map任務發送到reduce任務的速度。這個重排從第一個ma任務完成就開始了。圖中的第一個峯值是第一批大約1700reduce任務(整個MapReduce大約分配了1700臺機器,每臺機器一次至多執行1reduce任務)。計算進行到大約300秒後,第一批reduce任務中的其中一些結束,我們開始爲剩餘的reduce任務重排數據。所有的重排大約在計算進行到600秒時結束。

左下圖顯示已排序的數據由reduce任務寫入最終輸出文件的速度。在第一個重排階段結束和寫階段開始之間有一個延遲,這是因爲機器正忙於排序中間數據。寫操作以2-4GB/s的速度持續了一段時間。所有的寫操作在計算進行到850 秒時結束。計入啓動的開銷,整個運花費了891秒。這和當前已報道的最好結果類似,該記錄是TeraSort基準測試[18]1057秒。

一些要注意的事情:輸入速度高於重排速度高於輸出速度是因爲我們的本地化優化策略-大部分數據從本地硬盤讀取,繞考了我們的相關帶寬約束網絡。重排速度比輸出速度高是因爲輸出過程寫了兩份已排序數據(我們基於可靠性和可用性的原因生成了輸出的兩個副本)。我們寫了兩個副本是因爲這是底層文件系統爲可靠性和可用性提供的機制。如果底層文件系統使用糾刪碼編碼[14]而不是備份的方式,寫數據需要的網絡帶寬將會減少。

5.4 備用任務的影響

圖三(b)顯示了關閉備用任務的一個排序程序的執行情況。執行的流程和圖3a)顯示的類似,除了在沒有任何寫活動出現的地方有一個很長的尾巴。960秒後,只有5reduce任務沒有完成。然而這些最後的少量落伍者300秒之後才結束。整個計算花費了1283秒,在消逝的時間中增加了44%(?)。

5.5 機器故障

在圖三(c)顯示這樣一個排序程序的執行,其中我們在計算進行幾分鐘後故意殺掉了1746worker進程中的200個。底層集羣調度器立刻在這些機器上重啓新的worker進程(因爲只是進程被殺死了,機器仍在正確運行)。

Worker進程死亡顯示爲“負”的輸入速度,因爲之前一些完成的map工作消失了(由於相應的map worker進程被殺掉了)並且需要重做。這個map工作的重新執行相當快。整個計算,包含啓動開銷,在933秒內完成(只比正常執行時間增加了5%)。

6.經驗

我們在20031月寫了MapReduce庫的第一個版本,然後在20038月做了顯著增強,包括本地優化、任務執行在worker機器之間的動態負載均衡等等。從那以後,我們驚喜於MapReduce庫能如此廣泛地應用於於我們從事工作中的各種問題。它已經用於Google內部很廣的區域範圍,包括:

·大規模機器學習問題問題,

·Google NewsFroogle產品的集羣問題

·用於生產受歡迎查詢(比如Google Zeitgeist)的報告的數據提取。

·爲新的實驗或者產品提取網頁屬性(例如,爲本地搜索從大型網頁庫中抽取地理位置信息)。

·大規模的圖形計算。

圖四顯示了隨着時間推移,我們的基礎源碼管理系統中經驗證的獨立的MapReduce程序數量的顯著增加。從2003年早期的0個增長截止20049月底的幾乎900獨立的實例。MapReduce如此成功是因爲,使用MapReduce庫能夠在半個小時過程內寫出一個能夠在上千臺機器上高效運行的簡單程序,這極大地加快了開發和原形設計的週期。另外,它允許沒有分佈式和/或並行系統經驗的程序員很容易地開發大量資源。

在每個作業結束的時候,MapReduce庫對作業使用的計算資源的相關統計數據進行記錄。在表1中,我們列出了20048月份運行在Google上的MapReduce任務的子集的統計數據。

6.1 大規模索引

目前爲止我們對MapReduce最重要的使用之一就是重寫了產生Google網絡搜索服務所使用的數據結構的生產索引系統。索引系統將通過爬蟲系統檢索的海量文檔作爲輸入,這些文檔存儲爲一組GFS文件。這些文檔的原始內容的大小超過了20TB。索引程序作爲一系列五到十個MapReduce操作運行。使用MapReduce(而不是索引系統的優先版本中自組分佈式傳送)提供了一些好處:

·索引代碼更簡單、小巧、容易理解,因爲處理容錯、分佈式和並行的代碼隱藏在MapReduce庫內部。比如,當使用MapReduce表達時,一個計算過程的大小從大約3800C++代碼減少到大約700行代碼。

·MapReduce庫的性能足夠好,因此我們可以把概念上不相關的計算分開,而不是混在一起以期避免數據傳送。這使修改索引程序相當簡單。比如,在舊的索引系統耗費幾個月的一次改變在新系統中只需幾天實現。

·索引程序變得更易操作。因爲由機器失效、慢速機器以及網絡臨時阻塞引起的大部分問題自動地由MapReduce庫解決,不需要操作人員干預。另外,通過在索引集羣中增加機器可以容易地提高索引程序的性能。

7.相關工作

許多系統提供了嚴格的編程模型,並且使用這些限制來自動地並行化計算。例如,一個associative函數可以在logN的時間內在N個處理器上使用並行前綴計算[6,9,13]來計算N個元素的數組的所有前綴。MapReduce可以看作是基於我們在真實世界大型計算的經驗,對其中一些模型的簡化和淨化。更重要的是,我們提供了可擴展爲上千臺處理器的容錯實現。相比而言,大部並行處理系統只是在小規模上實現,並且將處理機器故障的細節留給了程序員。

大型同步編程[17]和一些MPI原語[11]提供了更高級別的抽象,可以使程序員容易地編寫並行程序。MapReduce和這些系統的關鍵區別在於,MapReduce開發了一個受限的編程模型自動地並行化用戶程序,並提供透明的容錯處理。

我們本地優化的靈感來源於活躍硬盤[12,15],其中計算被推送到靠近本地磁盤的計算元素,繼而減少了通過網絡或IO子系統傳輸的數據量。我們運行在直接連接少量硬盤的日常機器上,而不是直接運行在在磁盤處理器上,但是大體的方法是類似的。

我們的備用任務機制類似於Charlotte System[3]應用的迫切調度機制。簡單迫切調度機制的其中一個缺點是如果一個給定任務反覆失效,整個計算無法完成。我們通過跳過壞記錄的方式在我們機制中修正了這個問題的一些實例。

MapReduce的實現依賴於一個內部的集羣管理系統,該系統負責在一大羣共享機器上分佈和運行用戶任務。儘管不是本文的側重點,這個集羣管理系統在理念上和其它系統是一樣的,如Condor[16]

MapReduce庫的一部分-排序附加功能和類似於對NOW-Sort[1]的操作。源機器(map workers)把待排序的數據分區後,發送到Rreduce worker中的一個。每個reduce worker在本地對數據進行排序(儘可能在內存中)。當然,NOW-Sort沒有用戶自定義的MapReduce函數,這倆函數使我們的庫具有廣泛的適用性。

River[2]提供了一個編程模型,其中程序通過分佈式隊列發送數據進行互相通信。和MapReduce類似,River系統試圖即使在由異構硬件和系統擾動引入的不均勻面前也提供良好的平均情況性能。River通過精心調度硬盤和網絡傳輸來均衡完成時間以達到這個目的。通過限制編程模型,MapReduce框架能夠把問題分成大量精細任務。這些任務在可用的worker上動態調度,因此速度快的worker執行更多的任務。受限的編程模型也使我們可以調度接近作業末尾的任務冗餘執行,減少在不均勻面前的完成時間(比如有較慢或者受阻的worker)。

BAD-FS[5]有一個和MapReduce不同的編程模型,不像MapReduce那樣,它是針對廣域網上的作業執行。然而,有兩個基本相似點。(1)兩個系統都使用冗餘執行的方式從失效導致的數據丟失中恢復。(2)兩個都使用本地化調度減少通過擁堵網絡連接發送的數據量。

TACC[7] 是一個設計用來高可用性網絡服務的構造的系統。與MapReduce類似,它也依賴冗餘執行作爲實現容錯的機制。

8.總結

MapReduce編程模型在Google內部成功用於多個目的。我們把這種成功歸因爲幾個方面:首先,由於它隱藏了並行、容錯、本地優化、負載均衡的細節,即便對於沒有並行和分佈式系統經驗的程序員而言,模型也易於使用;其次,大量不同種類的問題都可以作爲MapReduce計算簡單的表達。比如,MapReduce用於生成Google的網絡搜索服務產品所需要的數據、用來排序的數據、用來數據挖掘的數據、用於機器學習的數據,以及許多其它系統;第三,我們開發了一個擴展到由數千臺機器組成的大型集羣上的MapReduce實現。這個實現使得使用這些機器資源很有效,因此也適合用在Google內部遇到的許多大型計算問題中。

我們也從這項工作中學到了一些事。首先,限制這個編程模型使得並行和分佈式計算非常容易,對計算進行容錯也非常容易;其次,網絡帶寬是稀缺資源。大量的系統優化因此是旨在減少通過網絡傳輸的數據量:本地優化允許我們從本地磁盤讀取數據,將中間數據的單獨一份拷貝寫入本地磁盤節約了網絡帶寬;第三,冗餘執行可以用來降低慢速機器的影響(alex注:即硬件配置的不平衡),並且可以用來處理機器失效和數據丟失。

附錄:詞頻程序

本節包含這樣一個程序:程序統計在一組由命令行指定的輸入文件中每個唯一的詞出現的次數。

#include "mapreduce/mapreduce.h"

//用戶的map函數

Class WordCounter : public Mapper{

public:

virtual void Map(const MapInput& input) {

       const string& text = input.value();

              const int n = text.size();

              for(int I = 0;I < n;) {

                     //跳過導致空格的i

                     while((i < n) & & isspace(text[i]))

                            i++;

                     //找到詞尾

                     int start = i;

                     while((i < n) && !isspace(text[i]))

                            i++;

                     if(start < i)

                            Emit(text.substr(start,i-start),"1");

              }

       }

};

REGISTER_MAPPER(WordCounter);

//用戶的reduce函數

class Adder : public Reducer {

       virtual void Reduce(ReduceInput* input) {

              //遍歷所有key值相同的條目並累加value

              int64 value = 0;

              while(!input->done()) {

                     value += StringToInt(input->value());

                     input->NextValue();

              }

              //Emit sum for input->key()

              Emit(IntToString(value));

       }

};

REGISTER_REDUCER(Adder);

int main(int argc,char** argv){

       ParseCommandLineFlags(argc,argv);

       MapReduceSpecification spec;

       //將輸入文件列表存入"spec"

       for(int i = 1;I < argc;i++) {

              MapReduceInput* input = spec.add_input();

              input->set_format("text");

              input->set_filepattern(argv[i]);

              input->set_mapper_class("WordCounter");

       }

       //指定輸出文件:

       ///gfs/test/freq-00000-of-00100

       ///gfs/test/freq-00001-of-00100

       //...

       MapReduceOutput* out = spec.output();

       out->set_filebase("/gfs/test/freq");

       out->set_num_tasks(100);

       out->set_format("text");

       out->set_reducer_class("Adder");

       //可選:map任務內部進行並行加和以節省網絡帶寬

       out->set_combiner_class("Adder");

       //調節參數:每個任務使用至多2000臺機器和100M的內存

       spec.set_machines(2000);

       spec.set_map_megabytes(100);

       spec.set_reduce_megabytes(100);

       //運行

       MapReduceResult result;

       if(!MapReduce(spec,&result))

              abort();

       //Done:result結構包含計數器,花費的時間以及用到的機器數目等

       Return 0;

}

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