MapReduce:超大機羣上的簡單數據處理(別人翻譯的Google論文)



                           摘要

MapReduce是一個編程模型,和處理,產生大數據集的相關實現.用戶指定一個map函數處理一個key/value,從而產生中間的key/value對集.然後再指定一個reduce函數合併所有的具有相同中間key的中間value.下面將列舉許多可以用這個模型來表示的現實世界的工作.

以這種方式寫的程序能自動的在大規模的普通機器上實現並行化.這個運行時系統關心這些細節:分割輸入數據,在機羣上的調度,機器的錯誤處理,管理機器之間必要的通信.這樣就可以讓那些沒有並行分佈式處理系統經驗的程序員利用大量分佈式系統的資源.

我們的MapReduce實現運行在規模可以靈活調整的由普通機器組成的機羣上,一個典型的MapReduce計算處理幾千臺機器上的以TB計算的數據.程序員發現這個系統非常好用:已經實現了數以百計的MapReduce程序,每天在Google的機羣上都有1000多個MapReduce程序在執行.

1.介紹

在過去的5年裏,作者和Google的許多人已經實現了數以百計的爲專門目的而寫的計算來處理大量的原始數據,比如,爬行的文檔,Web請求日誌,等等.爲了計算各種類型的派生數據,比如,倒排索引,Web文檔的圖結構的各種表示,每個主機上爬行的頁面數量的概要,每天被請求數量最多的集合,等等.很多這樣的計算在概念上很容易理解.然而,輸入的數據量很大,並且只有計算被分佈在成百上千的機器上才能在可以接受的時間內完成.怎樣並行計算,分發數據,處理錯誤,所有這些問題綜合在一起,使得原本很簡介的計算,因爲要大量的複雜代碼來處理這些問題,而變得讓人難以處理.

作爲對這個複雜性的迴應,我們設計一個新的抽象模型,它讓我們表示我們將要執行的簡單計算,而隱藏並行化,容錯,數據分佈,負載均衡的那些雜亂的細節,在一個庫裏.我們的抽象模型的靈感來自Lisp和許多其他函數語言的mapreduce的原始表示.我們認識到我們的許多計算都包含這樣的操作:在我們輸入數據的邏輯記錄上應用map操作,來計算出一箇中間key/value對集,在所有具有相同keyvalue上應用reduce操作,來適當的合併派生的數據.功能模型的使用,再結合用戶指定的mapreduce操作,讓我們可以非常容易的實現大規模並行化計算,和使用再次執行作爲初級機制來實現容錯.

這個工作的主要貢獻是通過簡單有力的接口來實現自動的並行化和大規模分佈式計算,結合這個接口的實現來在大量普通的PC機上實現高性能計算.

第二部分描述基本的編程模型,並且給一些例子.第三部分描述符合我們的基於集羣的計算環境的MapReduce的接口的實現.第四部分描述我們覺得編程模型中一些有用的技巧.第五部分對於各種不同的任務,測量我們實現的性能.第六部分探究在Google內部使用MapReduce作爲基礎來重寫我們的索引系統產品.第七部分討論相關的,和未來的工作.

2.編程模型

計算利用一個輸入key/value對集,來產生一個輸出key/value對集.MapReduce庫的用戶用兩個函數表達這個計算:mapreduce.

用戶自定義的map函數,接受一個輸入對,然後產生一箇中間key/value對集.MapReduce庫把所有具有相同中間keyI的中間value聚合在一起,然後把它們傳遞給reduce函數.

用戶自定義的reduce函數,接受一箇中間keyI和相關的一個value.它合併這些value,形成一個比較小的value.一般的,每次reduce調用只產生01個輸出value.通過一個迭代器把中間value提供給用戶自定義的reduce函數.這樣可以使我們根據內存來控制value列表的大小.

2.1實例

考慮這個問題:計算在一個大的文檔集合中每個詞出現的次數.用戶將寫和下面類似的僞代碼:

map(String key,String value):

 //key:文檔的名字

 //value:文檔的內容

 for each word w in value:

    EmitIntermediate(w,"1");

 

reduce(String key,Iterator values):

//key:一個詞

//values:一個計數列表

 int result=0;

 for each v in values:

   result+=ParseInt(v);

 Emit(AsString(resut));

map函數產生每個詞和這個詞的出現次數(在這個簡單的例子裏就是1).reduce函數把產生的每一個特定的詞的計數加在一起.

另外,用戶用輸入輸出文件的名字和可選的調節參數來填充一個mapreduce規範對象.用戶然後調用MapReduce函數,並把規範對象傳遞給它.用戶的代碼和MapReduce庫鏈接在一起(C++實現).附錄A包含這個實例的全部文本.

2.2類型

即使前面的僞代碼寫成了字符串輸入和輸出的term格式,但是概念上用戶寫的mapreduce函數有關聯的類型:

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

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

例如,輸入的key,value和輸出的key,value的域不同.此外,中間key,value和輸出key,values的域相同.

我們的C++實現傳遞字符串來和用戶自定義的函數交互,並把它留給用戶的代碼,來在字符串和適當的類型間進行轉換.

2.3更多實例

這裏有一些讓人感興趣的簡單程序,可以容易的用MapReduce計算來表示.

分佈式的Grep(UNIX工具程序, 可做文件內的字符串查找):如果輸入行匹配給定的樣式,map函數就輸出這一行.reduce函數就是把中間數據複製到輸出.

計算URL訪問頻率:map函數處理web頁面請求的記錄,輸出(URL,1).reduce函數把相同URLvalue都加起來,產生一個(URL,記錄總數)的對.

倒轉網絡鏈接圖:map函數爲每個鏈接輸出(目標,),一個URL叫做目標,包含這個URL的頁面叫做源.reduce函數根據給定的相關目標URLs連接所有的源URLs形成一個列表,產生(目標,源列表).

每個主機的術語向量:一個術語向量用一個(,頻率)列表來概述出現在一個文檔或一個文檔集中的最重要的一些詞.map函數爲每一個輸入文檔產生一個(主機名,術語向量)(主機名來自文檔的URL).reduce函數接收給定主機的所有文檔的術語向量.它把這些術語向量加在一起,丟棄低頻的術語,然後產生一個最終的(主機名,術語向量).

倒排索引:map函數分析每個文檔,然後產生一個(,文檔號)對的序列.reduce函數接受一個給定詞的所有對,排序相應的文檔IDs,並且產生一個(,文檔ID列表).所有的輸出對集形成一個簡單的倒排索引.它可以簡單的增加跟蹤詞位置的計算.

分佈式排序:map函數從每個記錄提取key,並且產生一個(key,record).reduce函數不改變任何的對.這個計算依賴分割工具(4.1描述)和排序屬性(4.2描述).

3實現

MapReduce接口可能有許多不同的實現.根據環境進行正確的選擇.例如,一個實現對一個共享內存較小的機器是合適的,另外的適合一個大NUMA的多處理器的機器,而有的適合一個更大的網絡機器的集合.

這部分描述一個在Google廣泛使用的計算環境的實現:用交換機連接的普通PC機的大機羣.我們的環境是:

1.Linux操作系統,雙處理器,2-4GB內存的機器.

2.普通的網絡硬件,每個機器的帶寬或者是百兆或者千兆,但是平均小於全部帶寬的一半.

3.因爲一個機羣包含成百上千的機器,所有機器會經常出現問題.

4.存儲用直接連到每個機器上的廉價IDE硬盤.一個從內部文件系統發展起來的分佈式文件系統被用來管理存儲在這些磁盤上的數據.文件系統用複製的方式在不可靠的硬件上來保證可靠性和有效性.

5.用戶提交工作給調度系統.每個工作包含一個任務集,每個工作被調度者映射到機羣中一個可用的機器集上.

 

3.1執行預覽

通過自動分割輸入數據成一個有Msplit的集,map調用被分佈到多臺機器上.輸入的split能夠在不同的機器上被並行處理.通過用分割函數分割中間key,來形成R個片(例如,hash(key)mod R),reduce調用被分佈到多臺機器上.分割數量(R)和分割函數由用戶來指定.

1顯示了我們實現的MapReduce操作的全部流程.當用戶的程序調用MapReduce的函數的時候,將發生下面的一系列動作(下面的數字和圖1中的數字標籤相對應):

    1.在用戶程序裏的MapReduce庫首先分割輸入文件成M個片,每個片的大小一般從 1664MB(用戶可以通過可選的參數來控制).然後在機羣中開始大量的拷貝程序.

      2.這些程序拷貝中的一個是master,其他的都是由master分配任務的worker.M map任務和Rreduce任務將被分配.管理者分配一個map任務或reduce任務給一個空閒的worker.

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

4.緩存在內存中的key/value對被週期性的寫入到本地磁盤上,通過分割函數把它們寫入R個區域.在本地磁盤上的緩存對的位置被傳送給master,master負責把這些位置傳送給reduce worker.

5.當一個reduce worker得到master的位置通知的時候,它使用遠程過程調用來從map worker的磁盤上讀取緩存的數據.reduce worker讀取了所有的中間數據後,它通過排序使具有相同key的內容聚合在一起.因爲許多不同的key映射到相同的reduce任務,所以排序是必須的.如果中間數據比內存還大,那麼還需要一個外部排序.

      6.reduce worker迭代排過序的中間數據,對於遇到的每一個唯一的中間key,它把key和相關的中間value集傳遞給用戶自定義的reduce函數.reduce函數的輸出被添加到這個reduce分割的最終的輸出文件中.

7.當所有的mapreduce任務都完成了,管理者喚醒用戶程序.在這個時候,在用戶程序裏的MapReduce調用返回到用戶代碼.

在成功完成之後,mapreduce執行的輸出存放在R個輸出文件中(每一個reduce任務產生一個由用戶指定名字的文件).一般,用戶不需要合併這R個輸出文件成一個文件--他們經常把這些文件當作一個輸入傳遞給其他的MapReduce調用,或者在可以處理多個分割文件的分佈式應用中使用他們.

3.2master數據結構

master保持一些數據結構.它爲每一個mapreduce任務存儲它們的狀態(空閒,工作中,完成),worker機器(非空閒任務的機器)的標識.

master就像一個管道,通過它,中間文件區域的位置從map任務傳遞到reduce任務.因此,對於每個完成的map任務,master存儲由map任務產生的R箇中間文件區域的大小和位置.map任務完成的時候,位置和大小的更新信息被接受.這些信息被逐步增加的傳遞給那些正在工作的reduce任務.

3.3容錯

因爲MapReduce庫被設計用來使用成百上千的機器來幫助處理非常大規模的數據,所以這個庫必須要能很好的處理機器故障.

worker故障

master週期性的ping每個worker.如果master在一個確定的時間段內沒有收到worker返回的信息,那麼它將把這個worker標記成失效.因爲每一個由這個失效的worker完成的map任務被重新設置成它初始的空閒狀態,所以它可以被安排給其他的worker.同樣的,每一個在失敗的worker上正在運行的mapreduce任務,也被重新設置成空閒狀態,並且將被重新調度.

在一個失敗機器上已經完成的map任務將被再次執行,因爲它的輸出存儲在它的磁盤上,所以不可訪問.已經完成的reduce任務將不會再次執行,因爲它的輸出存儲在全局文件系統中.

當一個map任務首先被workerA執行之後,又被B執行了(因爲A失效了),重新執行這個情況被通知給所有執行reduce任務的worker.任何還沒有從A讀數據的reduce任務將從workerB讀取數據.

MapReduce可以處理大規模worker失敗的情況.例如,在一個MapReduce操作期間,在正在運行的機羣上進行網絡維護引起80臺機器在幾分鐘內不可訪問了,MapReduce master只是簡單的再次執行已經被不可訪問的worker完成的工作,繼續執行,最終完成這個MapReduce操作.

master失敗

可以很容易的讓管理者週期的寫入上面描述的數據結構的checkpoints.如果這個master任務失效了,可以從上次最後一個checkpoint開始啓動另一個master進程.然而,因爲只有一個master,所以它的失敗是比較麻煩的,因此我們現在的實現是,如果master失敗,就中止MapReduce計算.客戶可以檢查這個狀態,並且可以根據需要重新執行MapReduce操作.

在錯誤面前的處理機制

當用戶提供的mapreduce操作對它的輸出值是確定的函數時,我們的分佈式實現產生,和全部程序沒有錯誤的順序執行一樣,相同的輸出.

我們依賴對mapreduce任務的輸出進行原子提交來完成這個性質.每個工作中的任務把它的輸出寫到私有臨時文件中.一個reduce任務產生一個這樣的文件,而一個map任務產生R個這樣的文件(一個reduce任務對應一個文件).當一個map任務完成的時候,worker發送一個消息給master,在這個消息中包含這R個臨時文件的名字.如果master從一個已經完成的map任務再次收到一個完成的消息,它將忽略這個消息.否則,它在master的數據結構裏記錄這R個文件的名字.

當一個reduce任務完成的時候,這個reduceworker原子的把臨時文件重命名成最終的輸出文件.如果相同的reduce任務在多個機器上執行,多個重命名調用將被執行,併產生相同的輸出文件.我們依賴由底層文件系統提供的原子重命名操作來保證,最終的文件系統狀態僅僅包含一個reduce任務產生的數據.

我們的mapreduce操作大部分都是確定的,並且我們的處理機制等價於一個順序的執行的這個事實,使得程序員可以很容易的理解程序的行爲.map/reduce操作是不確定的時候,我們提供雖然比較弱但是合理的處理機制.當在一個非確定操作的前面,一個reduce任務R1的輸出等價於一個非確定順序程序執行產生的輸出.然而,一個不同的reduce任務R2的輸出也許符合一個不同的非確定順序程序執行產生的輸出.

考慮map任務Mreduce任務R1,R2的情況.我們設定e(Ri)爲已經提交的Ri的執行(有且僅有一個這樣的執行).這個比較弱的語義出現,因爲e(R1)也許已經讀取了由M的執行產生的輸出,e(R2)也許已經讀取了由M的不同執行產生的輸出.

3.4存儲位置

在我們的計算機環境裏,網絡帶寬是一個相當缺乏的資源.我們利用把輸入數據(GFS管理)存儲在機器的本地磁盤上來保存網絡帶寬.GFS把每個文件分成64MB的一些塊,然後每個塊的幾個拷貝存儲在不同的機器上(一般是3個拷貝).MapReducemaster考慮輸入文件的位置信息,並且努力在一個包含相關輸入數據的機器上安排一個map任務.如果這樣做失敗了,它嘗試在那個任務的輸入數據的附近安排一個map任務(例如,分配到一個和包含輸入數據塊在一個switch裏的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任務對使用一個字節的數據).

此外,R經常被用戶限制,因爲每一個reduce任務最終都是一個獨立的輸出文件.實際上,我們傾向於選擇M,以便每一個單獨的任務大概都是1664MB的輸入數據(以便上面描述的位置優化是最有效的),我們把R設置成我們希望使用的worker機器數量的小倍數.我們經常執行MapReduce計算,M=200000,R=5000,使用2000臺工作者機器的情況下.

3.6備用任務

一個落後者是延長MapReduce操作時間的原因之一:一個機器花費一個異乎尋常地的長時間來完成最後的一些mapreduce任務中的一個.有很多原因可能產生落後者.例如,一個有壞磁盤的機器經常發生可以糾正的錯誤,這樣就使讀性能從30MB/s降低到3MB/s.機羣調度系統也許已經安排其他的任務在這個機器上,由於計算要使用CPU,內存,本地磁盤,網絡帶寬的原因,引起它執行MapReduce代碼很慢.我們最近遇到的一個問題是,一個在機器初始化時的Bug引起處理器緩存的失效:在一個被影響的機器上的計算性能有上百倍的影響.

我們有一個一般的機制來減輕這個落後者的問題.當一個MapReduce操作將要完成的時候,master調度備用進程來執行那些剩下的還在執行的任務.無論是原來的還是備用的執行完成了,工作都被標記成完成.我們已經調整了這個機制,通常只會佔用多幾個百分點的機器資源.我們發現這可以顯著的減少完成大規模MapReduce操作的時間.作爲一個例子,將要在5.3描述的排序程序,在關閉掉備用任務的情況下,要比有備用任務的情況下多花44%的時間.

4技巧

儘管簡單的mapreduce函數的功能對於大多數需求是足夠的了,但是我們開發了一些有用的擴充.這些將在這個部分描述.

4.1分割函數

MapReduce用戶指定reduce任務和reduce任務需要的輸出文件的數量.在中間key上使用分割函數,使數據分割後通過這些任務.一個缺省的分割函數使用hash方法(例如,hash(key) mod R).這個導致非常平衡的分割.然後,有的時候,使用其他的key分割函數來分割數據有非常有用的.例如,有時候,輸出的keyURLs,並且我們希望每個主機的所有條目保持在同一個輸出文件中.爲了支持像這樣的情況,MapReduce庫的用戶可以提供專門的分割函數.例如,使用"hash(Hostname(urlkey)) mod R"作爲分割函數,使所有來自同一個主機的URLs保存在同一個輸出文件中.

4.2順序保證

我們保證在一個給定的分割裏面,中間key/value對以key遞增的順序處理.這個順序保證可以使每個分割產出一個有序的輸出文件,當輸出文件的格式需要支持有效率的隨機訪問key的時候,或者對輸出數據集再作排序的時候,就很容易.

 

4.3combiner函數

在某些情況下,允許中間結果key重複會佔據相當的比重,並且用戶定義的reduce函數

滿足結合律和交換律.一個很好的例子就是在2.1部分的詞統計程序.因爲詞頻率傾向於一個zipf分佈(齊夫分佈),每個map任務將產生成百上千個這樣的記錄<the,1>.所有的這些計數將通過網絡被傳輸到一個單獨的reduce任務,然後由reduce函數加在一起產生一個數字.我們允許用戶指定一個可選的combiner函數,先在本地進行合併一下,然後再通過網絡發送.

在每一個執行map任務的機器上combiner函數被執行.一般的,相同的代碼被用在combinerreduce函數.combinerreduce函數之間唯一的區別是MapReduce庫怎樣控制函數的輸出.reduce函數的輸出被保存最終輸出文件裏.combiner函數的輸出被寫到中間文件裏,然後被髮送給reduce任務.

部分使用combiner可以顯著的提高一些MapReduce操作的速度.附錄A包含一個使用combiner函數的例子.

4.4輸入輸出類型

MapReduce庫支持以幾種不同的格式讀取輸入數據.例如,文本模式輸入把每一行看作是一個key/value.key是文件的偏移量,value是那一行的內容.其他普通的支持格式以key的順序存儲key/value對序列.每一個輸入類型的實現知道怎樣把輸入分割成對每個單獨的map任務來說是有意義的(例如,文本模式的範圍分割確保僅僅在每行的邊界進行範圍分割).雖然許多用戶僅僅使用很少的預定意輸入類型的一個,但是用戶可以通過提供一個簡單的reader接口來支持一個新的輸入類型.

一個reader不必要從文件裏讀數據.例如,我們可以很容易的定義它從數據庫裏讀記錄,或從內存中的數據結構讀取.

4.5副作用

有的時候,MapReduce的用戶發現在map操作或/reduce操作時產生輔助文件作爲一個附加的輸出是很方便的.我們依靠應用程序寫來使這個副作用成爲原子的.一般的,應用程序寫一個臨時文件,然後一旦這個文件全部產生完,就自動的被重命名.

對於單個任務產生的多個輸出文件來說,我們沒有提供其上的兩階段提交的原子操作支持.因此,一個產生需要交叉文件連接的多個輸出文件的任務,應該使確定性的任務.不過這個限制在實際的工作中並不是一個問題.

4.6跳過錯誤記錄

有的時候因爲用戶的代碼裏有bug,導致在某一個記錄上mapreduce函數突然crash.這樣的bug使得MapReduce操作不能完成.雖然一般是修復這個bug,但是有時候這是不現實的;也許這個bug是在源代碼不可得到的第三方庫裏.有的時候也可以忽略一些記錄,例如,當在一個大的數據集上進行統計分析.我們提供一個可選的執行模式,在這個模式下,MapReduce庫檢測那些記錄引起的crash,然後跳過那些記錄,來繼續執行程序.

每個worker程序安裝一個信號處理器來獲取內存段異常和總線錯誤.在調用一個用戶自定義的mapreduce操作之前,MapReduce庫把記錄的序列號存儲在一個全局變量裏.如果用戶代碼產生一個信號,那個信號處理器就會發送一個包含序號的"last gasp"UDP包給MapReducemaster.master不止一次看到同一個記錄的時候,它就會指出,當相關的mapreduce任務再次執行的時候,這個記錄應當被跳過.

4.7本地執行

調試在mapreduce函數中問題是很困難的,因爲實際的計算髮生在一個分佈式的系統中,經常是有一個master動態的分配工作給幾千臺機器.爲了簡化調試和測試,我們開發了一個可替換的實現,這個實現在本地執行所有的MapReduce操作.用戶可以控制執行,這樣計算可以限制到特定的map任務上.用戶以一個標誌調用他們的程序,然後可以容易的使用他們認爲好用的任何調試和測試工具(例如,gdb).

4.8狀態信息

master運行一個HTTP服務器,並且可以輸出一組狀況頁來供人們使用.狀態頁顯示計算進度,象多少個任務已經完成,多少個還在運行,輸入的字節數,中間數據字節數,輸出字節數,處理百分比,等等.這個頁也包含到標準錯誤的鏈接,和由每個任務產生的標準輸出的鏈接.用戶可以根據這些數據預測計算需要花費的時間,和是否需要更多的資源.當計算比預期的要慢很多的時候,這些頁面也可以被用來判斷是不是這樣.

此外,最上面的狀態頁顯示已經有多少個工作者失敗了,和當它們失敗的時候,那個mapreduce任務正在運行.當試圖診斷在用戶代碼裏的bug,這個信息也是有用的.

4.9計數器

MapReduce庫提供一個計數器工具,來計算各種事件的發生次數.例如,用戶代碼想要計算所有處理的詞的個數,或者被索引的德文文檔的數量.

爲了使用這個工具,用戶代碼創建一個命名的計數器對象,然後在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把來自成功的mapreduce任務的計數器值加起來,MapReduce操作完成的時候,把它返回給用戶代碼.當前計數器的值也被顯示在master狀態頁裏,以便人們可以查看實際的計算進度.當計算計數器值的時候消除重複執行的影響,避免數據的累加.(在備用任務的使用,和由於出錯的重新執行,可以產生重複執行)

有些計數器值被MapReduce庫自動的維護,比如,被處理的輸入key/value對的數量,和被產生的輸出key/value對的數量.

用戶發現計數器工具對於檢查MapReduce操作的完整性很有用.例如,在一些MapReduce操作中,用戶代碼也許想要確保輸出對的數量完全等於輸入對的數量,或者處理過的德文文檔的數量是在全部被處理的文檔數量中屬於合理的範圍.

5性能

在本節,我們用在一個大型集羣上運行的兩個計算來衡量MapReduce的性能.一個計算用來在一個大概1TB的數據中查找特定的匹配串.另一個計算排序大概1TB的數據.

這兩個程序代表了MapReduce的用戶實現的真實的程序的一個大子集.一類是,把數據從一種表示轉化到另一種表示.另一類是,從一個大的數據集中提取少量的關心的數據.

5.1機羣配置

所有的程序在包含大概1800臺機器的機羣上執行.機器的配置是:22GIntel Xeon超線程處理器,4GB內存,兩個160GB IDE磁盤,一個千兆網卡.這些機器部署在一個由兩層的,樹形交換網絡中,在根節點上大概有1002000G的帶寬.所有這些機器都有相同的部署(對等部署),因此任意兩點之間的來回時間小於1毫秒.

 

4GB的內存裏,大概有1-1.5GB被用來運行在機羣中其他的任務.這個程序是在週末的下午開始執行的,這個時候CPU,磁盤,網絡基本上是空閒的.

5.2Grep

這個Grep程序掃描大概10^10,每個100字節的記錄,查找比較少的3字符的查找串(這個查找串出現在92337個記錄中).輸入數據被分割成大概64MB的片(M=15000),全部 的輸出存放在一個文件中(R=1).

2顯示計算過程隨時間變化的情況.Y軸表示輸入數據被掃描的速度.隨着更多的機羣被分配給這個MapReduce計算,速度在逐步的提高,當有1764worker的時候這個速度達到最高的30GB/s.map任務完成的時候,速度開始下降,在計算開始後80,輸入的速度降到0.這個計算持續的時間大概是150.這包括了前面大概一分鐘的啓動時間.啓動時間用來把程序傳播到所有的機器上,等待GFS打開1000個輸入文件,得到必要的位置優化信息.

5.3排序

這個sort程序排序10^10個記錄,每個記錄100個字節(大概1TB的數據).這個程序是模仿TeraSort.

這個排序程序只包含不到50行的用戶代碼.其中有3map函數用來從文本行提取10字節的排序key,並且產生一個由這個key和原始文本行組成的中間key/value.我們使用一個內置的Identity函數作爲reduce操作.這個函數直接把中間key/value對作爲輸出的key/value.最終的排序輸出寫到一個2路複製的GFS文件中(也就是,程序的輸出會寫2TB的數據).

象以前一樣,輸入數據被分割成64MB的片(M=15000).我們把排序後的輸出寫到4000個文件中(R=4000).分區函數使用key的原始字節來把數據分區到R個小片中.

我們以這個基準的分割函數,知道key的分佈情況.在一般的排序程序中,我們會增加一個預處理的MapReduce操作,這個操作用於採樣key的情況,並且用這個採樣的key的分佈情況來計算對最終排序處理的分割點。

3(a)顯示這個排序程序的正常執行情況.左上圖顯示輸入數據的讀取速度.這個速度最高到達13GB/s,並且在不到200秒所有map任務完成之後迅速滑落到0.注意到這個輸入速度小於Grep.這是因爲這個排序map任務花費大概一半的時間和帶寬,來把中間數據寫到本地硬盤中.Grep相關的中間數據可以忽略不計.

左中圖顯示數據通過網絡從map任務傳輸給reduce任務的速度.當第一個map任務完成後,這個排序過程就開始了.圖示上的第一個高峯是啓動了第一批大概1700reduce任務(整個MapReduce任務被分配到1700臺機器上,每個機器一次只執行一個reduce任務).大概開始計算後的300,第一批reduce任務中的一些完成了,我們開始執行剩下的reduce任務.全部的排序過程持續了大概600秒的時間.

左下圖顯示排序後的數據被reduce任務寫入最終文件的速度.因爲機器忙於排序中間數據,所以在第一個排序階段的結束和寫階段的開始有一個延遲.寫的速度大概是2-4GB/s.大概開始計算後的850秒寫過程結束.包括前面的啓動過程,全部的計算任務持續的891.這個和TeraSort benchmark的最高紀錄1057秒差不多.

需要注意的事情是:因此位置優化的原因,很多數據都是從本地磁盤讀取的而沒有通過我們有限帶寬的網絡,所以輸入速度比排序速度和輸出速度都要快.排序速度比輸出速度快的原因是輸出階段寫兩個排序後數據的拷貝(我們寫兩個副本的原因是爲了可靠性和可用性).我們寫兩份的原因是因爲底層文件系統的可靠性和可用性的要求.如果底層文件系統用類似容錯編碼(erasure coding)的方式,而不採用複製寫的方式,在寫盤階段可以降低網絡帶寬的要求。

5.4備用任務的影響

在圖3(b),顯示我們不用備用任務的排序程序的執行情況.除了它有一個很長的幾乎沒有寫動作發生的尾巴外,執行流程和圖3(a)相似.960秒後,只有5reduce任務沒有完成.然而,就是這最後幾個落後者知道300秒後才完成.全部的計算任務執行了1283,多花了44%的時間.

5.5機器失效

在圖3(c),顯示我們有意的在排序程序計算過程中停止1746worker中的200臺機器上的程序的情況.底層機羣調度者在這些機器上馬上重新開始新的worker程序(因爲僅僅程序被停止,而機器仍然在正常運行).

因爲已經完成的map工作丟失了(由於相關的mapworker被殺掉了),需要重新再作,所以worker死掉會導致一個負數的輸入速率.相關map任務的重新執行很快就重新執行了.整個計算過程在933秒內完成,包括了前邊的啓動時間(只比正常執行時間多了5%的時間).

6經驗

我們在2003年的2月寫了MapReduce庫的第一個版本,並且在2003年的8月做了顯著的增強,包括位置優化,worker機器間任務執行的動態負載均衡,等等.從那個時候起,我們驚奇的發現MapReduce函數庫廣泛用於我們日常處理的問題.它現在在Google內部各個領域內廣泛應用,包括:

    大規模機器學習問題

Google NewsFroogle產品的機器問題.

提取數據產生一個流行查詢的報告(例如,GoogleZeitgeist).

爲新的試驗和產品提取網頁的屬性(例如,從一個web頁的大集合中提取位置信息  用在位置查詢).

   大規模的圖計算.

4顯示了我們主要的源代碼管理系統中,隨着時間推移,MapReduce程序的顯著增加,2003年早先時候的0個增長到20049月份的差不多900個不同的程序.MapReduce之所以這樣的成功,是因爲他能夠在不到半小時時間內寫出一個簡單的能夠應用於上千臺機器的大規模併發程序,並且極大的提高了開發和原形設計的週期效率.並且,他可以讓一個完全沒有分佈式和/或並行系統經驗的程序員,能夠很容易的利用大量的資源.

在每一個任務結束的時候,MapReduce函數庫記錄使用的計算資源的統計信息.在圖1,我們列出了20048月份在Google運行的一些MapReduce的工作的統計信息.

6.1大規模索引

到目前爲止,最成功的MapReduce的應用就是重寫了Googleweb 搜索服務所使用到的index系統.索引系統處理爬蟲系統抓回來的超大量的文檔集,這些文檔集保存在GFS文件裏.這些文檔的原始內容的大小,超過了20TB.索引程序是通過一系列的,大概510MapReduce操作來建立索引.通過利用MapReduce(替換掉上一個版本的特別設計的分佈處理的索引程序版本)有這樣一些好處:

   索引的代碼簡單,量少,容易理解,因爲容錯,分佈式,並行處理都隱藏在MapReduce庫中了.例如,當使用MapReduce函數庫的時候,計算的代碼行數從原來的3800C++代碼一下減少到大概700行代碼.

   MapReduce的函數庫的性能已經非常好,所以我們可以把概念上不相關的計算步驟分開處理,而不是混在一起以期減少在數據上的處理.這使得改變索引過程很容易.例如,我們對老索引系統的一個小更改可能要好幾個月的時間,但是在新系統內,只需要花幾天時間就可以了.

   索引系統的操作更容易了,這是因爲機器的失效,速度慢的機器,以及網絡失效都已經由MapReduce自己解決了,而不需要操作人員的交互.另外,我們可以簡單的通過對索引系統增加機器的方式提高處理性能.

7相關工作

很多系統都提供了嚴格的設計模式,並且通過對編程的嚴格限制來實現自動的並行計算.例如,一個結合函數可以通過N個元素的數組的前綴在N個處理器上使用並行前綴計算在log N的時間內計算完.MapReduce是基於我們的大型現實計算的經驗,對這些模型的一個簡化和精煉.並且,我們還提供了基於上千臺處理器的容錯實現.而大部分併發處理系統都只在小規模的尺度上實現,並且機器的容錯還是程序員來控制的.

Bulk Synchronous Programming以及一些MPIprimitives提供了更高級別的抽象,可以更容易寫出並行處理的程序.這些系統和MapReduce系統的不同之處在,MapReduce利用嚴格的編程模式自動實現用戶程序的併發處理,並且提供了透明的容錯處理.

我們本地的優化策略是受activedisks等技術的啓發,activedisks,計算任務是儘量推送到靠近本地磁盤的處理單元上,這樣就減少了通過I/O子系統或網絡的數據量.我們在少量磁盤直接連接到普通處理機運行,來代替直接連接到磁盤控制器的處理機上,但是一般的步驟是相似的.

我們的備用任務的機制和在Charlotte系統上的積極調度機制相似.這個簡單的積極調度的一個缺陷是,如果一個任務引起了一個重複性的失敗,那個整個計算將無法完成.我們通過在故障情況下跳過故障記錄的機制,在某種程度上解決了這個問題.

MapReduce實現依賴一個內置的機羣管理系統來在一個大規模共享機器組上分佈和運行用戶任務.雖然這個不是本論文的重點,但是集羣管理系統在理念上和Condor等其他系統是一樣的.

MapReduce庫中的排序工具在操作上和NOW-Sort相似.源機器(mapworker)分割將要被排序的數據,然後把它發送到Rreduceworker中的一個上.每個reduceworker來本地排序它的數據(如果可能,就在內存中).當然,NOW-Sort沒有用戶自定義的mapreduce函數,使得我們的庫可以廣泛的應用.

River提供一個編程模型,在這個模型下,處理進程可以靠在分佈式的隊列上發送數據進行彼此通訊.MapReduce一樣,River系統嘗試提供對不同應用有近似平均的性能,即使在不對等的硬件環境下或者在系統顛簸的情況下也能提供近似平均的性.River是通過精心調度硬盤和網絡的通訊,來平衡任務的完成時間.MapReduce不和它不同.利用嚴格編程模型,MapReduce構架來把問題分割成大量的任務.這些任務被自動的在可用的worker上調度,以便速度快的worker可以處理更多的任務.這個嚴格編程模型也讓我們可以在工作快要結束的時候安排冗餘的執行,來在非一致處理的情況減少完成時間(比如,在有慢機或者阻塞的worker的時候).

BAD-FS是一個很MapReduce完全不同的編程模型,它的目標是在一個廣闊的網絡上執行工作.然而,它們有兩個基本原理是相同的.(1)這兩個系統使用冗餘的執行來從由失效引起的數據丟失中恢復.(2)這兩個系統使用本地化調度策略,來減少通過擁擠的網絡連接發送的數據數量.

TACC是一個被設計用來簡化高有效性網絡服務結構的系統.MapReduce一樣,它通過再次執行來實現容錯.

8結束語

MapReduce編程模型已經在Google成功的用在不同的目的.我們把這個成功歸於以下幾個原因:第一,這個模型使用簡單,甚至對沒有並行和分佈式經驗的程序員也是如此,因爲它隱藏了並行化,容錯,位置優化和負載均衡的細節.第二,大量不同的問題可以用MapReduce計算來表達.例如,MapReduce被用來,Google的產品web搜索服務,排序,數據挖掘,機器學習,和其他許多系統,產生數據.第三,我們已經在一個好幾千臺計算機的大型集羣上開發實現了這個MapReduce.這個實現使得對於這些機器資源的利用非常簡單,因此也適用於解決Google遇到的其他很多需要大量計算的問題.

從這個工作中我們也學習到了一些東西.首先,嚴格的編程模型使得並行化和分佈式計算簡單,並且也易於構造這樣的容錯計算環境.第二,網絡帶寬是系統的瓶頸.因此在我們的系統中大量的優化目標是減少通過網絡發送的數據量,本地優化使用我們從本地磁盤讀取數據,並且把中間數據寫到本地磁盤,以保留網絡帶寬.第三,冗餘的執行可以用來減少速度慢的機器的影響,和控制機器失效和數據丟失.

感謝

Josh Levenberg校定和擴展了用戶級別的MapReduceAPI,並且結合他的適用經驗和其他人的改進建議,增加了很多新的功能.MapReduceGFS中讀取和寫入數據.我們要感謝Mohit Aron,Howard Gobioff,MarkusGutschke,David Krame,Shun-Tak Leung,Josh Redstone,他們在開發GFS中的工作.我們還感謝PercyLiang Olcan Sercinoglu 在開發用於MapReduce的集羣管理系統得工作.Mike Burrows,Wilson Hsieh,JoshLevenberg,Sharon Perl,RobPike,Debby Wallach爲本論文提出了寶貴的意見.OSDI的無名審閱者,以及我們的審覈者Eric Brewer,在論文應當如何改進方面給出了有益的意見.最後,我們感謝Google的工程部的所有MapReduce的用戶,感謝他們提供了有用的反饋,建議,以及錯誤報告等等.

A單詞頻率統計

本節包含了一個完整的程序,用於統計在一組命令行指定的輸入文件中,每一個不同的單詞出現頻率.

#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;) {

        //跳過前導空格

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

            i++;

         // 查找單詞的結束位置

         intstart = 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();

             }

              //提交這個輸入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臺機器,每個任務100MB內存

       spec.set_machines(2000);

      spec.set_map_megabytes(100);

      spec.set_reduce_megabytes(100);

       // 運行它

       MapReduceResultresult;

       if (!MapReduce(spec,&result)) abort();

       // 完成: 'result'結構包含計數,花費時間,和使用機器的信息

       return 0;

}

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