Spark RDD使用詳解--RDD原理

後開啟撒打發了發表於2018-01-16

RDD簡介

      在叢集背後,有一個非常重要的分散式資料架構,即彈性分散式資料集(Resilient Distributed Dataset,RDD)。RDD是Spark的最基本抽象,是對分散式記憶體的抽象使用,實現了以操作本地集合的方式來操作分散式資料集的抽象實現。RDD是Spark最核心的東西,它表示已被分割槽,不可變的並能夠被並行操作的資料集合,不同的資料集格式對應不同的RDD實現。RDD必須是可序列化的。RDD可以cache到記憶體中,每次對RDD資料集的操作之後的結果,都可以存放到記憶體中,下一個操作可以直接從記憶體中輸入,省去了MapReduce大量的磁碟IO操作。這對於迭代運算比較常見的機器學習演算法, 互動式資料探勘來說,效率提升比較大。

      (1)RDD的特點
      1)建立:只能通過轉換 ( transformation ,如map/filter/groupBy/join 等,區別於動作 action) 從兩種資料來源中建立 RDD 1 )穩定儲存中的資料; 2 )其他 RDD。
      2)只讀:狀態不可變,不能修改。
      3)分割槽:支援使 RDD 中的元素根據那個 key 來分割槽 ( partitioning ) ,儲存到多個結點上。還原時只會重新計算丟失分割槽的資料,而不會影響整個系統。
      4)路徑:在 RDD 中叫世族或血統 ( lineage ) ,即 RDD 有充足的資訊關於它是如何從其他 RDD 產生而來的。
      5)持久化:支援將會被重用的 RDD 快取 ( 如 in-memory 或溢位到磁碟 )。
      6)延遲計算: Spark 也會延遲計算 RDD ,使其能夠將轉換管道化 (pipeline transformation)。
      7)操作:豐富的轉換(transformation)和動作 ( action ) , count/reduce/collect/save 等。
      執行了多少次transformation操作,RDD都不會真正執行運算(記錄lineage),只有當action操作被執行時,運算才會觸發。

      (2)RDD的好處
      1)RDD只能從持久儲存或通過Transformations操作產生,相比於分散式共享記憶體(DSM)可以更高效實現容錯,對於丟失部分資料分割槽只需根據它的lineage就可重新計算出來,而不需要做特定的Checkpoint。
      2)RDD的不變性,可以實現類Hadoop MapReduce的推測式執行。
      3)RDD的資料分割槽特性,可以通過資料的本地性來提高效能,這不Hadoop MapReduce是一樣的。
      4)RDD都是可序列化的,在記憶體不足時可自動降級為磁碟儲存,把RDD儲存於磁碟上,這時效能會有大的下降但不會差於現在的MapReduce。
      5)批量操作:任務能夠根據資料本地性 (data locality) 被分配,從而提高效能。

      (3)RDD的內部屬性
      通過RDD的內部屬性,使用者可以獲取相應的後設資料資訊。通過這些資訊可以支援更復雜的演算法或優化。
      1)分割槽列表:通過分割槽列表可以找到一個RDD中包含的所有分割槽及其所在地址。
      2)計算每個分片的函式:通過函式可以對每個資料塊進行RDD需要進行的使用者自定義函式運算。
      3)對父RDD的依賴列表,依賴還具體分為寬依賴和窄依賴,但並不是所有的RDD都有依賴。
      4)可選:key-value型的RDD是根據雜湊來分割槽的,類似於mapreduce當中的Paritioner介面,控制key分到哪個reduce。
      5)可選:每一個分片的優先計算位置(preferred locations),比如HDFS的block的所在位置應該是優先計算的位置。(儲存的是一個表,可以將處理的分割槽“本地化”)   

[java] view plain copy
  1. //只計算一次   
  2. protected def getPartitions: Array[Partition]   
  3. //對一個分片進行計算,得出一個可遍歷的結果   
  4. def compute(split: Partition, context: TaskContext): Iterator[T]   
  5. //只計算一次,計算RDD對父RDD的依賴   
  6. protected def getDependencies: Seq[Dependency[_]] = deps   
  7. //可選的,分割槽的方法,針對第4點,類似於mapreduce當中的Paritioner介面,控制key分到哪個reduce   
  8. @transient val partitioner: Option[Partitioner] = None   
  9. //可選的,指定優先位置,輸入引數是split分片,輸出結果是一組優先的節點位置   
  10. protected def getPreferredLocations(split: Partition): Seq[String] = Nil  

      (4)RDD的儲存與分割槽
      1)使用者可以選擇不同的儲存級別儲存RDD以便重用。
      2)當前RDD預設是儲存於記憶體,但當記憶體不足時,RDD會spill到disk。
      3)RDD在需要進行分割槽把資料分佈於叢集中時會根據每條記錄Key進行分割槽(如Hash 分割槽),以此保證兩個資料集在Join時能高效。
      RDD根據useDisk、useMemory、useOffHeap、deserialized、replication引數的組合定義了以下儲存級別:

[java] view plain copy
  1. //儲存等級定義:  
  2. val NONE = new StorageLevel(falsefalsefalsefalse)  
  3. val DISK_ONLY = new StorageLevel(truefalsefalsefalse)  
  4. val DISK_ONLY_2 = new StorageLevel(truefalsefalsefalse2)  
  5. val MEMORY_ONLY = new StorageLevel(falsetruefalsetrue)  
  6. val MEMORY_ONLY_2 = new StorageLevel(falsetruefalsetrue2)  
  7. val MEMORY_ONLY_SER = new StorageLevel(falsetruefalsefalse)  
  8. val MEMORY_ONLY_SER_2 = new StorageLevel(falsetruefalsefalse2)  
  9. val MEMORY_AND_DISK = new StorageLevel(truetruefalsetrue)  
  10. val MEMORY_AND_DISK_2 = new StorageLevel(truetruefalsetrue2)  
  11. val MEMORY_AND_DISK_SER = new StorageLevel(truetruefalsefalse)  
  12. val MEMORY_AND_DISK_SER_2 = new StorageLevel(truetruefalsefalse2)  
  13. val OFF_HEAP = new StorageLevel(falsefalsetruefalse)  

      (5)RDD的容錯機制

      RDD的容錯機制實現分散式資料集容錯方法有兩種:資料檢查點和記錄更新,RDD採用記錄更新的方式:記錄所有更新點的成本很高。所以,RDD只支援粗顆粒變換,即只記錄單個塊(分割槽)上執行的單個操作,然後建立某個RDD的變換序列(血統 lineage)儲存下來;變換序列指,每個RDD都包含了它是如何由其他RDD變換過來的以及如何重建某一塊資料的資訊。因此RDD的容錯機制又稱“血統”容錯。 要實現這種“血統”容錯機制,最大的難題就是如何表達父RDD和子RDD之間的依賴關係。實際上依賴關係可以分兩種,窄依賴寬依賴。窄依賴:子RDD中的每個資料塊只依賴於父RDD中對應的有限個固定的資料塊;寬依賴:子RDD中的一個資料塊可以依賴於父RDD中的所有資料塊。例如:map變換,子RDD中的資料塊只依賴於父RDD中對應的一個資料塊;groupByKey變換,子RDD中的資料塊會依賴於多塊父RDD中的資料塊,因為一個key可能分佈於父RDD的任何一個資料塊中, 將依賴關係分類的兩個特性:第一,窄依賴可以在某個計算節點上直接通過計算父RDD的某塊資料計算得到子RDD對應的某塊資料;寬依賴則要等到父RDD所有資料都計算完成之後,並且父RDD的計算結果進行hash並傳到對應節點上之後才能計運算元RDD。第二,資料丟失時,對於窄依賴只需要重新計算丟失的那一塊資料來恢復;對於寬依賴則要將祖先RDD中的所有資料塊全部重新計算來恢復。所以在“血統”鏈特別是有寬依賴的時候,需要在適當的時機設定資料檢查點。也是這兩個特性要求對於不同依賴關係要採取不同的任務排程機制和容錯恢復機制。

      (6)Spark計算工作流
      圖1-5中描述了Spark的輸入、執行轉換、輸出。在執行轉換中通過運算元對RDD進行轉換。運算元是RDD中定義的函式,可以對RDD中的資料進行轉換和操作。
      ·輸入:在Spark程式執行中,資料從外部資料空間(例如,HDFS、Scala集合或資料)輸入到Spark,資料就進入了Spark執行時資料空間,會轉化為Spark中的資料塊,通過BlockManager進行管理。
      ·執行:在Spark資料輸入形成RDD後,便可以通過變換運算元fliter等,對資料操作並將RDD轉化為新的RDD,通過行動(Action)運算元,觸發Spark提交作業。如果資料需要複用,可以通過Cache運算元,將資料快取到記憶體。
      ·輸出:程式執行結束資料會輸出Spark執行時空間,儲存到分散式儲存中(如saveAsTextFile輸出到HDFS)或Scala資料或集合中(collect輸出到Scala集合,count返回Scala Int型資料)。


      Spark的核心資料模型是RDD,但RDD是個抽象類,具體由各子類實現,如MappedRDD、ShuffledRDD等子類。Spark將常用的大資料操作都轉化成為RDD的子類。


RDD程式設計模型

      來看一段程式碼:textFile運算元從HDFS讀取日誌檔案,返回“file”(RDD);filter運算元篩出帶“ERROR”的行,賦給 “errors”(新RDD);cache運算元把它快取下來以備未來使用;count運算元返回“errors”的行數。RDD看起來與Scala集合型別 沒有太大差別,但它們的資料和執行模型大相迥異。


      上圖給出了RDD資料模型,並將上例中用到的四個運算元對映到四種運算元型別。Spark程式工作在兩個空間中:Spark RDD空間和Scala原生資料空間。在原生資料空間裡,資料表現為標量(scalar,即Scala基本型別,用橘色小方塊表示)、集合型別(藍色虛線 框)和持久儲存(紅色圓柱)。

      下圖描述了Spark執行過程中通過運算元對RDD進行轉換, 運算元是RDD中定義的函式,可以對RDD中的資料進行轉換和操作。


圖1 兩個空間的切換,四類不同的RDD運算元

      輸入運算元(橘色箭頭)將Scala集合型別或儲存中的資料吸入RDD空間,轉為RDD(藍色實線框)。輸入運算元的輸入大致有兩類:一類針對 Scala集合型別,如parallelize;另一類針對儲存資料,如上例中的textFile。輸入運算元的輸出就是Spark空間的RDD。

      因為函式語義,RDD經過變換(transformation)運算元(藍色箭頭)生成新的RDD。變換運算元的輸入和輸出都是RDD。RDD會被劃分 成很多的分割槽 (partition)分佈到叢集的多個節點中,圖1用藍色小方塊代表分割槽。注意,分割槽是個邏輯概念,變換前後的新舊分割槽在物理上可能是同一塊記憶體或存 儲。這是很重要的優化,以防止函式式不變性導致的記憶體需求無限擴張。有些RDD是計算的中間結果,其分割槽並不一定有相應的記憶體或儲存與之對應,如果需要 (如以備未來使用),可以呼叫快取運算元(例子中的cache運算元,灰色箭頭表示)將分割槽物化(materialize)存下來(灰色方塊)。

      一部分變換運算元視RDD的元素為簡單元素,分為如下幾類:

  • 輸入輸出一對一(element-wise)的運算元,且結果RDD的分割槽結構不變,主要是map、flatMap(map後展平為一維RDD);

  • 輸入輸出一對一,但結果RDD的分割槽結構發生了變化,如union(兩個RDD合為一個)、coalesce(分割槽減少);

  • 從輸入中選擇部分元素的運算元,如filter、distinct(去除冗餘元素)、subtract(本RDD有、它RDD無的元素留下來)和sample(取樣)。

      另一部分變換運算元針對Key-Value集合,又分為:

  • 對單個RDD做element-wise運算,如mapValues(保持源RDD的分割槽方式,這與map不同);

  • 對單個RDD重排,如sort、partitionBy(實現一致性的分割槽劃分,這個對資料本地性優化很重要,後面會講);

  • 對單個RDD基於key進行重組和reduce,如groupByKey、reduceByKey;

  • 對兩個RDD基於key進行join和重組,如join、cogroup。

      後三類操作都涉及重排,稱為shuffle類操作。

     從RDD到RDD的變換運算元序列,一直在RDD空間發生。這裡很重要的設計是lazy evaluation:計算並不實際發生,只是不斷地記錄到後設資料。後設資料的結構是DAG(有向無環圖),其中每一個“頂點”是RDD(包括生產該RDD 的運算元),從父RDD到子RDD有“邊”,表示RDD間的依賴性。Spark給後設資料DAG取了個很酷的名字,Lineage(世系)。這個 Lineage也是前面容錯設計中所說的日誌更新。

      Lineage一直增長,直到遇上行動(action)運算元(圖1中的綠色箭頭),這時 就要evaluate了,把剛才累積的所有運算元一次性執行。行動運算元的輸入是RDD(以及該RDD在Lineage上依賴的所有RDD),輸出是執行後生 成的原生資料,可能是Scala標量、集合型別的資料或儲存。當一個運算元的輸出是上述型別時,該運算元必然是行動運算元,其效果則是從RDD空間返回原生資料空間。


RDD執行邏輯

      如圖所示,在Spark應用中,整個執行流程在邏輯上運算之間會形成有向無環圖。Action運算元觸發之後會將所有累積的運算元形成一個有向無環圖,然後由排程器排程該圖上的任務進行運算。Spark的排程方式與MapReduce有所不同。Spark根據RDD之間不同的依賴關係切分形成不同的階段(Stage),一個階段包含一系列函式進行流水線執行。圖中的A、B、C、D、E、F、G,分別代表不同的RDD,RDD內的一個方框代表一個資料塊。資料從HDFS輸入Spark,形成RDD A和RDD C,RDD C上執行map操作,轉換為RDD D,RDD B和RDD F進行join操作轉換為G,而在B到G的過程中又會進行Shuffle。最後RDD G通過函式saveAsSequenceFile輸出儲存到HDFS中。


RDD依賴關係

      RDD的依賴關係如下圖所示:
      窄依賴 (narrowdependencies) 和寬依賴 (widedependencies) 。窄依賴是指 父 RDD 的每個分割槽都只被子 RDD 的一個分割槽所使用,例如map、filter。相應的,那麼寬依賴就是指父 RDD 的分割槽被多個子 RDD 的分割槽所依賴,例如groupByKey、reduceByKey等操作。如果父RDD的一個Partition被一個子RDD的Partition所使用就是窄依賴,否則的話就是寬依賴。
      這種劃分有兩個用處。首先,窄依賴支援在一個結點上管道化執行。例如基於一對一的關係,可以在 filter 之後執行 map 。其次,窄依賴支援更高效的故障還原。因為對於窄依賴,只有丟失的父 RDD 的分割槽需要重新計算。而對於寬依賴,一個結點的故障可能導致來自所有父 RDD 的分割槽丟失,因此就需要完全重新執行。因此對於寬依賴,Spark 會在持有各個父分割槽的結點上,將中間資料持久化來簡化故障還原,就像 MapReduce 會持久化 map 的輸出一樣。
      特別說明:對於join操作有兩種情況,如果join操作的使用每個partition僅僅和已知的Partition進行join,此時的join操作就是窄依賴;其他情況的join操作就是寬依賴;因為是確定的Partition數量的依賴關係,所以就是窄依賴,得出一個推論,窄依賴不僅包含一對一的窄依賴,還包含一對固定個數的窄依賴(也就是說對父RDD的依賴的Partition的數量不會隨著RDD資料規模的改變而改變)

      如何劃分Stage如下圖所示:

      Stage劃分的依據就是寬依賴,什麼時候產生寬依賴呢?例如reduceByKey,groupByKey等Action。
      1.從後往前推理,遇到寬依賴就斷開,遇到窄依賴就把當前的RDD加入到Stage中;
      2.每個Stage裡面的Task的數量是由該Stage中最後一個RDD的Partition數量決定的;
      3.最後一個Stage裡面的任務的型別是ResultTask,前面所有其他Stage裡面的任務型別都是ShuffleMapTask;
      4.代表當前Stage的運算元一定是該Stage的最後一個計算步驟;

      補充:Hadoop中的MapReduce操作中的Mapper和Reducer在Spark中基本等量運算元是:map、reduceByKey;在一個Stage內部,首先是運算元合併,也就是所謂的函數語言程式設計的執行的時候最終進行函式的展開從而把一個Stage內部的多個運算元合併成為一個大運算元(其內部包含了當前Stage中所有運算元對資料的計算邏輯);其次是由於Transformation操作的Lazy特性!!在具體運算元交給叢集的Executor計算之前,首先會通過Spark Framework(DAGScheduler)進行運算元的優化。

RDD如何操作

      (1)RDD的建立方式
      1)從Hadoop檔案系統(或與Hadoop相容的其他持久化儲存系統,如Hive、Cassandra、HBase)輸入(例如HDFS)建立。
      2)從父RDD轉換得到新RDD。
      3)通過parallelize或makeRDD將單機資料建立為分散式RDD。

      (2)RDD的兩種操作運算元
      對於RDD可以有兩種操作運算元:轉換(Transformation)與行動(Action)。
      1)轉換(Transformation):Transformation操作是延遲計算的,也就是說從一個RDD轉換生成另一個RDD的轉換操作不是馬上執行,需要等到有Action操作的時候才會真正觸發運算。
      2)行動(Action):Action運算元會觸發Spark提交作業(Job),並將資料輸出Spark系統。

     1.Transformation具體內容:


     2.Action具體內容:



總結

      相比MapReduce,Spark提供了更加優化和複雜的執行流。讀者還可以深入瞭解Spark的執行機制與Spark運算元,這樣能更加直觀地瞭解API的使用。Spark提供了更加豐富的函式式運算元,這樣就為Spark上層元件的開發奠定了堅實的基礎。後續文章將詳細介紹Spark運算元原始碼及示例。

相關文章