Spark教程(二)—— RDD運行原理

一. RDD設計背景

        程序運行階段會涉及很多迭代算法, 這些場景的共同之處是, 不同計算階段會重用中間結果, 即一個階段的輸出作爲下一個階段的輸入. MapReduce在處理這種情況的時候更多的是把中間結果寫入到HDFS中, 這種操作會帶來大量的數據複製, 磁盤IO和序列化開銷. RDD就是爲了滿足這種需求出現的, 它提供一個抽象的數據架構, 不必擔心底層數據的分佈式特性, 只需將具體的應用邏輯表達爲一系列轉換處理, 不同的RDD之間的轉換操作形成依賴關係, 可以實現管道化, 從而避免了中間結果的存儲, 大大降低了數據複製, 磁盤IO和序列化開銷.

二. RDD概念

        一個RDD是一個分佈式對象集合, 本質上是一個只讀的分區記錄集合. 一個RDD可以分成多個分區, 每個分區可以在不同的集羣節點上被保存, 從而實現並行處理. 

        RDD提供一種高度受限的共享內存模型, 即RDD是隻讀的記錄分區集合, 不能直接修改, 只能基於穩定的物理存儲中的數據集來創建RDD, 或者通過其他RDD轉換來得到新的RDD.

        RDD的數據運算有兩種類型, 分別是 "行動" 和 "轉換". 前者用於執行計算並指定輸出形式, 後者指定RDD之間的相互依賴關係.

        "行動" 和 "轉換" 類型的區別是轉換操作(比如map、filter、groupBy、join等)接受RDD並返回RDD, 而行動操作(比如count、collect等)接受RDD但是返回非RDD(即輸出一個值或結果).

        RDD提供的轉換接口都非常簡單,都是類似map、filter、groupBy、join等粗粒度的數據轉換操作,而不是針對某個數據項的細粒度修改。因此,RDD比較適合對於數據集中元素執行相同操作的批處理式應用,而不適合用於需要異步、細粒度狀態的應用,比如Web應用系統、增量式的網頁爬蟲等。

三. RDD執行實例

        RDD典型的執行過程如下:

  • RDD讀入外部數據源(或者內存中的集合)進行創建
  • RDD經過一系列的“轉換”操作,每一次都會產生不同的RDD,供給下一個“轉換”使用
  • 最後一個RDD經“行動”操作進行處理,並輸出到外部數據源(或者變成Scala集合或標量)

        需要說明的是,RDD採用了惰性調用,即在RDD的執行過程中(如圖9-8所示),真正的計算髮生在RDD的“行動”操作,對於“行動”之前的所有“轉換”操作,Spark只是記錄下“轉換”操作應用的一些基礎數據集以及RDD生成的軌跡,即相互之間的依賴關係,而不會觸發真正的計算。

        如下圖所示,在輸入中邏輯上生成A和C兩個RDD, 經過一系列"轉換操作", 邏輯上生成"F"這個RDD, 之所以說是邏輯上, 是因爲這個時候計算並沒有發生. Spark只是記錄了RDD之間的依賴關係. 當F要進行輸出時, 就會執行"行動操作". Spark纔會根據RDD的依賴關係生成DAG, 並從起點開始真正的計算.

        上述這一系列操作稱爲"血緣關係", 正是因爲"血緣關係"RDD的系列操作才得以實現"管道化". 這樣一個操作得到的結果不需要保存爲中間數據, 而是直接管道流入到下一個操作進行處理. 同時, 這種通過血緣關係把一系列操作進行管道化連接的設計方式, 也使得管道中每次操作的計算變得相對簡單.

        例如, 一個spark應用程序, 基本是基於RDD的一系列計算操作, 具體如下所示:

fileRDD = sc.textFile('hdfs://localhost:9000/test.txt')
def contains(line):
    return 'hello world' in line
filterRDD = fileRDD.filter(contains)
filterRDD.cache()
filterRDD.count()

        第1行代碼從HDFS文件中讀取數據創建一個RDD.

        第2,3行代碼定義一個過濾函數.

        第4行代碼吧fileRDD進行轉換操作得到一個新的RDD, 即filterRDD.

        第5行代碼表示filterRDD進行持久化, 把它保存到內存或磁盤中(這裏採用cache接口把數據集保存在內存中), 方便後續重複使用. 當數據被反覆訪問時(比如查詢一些熱點數據,或者運行迭代算法),這是非常有用的,而且通過cache()可以緩存非常大的數據集,支持跨越幾十甚至上百個節點.

        第6行代碼中的count()是一個行動操作,用於計算一個RDD集合中包含的元素個數.

        所有程序執行的過程如下:

  • 創建這個Spark程序的執行上下文,即創建SparkContext對象
  • 從外部數據源(即HDFS文件)中讀取數據創建fileRDD對象
  • 構建起fileRDD和filterRDD之間的依賴關係,形成DAG圖,這時候並沒有發生真正的計算,只是記錄轉換的軌跡
  • 執行到第6行代碼時,count()是一個行動類型的操作,觸發真正的計算,開始實際執行從fileRDD到filterRDD的轉換操作,並把結果持久化到內存中,最後計算出filterRDD中包含的元素個數

四. RDD特性

(1) 高效的容錯機制. 現有的分佈式計算框架, 爲了實現容錯, 往往會在集羣節點之間進行數據複製或記錄日誌, 也就是在節點之間發生大量的數據傳輸, 這會帶來很大的開銷. 在RDD的設計中, 數據只讀, 不可修改, 如果需要修改數據, 必須從父RDD轉換到子RDD, 由此在不同的RDD之間建立了血緣關係. 所以RDD是一種天生具有容錯機制的特殊集合, 不需要冗餘數據來實現容錯. 只需要通過RDD的父子依賴關係重新計算得到丟失分區來實現容錯.

(2) 中間結果持久化到內存. 數據在內存中的多個RDD之間進行傳遞, 不需要"落盤", 避免不必要的讀寫磁盤開銷.

(3) 存放的數據可以是Java對象, 避免了不必要的對象序列化和反序列化開銷.

五. RDD之間的依賴關係

        RDD中不同的操作會使得不同RDD中的分區會產生不同的依賴。RDD中的依賴關係分爲窄依賴(Narrow Dependency)與寬依賴(Wide Dependency).

        窄依賴表現爲一個父RDD的分區對應於一個子RDD的分區,或多個父RDD的分區對應於一個子RDD的分區. 比如下圖中RDD1是RDD2的父RDD,RDD2是子RDD,RDD1的分區1,對應於RDD2的一個分區(即分區4);再比如,RDD6和RDD7都是RDD8的父RDD,RDD6中的分區(分區15)和RDD7中的分區(分區18),兩者都對應於RDD8中的一個分區(分區21).

        寬依賴則表現爲存在一個父RDD的一個分區對應一個子RDD的多個分區。比如圖9-10(b)中,RDD9是RDD12的父RDD,RDD9中的分區24對應了RDD12中的兩個分區(即分區27和分區28).

        總體而言,如果父RDD的一個分區只被一個子RDD的一個分區所使用就是窄依賴,否則就是寬依賴。窄依賴典型的操作包括map、filter、union等,寬依賴典型的操作包括groupByKey、sortByKey等。對於連接(join)操作,可以分爲兩種情況。

        (1) 對輸入進行協同劃分, 屬於窄依賴. 所謂協同劃分是指是指多個父RDD的某一分區的所有“鍵(key)”,落在子RDD的同一個分區內,不會產生同一個父RDD的某一分區,落在子RDD的兩個分區的情況.

        (2) 對輸入做非協同劃分,屬於寬依賴.

        對於窄依賴的RDD,可以以流水線的方式計算所有父分區,不會造成網絡之間的數據混合。對於寬依賴的RDD,則通常伴隨着Shuffle操作,即首先需要計算好所有父分區數據,然後在節點之間進行Shuffle。

        窄依賴和寬依賴的區別:

       Spark的這種依賴關係設計,使其具有了天生的容錯性,大大加快了Spark的執行速度。因爲,RDD數據集通過“血緣關係”記住了它是如何從其它RDD中演變過來的,血緣關係記錄的是粗顆粒度的轉換操作行爲,當這個RDD的部分分區數據丟失時,它可以通過血緣關係獲取足夠的信息來重新運算和恢復丟失的數據分區,由此帶來了性能的提升。

        相對而言,在兩種依賴關係中,窄依賴的失敗恢復更爲高效,它只需要根據父RDD分區重新計算丟失的分區即可(不需要重新計算所有分區),而且可以並行地在不同節點進行重新計算。

        對於寬依賴而言,單個節點失效通常意味着重新計算過程會涉及多個父RDD分區,開銷較大。

        此外,Spark還提供了數據檢查點和記錄日誌,用於持久化中間RDD,從而使得在進行失敗恢復時不需要追溯到最開始的階段。在進行故障恢復時,Spark會對數據檢查點開銷和重新計算RDD分區的開銷進行比較,從而自動選擇最優的恢復策略。

六. 階段劃分

        Spark通過分析各個RDD的依賴關係生成了DAG,再通過分析各個RDD中的分區之間的依賴關係來決定如何劃分階段,具體劃分方法是:在DAG中進行反向解析,遇到寬依賴就斷開,遇到窄依賴就把當前的RDD加入到當前的階段中;將窄依賴儘量劃分在同一個階段中,可以實現流水線計算.

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

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

        在Spark中,一個應用(Application)由一個任務控制節點(Driver)和若干個作業(Job)構成,一個作業由多個階段(Stage)構成,一個階段由多個任務(Task)組成. 

七. RDD運行過程

        通過上述對RDD概念、依賴關係和階段劃分的介紹,結合之前介紹的Spark運行基本流程,這裏再總結一下RDD在Spark架構中的運行過程:

  • (1) 創建RDD對象
  • (2) SparkContext負責計算RDD之間的依賴關係,構建DAG
  • (3) DAGScheduler負責把DAG圖分解成多個階段,每個階段中包含了多個任務,每個任務會被任務調度器分發給各個工作節點(Worker Node)上的Executor去執行

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