標籤:
本文轉載:http://shiyanjun.cn/archives/744.html
摘要
本文提出了分布式記憶體抽象的概念——彈性分布式資料集(RDD,Resilient Distributed Datasets),它具備像MapReduce等資料流模型的容錯特性,並且允許開發人員在大型叢集上執行基於記憶體的計算。現有的資料流系統對兩種應用的處理並不高效:一是迭代式演算法,這在圖應用和機器學習領域很常見;二是互動式資料採礦工具。這兩種情況下,將資料儲存在記憶體中能夠極大地提高效能。為了有效地實現容錯,RDD提供了一種高度受限的共用記憶體,即RDD是唯讀,並且只能通過其他RDD上的大量操作來建立。儘管如此,RDD仍然足以表示很多類型的計算,包括MapReduce和專用的迭代編程模型(如Pregel)等。我們實現的RDD在迭代計算方面比Hadoop快20多倍,同時還可以在5-7秒內互動式地查詢1TB資料集。
1.引言
無論是工業界還是學術界,都已經廣泛使用進階叢集編程模型來處理日益增長的資料,如MapReduce和Dryad。這些系統將分布式編程簡化為自動提供位置感知性調度、容錯以及負載平衡,使得大量使用者能夠在商用叢集上分析超大資料集。
大多數現有的叢集計算系統都是基於非迴圈的資料流模型。從穩定的實體儲存體(如Distributed File System)中載入記錄,記錄被傳入由一組確定性操作構成的DAG,然後寫回穩定儲存。DAG資料流圖能夠在運行時自動實現任務調度和故障恢複。
儘管非迴圈資料流是一種很強大的抽象方法,但仍然有些應用無法使用這種方式描述。我們就是針對這些不太適合非迴圈模型的應用,它們的特點是在多個並行操作之間重用工作資料集。這類應用程式套件括:(1)機器學習和圖應用中常用的迭代演算法(每一步對資料執行相似的函數);(2)互動式資料採礦工具(使用者反覆查詢一個資料子集)。基於資料流的架構並不明確支援工作集,所以需要將資料輸出到磁碟,然後在每次查詢時重新載入,這帶來較大的開銷。
我們提出了一種分布式的記憶體抽象,稱為彈性分布式資料集(RDD,Resilient Distributed Datasets)。它支援基於工作集的應用,同時具有資料流模型的特點:自動容錯、位置感知調度和延展性。RDD允許使用者在執行多個查詢時顯式地將工作集緩衝在記憶體中,後續的查詢能夠重用工作集,這極大地提升了查詢速度。
RDD提供了一種高度受限的共用記憶體模型,即RDD是唯讀記錄分區的集合,只能通過在其他RDD執行確定的轉換操作(如map、join和group by)而建立,然而這些限制使得實現容錯的開銷很低。與分布式共用記憶體系統需要付出高昂代價的檢查點和復原機制不同,RDD通過Lineage來重建丟失的分區:一個RDD中包含了如何從其他RDD衍生所必需的相關資訊,從而不需要檢查點操作就可以重構丟失的資料分區。儘管RDD不是一個通用的共用記憶體抽象,但卻具備了良好的描述能力、延展性和可靠性,但卻能夠廣泛適用於資料並行類應用。
第一個指出非迴圈資料流存在不足的並非是我們,例如,Google的Pregel[21],是一種專門用於迭代式圖演算法的編程模型;Twister[13]和HaLoop[8],是兩種典型的迭代式MapReduce模型。但是,對於一些特定類型的應用,這些系統提供了一個受限的通訊模型。相比之下,RDD則為基於工作集的應用提供了更為通用的抽象,使用者可以對中間結果進行顯式的命名和物化,控制其分區,還能執行使用者選擇的特定操作(而不是在運行時去迴圈執行一系列MapReduce步驟)。RDD可以用來描述Pregel、迭代式MapReduce,以及這兩種模型無法描述的其他應用,如互動式資料採礦工具(使用者將資料集裝入記憶體,然後執行ad-hoc查詢)。
Spark是我們實現的RDD系統,在我們內部能夠被用於開發多種並行應用。Spark採用Scala語言[5]實現,提供類似於DryadLINQ的整合語言編程介面[34],使使用者可以非常容易地編寫並行任務。此外,隨著Scala新版本解譯器的完善,Spark還能夠用於互動式查詢大資料集。我們相信Spark會是第一個能夠使用有效、通用程式設計語言,並在叢集上對大資料集進行互動式分析的系統。
我們通過微基準和使用者應用程式來評估RDD。實驗表明,在處理迭代式應用上Spark比Hadoop快高達20多倍,計算資料分析類報表的效能提高了40多倍,同時能夠在5-7秒的延時內互動式掃描1TB資料集。此外,我們還在Spark之上實現了Pregel和HaLoop編程模型(包括其位置最佳化策略),以庫的形式實現(分別使用了100和200行Scala代碼)。最後,利用RDD內在的確定性特性,我們還建立了一種Spark調試工具rddbg,允許使用者在任務期間利用Lineage重建RDD,然後像傳統調試器那樣重新執行任務。
本文首先在第2部分介紹了RDD的概念,然後第3部分描述Spark API,第4部分解釋如何使用RDD表示幾種並行應用(包括Pregel和HaLoop),第5部分討論Spark中RDD的表示方法以及任務調度器,第6部分描述具體實現和rddbg,第7部分對RDD進行評估,第8部分給出了相關研究工作,最後第9部分總結。
2.彈性分布式資料集(RDD)
本部分描述RDD和編程模型。首先討論設計目標(2.1),然後定義RDD(2.2),討論Spark的編程模型(2.3),並給出一個樣本(2.4),最後對比RDD與分布式共用記憶體(2.5)。
2.1 目標和概述
我們的目標是為基於工作集的應用(即多個並行操作重用中間結果的這類應用)提供抽象,同時保持MapReduce及其相關模型的優勢特性:即自動容錯、位置感知性調度和延展性。RDD比資料流模型更易於編程,同時基於工作集的計算也具有良好的描述能力。
在這些特性中,最難實現的是容錯性。一般來說,分布式資料集的容錯性有兩種方式:即資料檢查點和記錄資料的更新。我們面向的是大規模資料分析,資料檢查點操作成本很高:需要通過資料中心的網路連接在機器之間複製龐大的資料集,而網路頻寬往往比記憶體頻寬低得多,同時還需要消耗更多的儲存資源(在記憶體中複製資料可以減少需要緩衝的資料量,而儲存到磁碟則會拖慢應用程式)。所以,我們選擇記錄更新的方式。但是,如果更新太多,那麼記錄更新成本也不低。因此,RDD只支援粗粒度轉換,即在大量記錄上執行的單個操作。將建立RDD的一系列轉換記錄下來(即Lineage),以便恢複丟失的分區。
雖然只支援粗粒度轉換限制了編程模型,但我們發現RDD仍然可以很好地適用於很多應用,特別是支援資料並行的批量分析應用,包括資料採礦、機器學習、圖演算法等,因為這些程式通常都會在很多記錄上執行相同的操作。RDD不太適合那些非同步更新共用狀態的應用,例如並行web爬行器。因此,我們的目標是為大多數分析型應用提供有效編程模型,而其他類型的應用交給專門的系統。
2.2 RDD抽象
RDD是唯讀、分區記錄的集合。RDD只能基於在穩定實體儲存體中的資料集和其他已有的RDD上執行確定性操作來建立。這些確定性操作稱之為轉換,如map、filter、groupBy、join。
RDD不需要物化。RDD含有如何從其他RDD衍生(即計算)出本RDD的相關資訊(即Lineage),據此可以從實體儲存體的資料計算出相應的RDD分區。
2.3 編程模型
在Spark中,RDD被表示為對象,通過這些對象上的方法(或函數)調用轉換。
定義RDD之後,程式員就可以在動作中使用RDD了。動作是嚮應用程式傳回值,或向儲存系統匯出資料的那些操作,例如,count(返回RDD中的元素個數),collect(返回元素本身),save(將RDD輸出到儲存系統)。在Spark中,只有在動作第一次使用RDD時,才會計算RDD(即延遲計算)。這樣在構建RDD的時候,運行時通過管道的方式傳輸多個轉換。
程式員還可以從兩個方面控制RDD,即緩衝和分區。使用者可以請求將RDD緩衝,這樣運行時將已經計算好的RDD分區儲存起來,以加速後期的重用。緩衝的RDD一般儲存在記憶體中,但如果記憶體不夠,可以寫到磁碟上。
另一方面,RDD還允許使用者根據關鍵字(key)指定分區順序,這是一個可選的功能。目前支援雜湊分割和定界分割。例如,應用程式請求將兩個RDD按照同樣的雜湊分割方式進行分區(將同一機器上具有相同關鍵字的記錄放在一個分區),以加速它們之間的join操作。在Pregel和HaLoop中,多次迭代之間採用一致性的分區置換策略進行最佳化,我們同樣也允許使用者指定這種最佳化。
2.4 樣本:控制台日誌挖掘
本部分我們通過一個具體樣本來闡述RDD。假定有一個大型網站出錯,操作員想要檢查Hadoop檔案系統(HDFS)中的記錄檔(TB級大小)來找出原因。通過使用Spark,操作員只需將日誌中的錯誤資訊裝載到一組節點的記憶體中,然後執行互動式查詢。首先,需要在Spark解譯器中輸入如下Scala命令:
lines = spark.textFile("hdfs://...")errors = lines.filter(_.startsWith("ERROR"))errors.cache()
第1行從HDFS檔案定義了一個RDD(即一個文本行集合),第2行獲得一個過濾後的RDD,第3行請求將errors緩衝起來。注意在Scala文法中filter的參數是一個閉包。
這時叢集還沒有開始執行任何任務。但是,使用者已經可以在這個RDD上執行對應的動作,例如統計錯誤訊息的數目:errors.count()
使用者還可以在RDD上執行更多的轉換操作,並使用轉換結果,如:
// Count errors mentioning MySQL:errors.filter(_.contains("MySQL")).count()// Return the time fields of errors mentioning HDFS as an array (assuming time is field// number 3 in a tab-separated format):errors.filter(_.contains("HDFS")).map(_.split(‘\t‘)(3)).collect()
使用errors的第一個action運行以後,Spark會把errors的分區緩衝在記憶體中,極大地加快了後續計算速度。注意,最初的RDD lines不會被緩衝。因為錯誤資訊可能只佔原資料集的很小一部分(小到足以放入記憶體)。
最後,為了說明模型的容錯性,圖1給出了第3個查詢的Lineage圖。在lines RDD上執行filter操作,得到errors,然後再filter、map後得到新的RDD,在這個RDD上執行collect操作。Spark調度器以流水線的方式執行後兩個轉換,向擁有errors分區緩衝的節點發送一組任務。此外,如果某個errors分區丟失,Spark只在相應的lines分區上執行filter操作來重建該errors分區。
RDD:基於記憶體的叢集計算容錯抽象