RDD原理

RDD概念

RDD(Resilient Distributed Dataset)叫做分佈式數據集,是Spark中最基本的數據抽象,它代表一個不可變、可分區、裏面的元素可並行計算的集合。RDD具有數據流模型的特點:自動容錯、位置感知性調度和可伸縮性。RDD允許用戶在執行多個查詢時顯式地將工作集緩存在內存中,後續的查詢能夠重用工作集,這極大地提升了查詢速度。

RDD是Spark的最基本抽象,是對分佈式內存的抽象使用,實現了以操作本地集合的方式來操作分佈式數據集的抽象實現。RDD是Spark最核心的東西,它表示已被分區,不可變的並能夠被並行操作的數據集合,不同的數據集格式對應不同的RDD實現。RDD必須是可序列化的。RDD可以cache到內存中,每次對RDD數據集的操作之後的結果,都可以存放到內存中,下一個操作可以直接從內存中輸入,省去了MapReduce大量的磁盤IO操作

RDD可以橫向多分區,當計算過程中內存不足時,將數據刷到磁盤等外部存儲上,從而實現數據在內存和外存的靈活切換。可以說,RDD是有虛擬數據結構組成,並不包含真實數據體。

RDD的內部屬性

通過RDD的內部屬性,用戶可以獲取相應的元數據信息。通過這些信息可以支持更復雜的算法或優化。

一組分片(Partition),即數據集的基本組成單位

對於RDD來說,每個分片都會被一個task計算任務處理,並決定並行計算的粒度。用戶可以在創建RDD時指定RDD的分片個數,如果沒有指定,那麼就會採用默認值。默認值就是程序所分配的CPU Core的數據。

計算每個分片的函數

Spark中RDD的計算是以分片爲單位的,通過函數可以對每個數據塊進行RDD需要進行的用戶自定義函數運算。函數會對迭代器進行復合,不需要保存每次計算的結果。

RDD之間的依賴關係

對父RDD的依賴列表,依賴還具體分爲寬依賴和窄依賴,但並不是所有的RDD都有依賴。RDD的每次轉換都會生成一個新的RDD,所以RDD之間就會形成類似於流水線一樣的前後依賴關係。在部分分區數據丟失時,Spark可以通過這個依賴關係重新計算丟失的分區數據,而不是對RDD的所有分區進行重新計算。

一個Partitioner,即RDD的分片函數

當前Spark中實現了兩種類型的分片函數,一個是基於哈希的HashPartitioner,另外一個是基於範圍的RangePartitioner。只有對於於key-value的RDD,纔會有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函數不但決定了RDD本身的分片數量,也決定了parent RDD Shuffle輸出時的分片數量。

分區列表,存儲存取每個Partition的優先位置(preferred location)

對於一個HDFS文件來說,這個列表保存的就是每個Partition所在的塊的位置。按照“移動數據不如移動計算”的理念,Spark在進行任務調度的時候,會儘可能地將計算任務分配到其所要處理數據塊的存儲位置。

可選屬性

key-value型的RDD是根據哈希來分區的,類似於mapreduce當中的Paritioner接口,控制key分到哪個reduce。

可選屬性

每一個分片的優先計算位置(preferred locations),比如HDFS的block的所在位置應該是優先計算的位置。(存儲的是一個表,可以將處理的分區“本地化”)

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操作被執行時,運算纔會觸發。

RDD的優點

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

RDD的存儲與分區

  1. 用戶可以選擇不同的存儲級別存儲RDD以便重用。
  2. 當前RDD默認是存儲於內存,但當內存不足時,RDD會spill到disk。
  3. RDD在需要進行分區把數據分佈於集羣中時會根據每條記錄Key進行分區(如Hash 分區),以此保證兩個數據集在Join時能高效。
  4. RDD根據useDisk、useMemory、useOffHeap、deserialized、replication參數的組合定義了以下存儲級別:
//存儲等級定義:  
val NONE = new StorageLevel(false, false, false, false)  
val DISK_ONLY = new StorageLevel(true, false, false, false)  
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)  
val MEMORY_ONLY = new StorageLevel(false, true, false, true)  
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)  
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)  
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)  
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)  
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)  
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)  
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)  
val OFF_HEAP = new StorageLevel(false, false, true, false)  

RDD的容錯機制

RDD的容錯機制實現分佈式數據集容錯方法有兩種:數據檢查點記錄更新

RDD採用記錄更新的方式:記錄所有更新點的成本很高。所以,RDD只支持粗顆粒變換,即只記錄單個塊(分區)上執行的單個操作,然後創建某個RDD的變換序列(血統 lineage)存儲下來;

變換序列指,每個RDD都包含了它是如何由其他RDD變換過來的以及如何重建某一塊數據的信息。因此RDD的容錯機制又稱“血統”容錯。

要實現這種“血統”容錯機制,最大的難題就是如何表達父RDD和子RDD之間的依賴關係。實際上依賴關係可以分兩種,窄依賴寬依賴

窄依賴:子RDD中的每個數據塊只依賴於父RDD中對應的有限個固定的數據塊

寬依賴:子RDD中的一個數據塊可以依賴於父RDD中的所有數據塊。例如:map變換,子RDD中的數據塊只依賴於父RDD中對應的一個數據塊;groupByKey變換,子RDD中的數據塊會依賴於多塊父RDD中的數據塊,因爲一個key可能分佈於父RDD的任何一個數據塊中

將依賴關係分類的兩個特性:
1. 窄依賴可以在某個計算節點上直接通過計算父RDD的某塊數據計算得到子RDD對應的某塊數據;寬依賴則要等到父RDD所有數據都計算完成之後,並且父RDD的計算結果進行hash並傳到對應節點上之後才能計算子RDD。
2. 數據丟失時,對於窄依賴只需要重新計算丟失的那一塊數據來恢復;對於寬依賴則要將祖先RDD中的所有數據塊全部重新計算來恢復。

所以在“血統”鏈特別是有寬依賴的時候,需要在適當的時機設置數據檢查點。也是這兩個特性要求對於不同依賴關係要採取不同的任務調度機制和容錯恢復機制。

Spark計算工作流

  1. 輸入:在Spark程序運行中,數據從外部數據空間(例如,HDFS、Scala集合或數據)輸入到Spark,數據就進入了Spark運行時數據空間,會轉化爲Spark中的數據塊,通過BlockManager進行管理。
  2. 運行:在Spark數據輸入形成RDD後,便可以通過變換算子fliter等,對數據操作並將RDD轉化爲新的RDD,通過行動(Action)算子,觸發Spark提交作業。如果數據需要複用,可以通過Cache算子,將數據緩存到內存。
  3. 輸出:程序運行結束數據會輸出Spark運行時空間,存儲到分佈式存儲中(如saveAsTextFile輸出到HDFS)或Scala數據或集合中(collect輸出到Scala集合,count返回Scala Int型數據)。

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

image
image
image
image

RDD編程模型

textFile算子從HDFS讀取日誌文件,返回“file”(RDD);filter算子篩出帶“ERROR”的行,賦給 “errors”(新RDD);cache算子把它緩存下來以備未來使用;count算子返回“errors”的行數。RDD看起來與Scala集合類型 沒有太大差別,但它們的數據和運行模型大相迥異。

val file = sc.textFile("hdfs://...")
val errors = file.filter(_.contains("ERROR"))
errors.cache()
errors.count()

上面代碼給出了RDD數據模型,並將上例中用到的四個算子映射到四種算子類型。Spark程序工作在兩個空間中:Spark RDD空間和Scala原生數據空間。在原生數據空間裏,數據表現爲標量(scalar,即Scala基本類型,用橘色小方塊表示)、集合類型(藍色虛線 框)和持久存儲(紅色圓柱)。

下圖描述了Spark運行過程中通過算子對RDD進行轉換, 算子是RDD中定義的函數,可以對RDD中的數據進行轉換和操作。
image

輸入算子(橘色箭頭)將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中。
image
image

RDD依賴關係

RDD依賴關係如下圖所示:
image

窄依賴 (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的劃分:

image
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)進行算子的優化。

image

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