1 RDD的依賴關係及容錯
1.1 RDD的依賴關係
RDD的依賴關係分爲兩種:窄依賴(Narrow Dependencies)與寬依賴(Wide Dependencies,源碼中稱爲Shuffle Dependencies)
依賴有2個作用,其一用來解決數據容錯的高效性;其二用來劃分stage。
窄依賴:每個父RDD的一個Partition最多被子RDD的一個Partition所使用(1:1 或 n:1)。例如map、filter、union等操作都會產生窄依賴;
子RDD分區通常對應常數個父RDD分區(O(1),與數據規模無關。
寬依賴:一個父RDD的Partition會被多個子RDD的Partition所使用,例如groupByKey、reduceByKey、sortByKey等操作都會產生寬依賴;(1:m 或 n:m)
(子RDD分區通常對應所有的父RDD分區(O(n),與數據規模有關)
相比於寬依賴,窄依賴對優化很有利 ,主要基於以下兩點:
1、寬依賴往往對應着shuffle操作,需要在運行過程中將同一個父RDD的分區傳入到不同的子RDD分區中,中間可能涉及多個節點之間的數據傳輸;而窄依賴的每個父RDD的分區只會傳入到一個子RDD分區中,通常可以在一個節點內完成轉換。
2、當RDD分區丟失時(某個節點故障),spark會對數據進行重算。
Ø 對於窄依賴,由於父RDD的一個分區只對應一個子RDD分區,這樣只需要重算和子RDD分區對應的父RDD分區即可,所以這個重算對數據的利用率是100%的;
Ø 對於寬依賴,重算的父RDD分區對應多個子RDD分區,這樣實際上父RDD 中只有一部分的數據是被用於恢復這個丟失的子RDD分區的,另一部分對應子RDD的其它未丟失分區,這就造成了多餘的計算;更一般的,寬依賴中子RDD分區通常來自多個父RDD分區,極端情況下,所有的父RDD分區都要進行重新計算。
Ø 如下圖所示,b1分區丟失,則需要重新計算a1,a2和a3,這就產生了冗餘計算(a1,a2,a3中對應b2的數據)。
區分這兩種依賴很有用。首先,窄依賴允許在一個集羣節點上以流水線的方式(pipeline)計算所有父分區。例如,逐個元素地執行map、然後filter操作;而寬依賴則需要首先計算好所有父分區數據,然後在節點之間進行Shuffle,這與MapReduce類似。第二,窄依賴能夠更有效地進行失效節點的恢復,即只需重新計算丟失RDD分區的父分區,而且不同節點之間可以並行計算;而對於一個寬依賴關係的Lineage圖,單個節點失效可能導致這個RDD的所有祖先丟失部分分區,因而需要整體重新計算。
【誤解】之前一直理解錯了,以爲窄依賴中每個子RDD可能對應多個父RDD,當子RDD丟失時會導致多個父RDD進行重新計算,所以窄依賴不如寬依賴有優勢。而實際上應該深入到分區級別去看待這個問題,而且重算的效用也不在於算的多少,而在於有多少是冗餘的計算。窄依賴中需要重算的都是必須的,所以重算不冗餘。
窄依賴的函數有:map、filter、union、join(父RDD是hash-partitioned )、mapPartitions、mapValues
寬依賴的函數有:groupByKey、join(父RDD不是hash-partitioned )、partitionBy
1.2 依賴樣例
依賴的繼承關係:
val rdd1 = sc.parallelize(1 to 10, 1)
val rdd2 = sc.parallelize(11 to 20, 1)
val rdd3 = rdd1.union(rdd2)
rdd3.dependencies.size
// 長度爲2,值爲rdd1、rdd2,意爲rdd3依賴rdd1、rdd2
rdd3.dependencies
// 結果:
rdd3.dependencies(0).rdd.collect
// 打印rdd1的數據
rdd3.dependencies(1).rdd.collect
// 打印rdd2的數據
rdd3.dependencies(3).rdd.collect
// 數組越界,報錯
哪些RDD Actions對應shuffleDependency?下面的join(r5)好像就沒有shuffleDependency
val r1 = sc.parallelize(List("dog", "salmon", "salmon", "rat", "elephant"))
val r2 = r1.keyBy(_.length)
val r3 = sc.parallelize(List("dog","cat","gnu","salmon","rabbit","turkey","wolf","bear","bee"))
val r4 = r3.keyBy(_.length)
val r5 = r2.join(r4)
回答:join不一定會有shuffleDependency,上面的操作中就沒有。
redueceByKey會產生shuffleDependency。
注意上面操作中的keyBy,和我的想象不太一樣。要注意一下。
keyBy:與map操作較爲類似,給每個元素增加了一個key
以下這個例子有點意思:
val r1 = sc.textFile("hdfs:///user/hadoop/data/block_test1.csv")
r1
val r2 = r1.dependencies(0).rdd
r2.partitions.size
r2.preferredLocations(r2.partitions(0))
r2.preferredLocations(r2.partitions(3))
有意思的地方在於(查找依賴、優先位置):
1、r1的類型爲MapPartitionsRDD
2、r1依賴於r2,如果沒有這個賦值語句是看不出來的。r2的類型爲:HadoopRDD
3、可以檢索r2各個分區的位置,該hdfs文件系統的副本數設置爲2
1.3 RDD的容錯(lineage、checkpoint)
一般來說,分佈式數據集的容錯性有兩種方式:數據檢查點和記錄數據的更新(CheckPoint Data,和Logging The Updates)。
面向大規模數據分析,數據檢查點操作成本很高,需要通過數據中心的網絡連接在機器之間複製龐大的數據集,而網絡帶寬往往比內存帶寬低得多,同時還需要消耗更多的存儲資源。
因此,Spark選擇記錄更新的方式。但是,如果更新粒度太細太多,那麼記錄更新成本也不低。因此,RDD只支持粗粒度轉換,即只記錄單個塊上執行的單個操作(記錄如何從其他RDD轉換而來,即lineage),然後將創建RDD的一系列變換序列(每個RDD都包含了他是如何由其他RDD變換過來的以及如何重建某一塊數據的信息。因此RDD的容錯機制又稱“血統(Lineage)”容錯)記錄下來,以便恢復丟失的分區。
Lineage本質上很類似於數據庫中的重做日誌(Redo Log),只不過這個重做日誌粒度很大,是對全局數據做同樣的重做進而恢復數據。
Lineage容錯原理:在容錯機制中,如果一個節點死機了,而且運算窄依賴,則只要把丟失的父RDD分區重算即可,不依賴於其他節點。而寬依賴需要父RDD的所有分區都存在,重算就很昂貴了。可以這樣理解開銷的經濟與否:在窄依賴中,在子RDD的分區丟失、重算父RDD分區時,父RDD相應分區的所有數據都是子RDD分區的數據,並不存在冗餘計算。在寬依賴情況下,丟失一個子RDD分區重算的每個父RDD的每個分區的所有數據並不是都給丟失的子RDD分區用的,會有一部分數據相當於對應的是未丟失的子RDD分區中需要的數據,這樣就會產生冗餘計算開銷,這也是寬依賴開銷更大的原因。因此如果使用Checkpoint算子來做檢查點,不僅要考慮Lineage是否足夠長,也要考慮是否有寬依賴,對寬依賴加Checkpoint是最物有所值的。
Checkpoint機制。在以下2種情況下,RDD需要加檢查點:
Ø DAG中的Lineage過長,如果重算,則開銷太大(如在多次迭代中)
Ø 在寬依賴上做Checkpoint獲得的收益更大
由於RDD是隻讀的,所以Spark的RDD計算中一致性不是主要關心的內容,內存相對容易管理,這也是設計者很有遠見的地方,這樣減少了框架的複雜性,提升了性能和可擴展性,爲以後上層框架的豐富奠定了強有力的基礎。
在RDD計算中,通過檢查點機制進行容錯,傳統做檢查點有兩種方式:通過冗餘數據和日誌記錄更新操作。在RDD中的doCheckPoint方法相當於通過冗餘數據來緩存數據,而之前介紹的血統就是通過相當粗粒度的記錄更新操作來實現容錯的。
檢查點(本質是通過將RDD寫入Disk做檢查點)是爲了通過lineage做容錯的輔助,Lineage過長會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分區,從做檢查點的RDD開始重做Lineage,就會減少開銷。
1.4 checkpoint與cache的關係
1、從本質上說:checkpoint是容錯機制;cache是優化機制
2、checkpoint將數據寫到共享存儲中(hdfs);cache通常是內存中
3、運算時間很長或運算量太大才能得到的 RDD,computing chain 過長或依賴其他 RDD 很多的RDD,需要做checkpoint。會被重複使用的(但不能太大)RDD,做cache。
實際上,將 ShuffleMapTask 的輸出結果存放到本地磁盤也算是 checkpoint,只不過這個checkpoint 的主要目的是去 partition 輸出數據。
4、RDD 的checkpoint 操作完成後會斬斷lineage,cache操作對lineage沒有影響。
checkpoint 在 Spark Streaming中特別重要,spark streaming 中對於一些有狀態的操作,這在某些 stateful 轉換中是需要的,在這種轉換中,生成 RDD 需要依賴前面的 batches,會導致依賴鏈隨着時間而變長。爲了避免這種沒有盡頭的變長,要定期將中間生成的 RDDs 保存到可靠存儲來切斷依賴鏈,必須隔一段時間進行一次checkpoint。
cache 和 checkpoint 是有顯著區別的,緩存把 RDD 計算出來然後放在內存中, 但是RDD 的依賴鏈(相當於數據庫中的redo 日誌),也不能丟掉,當某個點某個 executor 宕了,上面cache 的RDD就會丟掉,需要通過依賴鏈重放計算出來,不同的是,checkpoint 是把 RDD 保存在 HDFS中,是多副本可靠存儲,所以依賴鏈就可以丟掉了,即斬斷了依賴鏈,是通過複製實現的高容錯。
注意:checkpoint需要把 job 重新從頭算一遍,最好先cache一下,checkpoint就可以直接保存緩存中的 RDD 了,就不需要重頭計算一遍了,對性能有極大的提升。
1.5 checkpoint的使用與流程
checkpoint 的正確使用姿勢
val data = sc.textFile("/tmp/spark/1.data").cache() // 注意要cache
sc.setCheckpointDir("/tmp/spark/checkpoint")
data.checkpoint
data.count
//問題:cache和checkpoint有沒有先後的問題;有了cache可以避免第二次計算,我在代碼中可以看見相關的說明!!!
使用很簡單, 就是設置一下 checkpoint 目錄,然後再rdd上調用 checkpoint 方法, action 的時候就對數據進行了 checkpoint
checkpoint 寫流程
RDD checkpoint 過程中會經過以下幾個狀態,
[ Initialized –> marked for checkpointing –> checkpointing in progress –> checkpointed ]