2012-NSDI-RDD

本篇博客是本人原創整理,其中參考了網上相關資料,最後沒有列全參考資料,深感抱歉。

主要基於RDD論文進行翻譯,參考了網上的中文翻譯版本。並增加了Spark總體概述內容,此部分主要參考廈大林子雨老師的課件。

此報告作爲本人的課程學習報告,如需轉載,請告知本人。


Title

Resilient Distributed Datasets: A Fault-TolerantAbstraction for In-Memory Cluster Computing

Writer

Matei Zaharia, Mosharaf Chowdhury, Tathagata Das,Ankur Dave, Justin Ma, Murphy McCauley, Michael J. Franklin, ScottShenker, Ion Stoica

University of California, Berkeley

NSDI 2012. Best Paper Award and Honorable Mention for Community Award.

1Spark概述

1.1Spark簡介

Spark最初由美國加州伯克利大學的AMP實驗室於2009年開發,是基於內存計算的大數據並行計算框架,可用於構建大型的、低延遲的數據分析應用程序。2013年Spark加入Apache孵化器項目後發展迅猛,如今已成爲Apache軟件基金會最重要的三大分佈式計算系統開源項目之一(Hadoop、Spark、Storm)。Spark在2014年打破了Hadoop保持的基準排序紀錄,用十分之一的計算資源,獲得了比Hadoop快3倍的速度。

Spark主要有4個特點:

  1. 運行速度快:使用DAG執行引擎以支持循環數據流與內存計算。
  2. 容易使用:支持使用Scala、Java、Python和R語言進行編程,也可以通過Spark Shell進行交互式編程。
  3. 通用性強:Spark提供了完整而強大的技術棧,包括SQL查詢、流式計算、機器學習和圖算法組件。
  4. 運行模式多樣::可運行於獨立的集羣模式中,或者運行於Hadoop中,也可運行於Amazon EC2等雲環境中,並且可以訪問HDFS、Cassandra、HBase、Hive等多種數據源。

1.2Spark與Hadoop比較

Hadoop存在如下一些缺點:

  1. 表達能力有限:計算都要轉換成Map和Reduce操作,難以描述複雜的數據處理過程。
  2. 磁盤IO開銷大:每次執行都需要從磁盤讀數據,計算過程中,需要將中間結果寫入磁盤。
  3. 延遲高:任務之間的銜接涉及IO開銷,前一個任務執行完成之前,其他任務無法開始,難以勝任複雜、多階段的計算任務。

Spark在借鑑Hadoop MapReduce優點的同時,很好地解決了MapReduce所面臨的問題。相比於MapReduce,Spark主要具有如下優點:

  1. Spark的計算模式也屬於MapReduce,但不侷限於此,還提供了多種數據集操作類型,編程模型比MapReduce更靈活。
  2. Spark提供了內存計算,可將中間結果放到內存中,對於迭代運算效率更高。
  3. Spark基於DAG的任務調度執行機制,要優於MapReduce的迭代執行機制。

1.3Spark生態圈

在實際應用中,大數據處理主要包括以下三個類型:

  1. 複雜的批量數據處理:通常時間跨度在數十分鐘到數小時之間。
  2. 基於歷史數據的交互式查詢:通常時間跨度在數十秒到數分鐘之間。
  3. 基於實時數據流的數據處理:通常時間跨度在數百毫秒到數秒之間。

當同時存在以上三種場景時,就需要同時部署三種不同的軟件,比如MapReduce、Impala、Storm。這樣做會帶來一些問題:

  1. 不同場景之間輸入輸出數據無法做到無縫共享,通常需要進行數據格式的轉換。
  2. 不同的軟件需要不同的開發和維護團隊,帶來了較高的使用成本。
  3. 比較難以對同一個集羣中的各個系統進行統一的資源協調和分配。

Spark的設計遵循“一個軟件棧滿足不同應用場景”的理念,逐漸形成了一套完整的生態系統——BDAS(Berkeley DataAnalytic Stack)。

以Spark爲核心的BDAS既能夠提供內存計算框架,也可以支持SQL即席查詢、實時流式計算、機器學習和圖計算等。

Spark可以部署在資源管理器YARN之上,提供一站式的大數據解決方案。因此,Spark所提供的生態系統足以應對上述三種場景,即同時支持批處理、交互式查詢和流數據處理。

圖1展示了BDAS架構圖。


圖1:BDAS架構

2RDD運行原理

2.1RDD設計背景

現在的計算框架如MapReduce在大數據分析中被廣泛採用,爲什麼還要設計新的Spark?有如下幾點原因:

  1. 在實際應用中,存在許多迭代式算法(比如迭代式機器學習、圖算法等)和交互式數據挖掘工具(比如用戶在同一數據子集上運行多個Ad-hoc查詢),這些應用場景的共同之處是:不同計算階段之間會重用中間結果,即一個階段的輸出結果會作爲下一個階段的輸入。
  2. 目前的大部分框架(比如MapReduce)對計算之間的數據複用的處理方式都是把中間結果寫入到一個穩定的外部存儲系統中(比如HDFS),這樣會帶來大量的數據複製、磁盤I/O和序列化開銷。
  3. 雖然有些框架對數據重用提出了相應的解決方法,比如類似Pregel等圖計算框架將中間結果保存在內存中,HaLoop提供了迭代MapReduce接口,但是這些都是針對特定功能設計的框架,並對用戶屏蔽了數據共享的方式,不具備通用的抽象模式,例如,允許一個用戶可以加載幾個數據集到內存中並進行一些跨數據集的即時查詢。

針對以上問題,Spark提出了一種新的數據抽象模式RDD(彈性分佈式數據集),令用戶可以直接控制數據的共享。用戶不用擔心底層數據的分佈式特性,只需將具體的應用邏輯表達爲一系列轉換處理,不同RDD之間的轉換操作形成依賴關係,可以實現管道化,從而避免了中間結果的存儲,大大降低了數據複製、磁盤I/O和序列化開銷。

2.2RDD概念

  1. 一個RDD就是一個分佈式對象集合,本質上是一個只讀的分區記錄集合,每個RDD可以分成多個分區,每個分區就是一個數據集片段,並且一個RDD的不同分區可以被保存到集羣中不同的節點上,從而可以在集羣中的不同節點上進行並行計算。
  2. 由於RDD提供了一種高度受限的共享內存模型,不能直接修改,因此RDD的來源只有兩種:基於穩定的物理存儲中的數據集來創建RDD,即從外部系統輸入,比如HDFS、Hive、Cassandra、Hbase等;從父RDD上執行確定的Transformation操作得到,比如map、filter、join、groupBy等。
  1. RDDs在任何時候都不需要被“物化”(進行實際的變換並最終寫入穩定的存儲器上)。實際上,一個RDD有足夠的信息知道自己是從哪個數據集計算而來(就是Lineage)。它有一個強大的特性:從本質上說,若RDD失效且不能重建,程序將不能引用該RDD。
  2. 用戶可以控制RDDs的其他兩個方面:持久化和分區。對於複用數據,可以選擇內存緩存,也可以基於一個元素的key值來爲RDD所有元素在機器節點間進行數據分區。這對位置優化來說是有用的,比如可用來保證兩個要join的數據集使用了相同的哈希分區方式。

RDD典型的執行過程如下圖2所示:


圖2:RDD執行過程的一個實例。首先從外部數據源創建RDDs,然後經過一系列的Transformations操作(如map、filter),每一次都會產生新的RDD,最後一個RDD經過Action操作(如count、collect、save)進行計算得到相應的結果值,並輸出到外部數據源。這一系列處理被稱爲一個Lineage(血緣關係),即DAG(有向無環圖)拓撲排序的結果。可以看到,RDDs只有觸發了Action操作纔會真正計算,這是RDDs的Lazy特性,因此可以先對Transformations進行組裝一系列的Pipelines,然後再通過Actions計算。

此外,編程人員可以通過調用RDDs的persist方法來緩存複用數據。Spark默認將緩存數據放在內存中,但是如果內存不足則會寫入磁盤。用戶可以通過persist的參數來調整緩存策略,比如只將數據存儲在磁盤中或者複製備份到多臺機器上。最後,用戶也可以爲每一個RDDs的緩存設置優先級,明確哪個在內存中的RDDs應該最先寫入磁盤。

舉例說明:監控日誌數據挖掘

假設一個web服務遇到錯誤,運維人員想從存儲在HDFS中的幾TB的日誌中找出錯誤的原因。運維人員可以通過Spark將日誌中的錯誤信息加載到分佈式的內存中,然後對這些內存中的數據進行查詢。以下是Scala代碼:

 

在對errors第一次做Action操作之後,Spark會將errors的所有分區的數據存儲在內存中,這樣對後面的errors的計算速度會有很大提升。需要注意的是,像lines這樣的基礎數據的RDD不會存儲在內存中,包含錯誤信息的數據可能只是整個日誌數據的一小部分,所以將過濾後的數據放在內存中較爲合理。

下圖3展示了代碼中第三個查詢的Linage Graph。


圖3:例子中第三個查詢的Lineage Graph,其中方框表示RDDs,箭頭表示轉換。

2.3RDD操作

表1:Spark中RDD常用的Transformations和Actions操作。Seq[T]表示元素類型爲T的一個列表。表中,一些操作比如join只適合key-value類型的RDDs。函數名需要和編程語言函數名一致。比如, map 是一個 one-to-one 的映射操作, 而 flatMap 的每一個輸入值會對應一個或者更多的輸出值(有點像MapReduce 中的 map)。除了這些操作,用於可以通過persist操作請求緩存RDD。另外,用戶可以拿到被Partitioner 分區後的分區數以及根據 Partitioner 對另一個 dataset 進行分區。 像 groupByKey、reduceByKey 以及 sort 等操作都是經過了hash 或者 range分區後的 RDD。

可以將Transformation操作理解成一種惰性操作,它只是定義了一個新的RDD,而不是立即計算它,而Action操作這時立即計算,並返回結果給程序,或者將結果寫入到外存儲中。

舉例說明:邏輯迴歸

邏輯迴歸算法是一個用來找到最佳區別兩種點集(比如垃圾郵件和非垃圾郵件)的超平面w的常用分類算法,它用到了梯度下降方法:一個隨機的值作爲w的初始值,每次迭代都會將含有w的方法應用到每一個數據點然後累加得到梯度值,w會往改善結果的方向移動。其Scala代碼如下所示。


一開始,定義一個叫points的RDD,這個RDD從一個text文件中經過map將每一行轉換爲Point對象得到。然後我們重複對points進行map和reduce操作計算出每一步的梯度值。在迭代之間我們將points存放在內存中可以使得性能提高20倍。

舉例說明:PageRank

在PageRank中,如果一個文檔引用另一個文檔,那被引用的文檔的Rank需要加上引用文檔發送過來的貢獻值。每次迭代中,每一個文檔都會發送r/n的貢獻值給它的鄰居,其中r表示這個文檔的排名值,n表示這個文檔的鄰居數量。然後更新文檔的排名值爲a/N+(1-a)*sum ci, 這個表達式值表示這個文檔收到的貢獻值, N 表示所有的文檔的數量。其Scala代碼如下所示:

2.4RDDs的表達

抽象RDDs的一個挑戰是如何經過一系列的Transformation操作後追蹤其繼承關係。主要有以下5個接口可以表達RDDs:

  1. 分區列表:每一個分區就是RDD的一部分數據;
  2. 分區位置列表:指明分區優先存放的結點位置;
  3. 依賴列表:存儲父RDD的依賴列表;
  4. 計算函數:利用父分區計算RDD各分區的值;
  5. 分區器:指明RDD的分區方式(hash/range)。

在設計接口的過程中,最有趣的問題在於如何表示RDDs之間的依賴關係。R

DDs之間的依賴有兩種類型:

  1. 窄依賴:表現爲一個父RDD的分區對應於一個子RDD的分區,或多個父RDD的分區對應於一個子RDD的分區,比如map,filter,union等都是窄依賴。
  2. 寬依賴:表現爲存在一個父RDD的一個分區對應於一個子RDD的多個分區,比如groupByKey、sortByKey等是寬依賴。

對於join操作可以分爲兩種情況:

  1. 對輸入進行co-partitioned(協同劃分),屬於窄依賴(圖左所示)。協同劃分是指多個父RDD的某一個分區的所有“鍵(key)”,落在子RDD的同一個分區內,不會產生同一個父RDD的某一分區,落在子RDD的兩個分區的情況。
  2. 對輸入做非協同劃分,屬於寬依賴(圖右所示)。


圖4:窄依賴與寬依賴的區別。每一個方框代表一個RDD,帶有顏色的矩形表示分區。

 以下兩個原因使得區分寬窄依賴很有用:

  1. 窄依賴可以以流水線的方式計算所有父親的分區數據,不會造成網絡之間的數據混合。對於寬依賴的RDD,則通常伴隨着Shuffle操作,即首先需要計算好所有父分區的數據,然後在節點之間進行Shuffle(類似於MapReduce中的Shuffle操作)。
  2. 窄依賴從一個失敗節點中恢復是非常高效的。因爲,RDD數據集通過Lineage記住了它是如何從其他RDD中演變過來的,血緣關係記錄的是粗粒度的轉換操作行爲,當這個RDD的部分分區數據丟失時,它可以通過血緣關係獲取足夠的信息來重新運算和恢復丟失的數據分區(不需要重新計算所有分區),而且可以並行地在不同節點進行重新計算。對於寬依賴,單個節點失效通常意味着重新計算過程會涉及多個父RDD分區,開銷較大。

2.5Stage的劃分

Spark通過分析各個RDD的依賴關係生成了DAG,再通過分析各個RDD中的分區之間的依賴關係來決定如何劃分階段,具體劃分方法如下:

  1. 在DAG中進行反向解析,遇到寬依賴就斷開;
  2. 遇到窄依賴就把當前的RDD加入到當前的階段中;
  3. 將窄依賴儘量劃分在同一個階段中,可以實現流水線計算。


圖5:根據RDD分區的依賴關係劃分Stage。其中實線的方框代表RDDs,藍色的方框代表分區。假設從HDFS中讀入數據生成3個不同的RDD(即A、C、E),通過一系列轉換操作後再將計算結果保存回HDFS。對DAG進行解析時,在依賴圖中進行反向解析,由於從RDD A到RDD B的轉換以及從RDD F到RDD G的轉換,都屬於寬依賴。因此,在寬依賴處斷開後可以得到3個階段,即Stage 1,Stage 2和Stage 3。可以看出,在階段2中,從map到union都是窄依賴,可以形成一個流水線操作。比如,分區7通過map操作生成的分區9,可以不用等待分區8到分區10這個轉換操作的計算結束,而是繼續進行union操作,轉換得到分區13,這樣流水線執行大大提高計算的效率。

由上述論述可知 ,把一個DAG圖劃分成多個Stage以後,每個階段都代表了一組關聯的、相互之間沒有Shuffle依賴關係的任務組成任務集合。每個任務集合會被任務調度器(TaskScheduler)進行處理,由任務調度器將任務分發給Executor運行。

2.6RDD運行過程

RDD在Spark中的運行過程如下圖6所示:

  1. 創建RDD對象;
  2. SparkContext負責計算RDD之間的依賴關係,構建DAG;
  3. DAGScheduler負責把DAG圖劃分爲多個Stage,每個Stage中包含了多個Task;
  4. 每個Task會被TaskScheduler分發給各個Worker Node上的Executor去執行。


圖6:RDD在Spark中的運行過程。

2.7RDD特性

爲了更好理解RDD的好處,下表中用RDDs和分佈式共享內存系統進行了對比。在DSM系統中,應用程序讀取和寫入全局地址空間的任意位置。這裏的DSM不僅包括傳統的共享內存系統,還包括其他能讓應用程序執行細粒度“寫”共享狀態的系統,例如提供DHT的Picclo和分佈式數據庫。DSM是一個很普遍的抽象,但是這個普遍性使得它在商用集羣中實現高效且容錯的系統比較困難。


表2:RDD與分佈式共享內存系統比較

RDDs和DSM之間的區別如下:

  1. RDDs只能通過粗粒度轉換創建,而DSM允許對每個存儲單元讀取和寫入。這使得RDD在批量寫入主導的應用上受到限制,但增強了其容錯方面的效率。
  2. RDDs可以使用Lineage恢復數據,不需要檢查點的開銷。此外,當出現失敗時,RDDs的分區中只有丟失的那部分需要重新計算,而且該計算可在多個節點上併發完成,不必回滾整個程序。而DSM需要檢查點和程序開銷。
  3. RDDs的不可變性讓系統像MapReduce那樣用後備任務代替運行緩慢的任務來減少緩慢節點(stragglers)的影響。因爲在DSM中任務的兩個副本會訪問相同的存儲器位置和受彼此更新的干擾,這樣後備任務在DSM中很難實現。
  4. RDDs還具備了DSM的兩個優點。首先,在RDDs的批量操作過程中,任務的執行可以根據數據的所處的位置來進行優化,從而提高性能。其次,只要所進行的操作時只基於掃描的,當內存不足時,RDD的性能下降也是平穩的。不能載入內存的分區可以存儲在磁盤上,其性能也會與當前其他數據並行系統相當。

Spark採用RDD以後能夠實現高效計算,原因主要在於:

  1. 高效的容錯性。
  1. 已經存在的分佈式內存抽象系統,比如distributed shared memory、key-value stores、databases以及Piccolo,爲了實現容錯,必須在集羣節點之間進行數據複製或者記錄日誌,也就是在節點之間會發生大量的數據傳輸,這對於數據密集型應用而言會帶來很大開銷。
  2. RDD是一種天生具有容錯機制的特殊集合,不需要數據冗餘的方式(比如檢查點)實現容錯,而只需通過RDD父子依賴(血緣)關係重新計算丟失的分區來實現容錯,無需回滾系統,這樣避免了數據複製的高開銷,而且重算過程可以在不同節點之間並行,實現高效的容錯。
  3. 此外,RDD提供的轉換操作都是一些粗粒度的操作(比如map、filter、join),RDD依賴關係只記錄這種粗粒度的轉換操作,而不需要記錄具體的數據和各種細粒度操作的日誌(比如對哪個數據項進行了修改),這句大大降低了數據密集型應用中的容錯開銷。
  1. 中間結果持久化到內存中。數據在內存中的多個RDD操作之間進行傳遞,不需要“落地”到磁盤,避免了不必要的讀寫磁盤開銷。
  2. 存放的數據可以是Java對象,避免了不必要的對象序列化和反序列化開銷。   

2.8RDD應用場景

  1. RDDs非常適合將相同操作應用在整個數據集的所有的元素上的批處理應用。在這些場景下,RDDs可以利用Lineage Graph來高效的記住每一個Transformation的步驟,並且不需要記錄大量的數據就可以恢復丟失的分區數據。
  2. RDDs不太適合用於需要異步且細粒度的更新共享狀態的應用,比如一個web應用或者數據遞增的web爬蟲應用的存儲系統。對於這些應用,使用傳統的記錄更新日誌以及對數據進行checkpoint會更加高效,比如使用數據庫、RAMCloud、Percolator以及Piccolo。

3 實現

3.1作業調度

  1. Spark的調度器類似於Dryad,但是增加了對持久化RDD分區是否在內存裏的考慮
  2. 調度器在分配tasks時採用延遲調度來達到數據本地性的目的(即數據在哪裏,計算就在哪裏)
  3. 對於寬依賴,將中間數據寫入到節點的磁盤中以利於從錯誤中恢復。
  4. 對於執行失敗的任務,只要對應stage的父類信息可用,便會在其他節點上重新執行。


圖7:怎麼計算 spark job stage 的例子。實現的方框表示 RDDs ,帶有顏色的方形表示分區, 黑色的是表示這個分區的數據存儲在內存中。在這個場景中, stage 1 的輸出結果已經在內存中, 所以我們開始運行 stage 2 , 然後是 stage 3。

3.2解釋器集成

  1. 讓用戶通過解釋器來交互性的運行Spark,從而達到查詢大數據集的目的。
  2. Scala解釋器通常將用戶輸入的每一行代碼編譯成一個類。
  3. Spark中做出兩個改變:
  1. 類傳輸:爲了讓工作節點能夠從各行生成的類中獲取到字節碼,解釋器通過HTTP來爲類提供服務。
  2. 代碼生成器的改動:讓各行對象的實例可以被直接引用。

3.3內存管理

Spark在持久化RDDs時提供三種內存管理方式:

  1. Java對象的內存存儲:提供最快的性能。
  2. 序列化數據的內存存儲:用戶內存空間有限時,比java對象圖存儲更加有效。
  3. 磁盤存儲:對大到無法放進內存但每次重新計算又很耗時的RDD非常有用。

此外,當有新的RDD分區被計算出來而內存空間又不足時,Spark使用LRU策略將老分區移除到磁盤上。

3.4檢查點支持

儘管RDD的Lineage可以用來還原數據,但這通常會非常耗時,因此將某些RDDs持久化到磁盤上會非常有用。對於Spark來說,因爲RDD都是不可變的,不用考慮數據的一致性,所以完全可以在後臺持久化RDD,而無需暫停整個系統。

4 評估

通過在Amazon EC2上進行一系列實驗和用戶應用程序的基準測試,對Spark和RDDs進行了性能評估。測試結果如下:

  1. 在迭代機器學習和圖計算中,Spark性能要比Hadoop模型好20倍,這些性能提升來自於將數據以java對象存入內存從而減少系統IO和反序列化的開銷。
  2. 用戶應用程序同樣有很好的性能和擴展性。使用Spark來運行一個原本運行在Hadoop上的分析報告的應用,相較於Hadoop性能提升了40倍。
  3. 當出現節點故障時,Spark可以通過重新計算,快速地恢復那些丟失的RDD分區。
  4. Spark可以在5-7秒內交互式地查詢1TB的數據。

5 總結

RDD是Spark的核心,也是整個Spark的架構基礎。本篇論文重點總結如下:

  1. RDDs提出動機是爲了解決迭代式算法場景和交互式數據挖掘場景中數據共享問題。
  2. RDDs提供了一種受限制的共享內存的方式,這種方式是基於粗粒度的轉換共享狀態而非細粒度的更新共享狀態。
  3. RDD是一個只讀的,被分區的數據集。
  4. RDD具有高效的容錯性,可以通過Linage重新計算丟失分區。

6 參考資料

1.Spark

2.SparkCN

3.Scala教程

4.Spark簡介_廈門大學數據庫實驗室

5.Spark及其生態圈簡介

6.Spark踩坑記:從RDD看集羣調度

7.Spark容錯機制

8.SparkShuffle詳解及作業

9.Spark原理介紹

10.Hadoop&Spark MapReduce對比 & 框架設計與理解

11.SparkRDD深入瞭解

12.邏輯迴歸

13.梯度下降(GradientDescent)小結

14.PageRank算法--從原理到實現


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