Spark學習筆記5——容錯機制

容錯 指的是一個系統在部分模塊出現故障時還能否持續的對外提供服務,一個高可用的系統應該具有很高的容錯性;對於一個大的集羣系統來說,機器故障、網絡異常等都是很常見的,Spark這樣的大型分佈式計算集羣提供了很多的容錯機制來提高整個系統的可用性。

一般來說,分佈式數據集的容錯性有兩種方式:數據檢查點和記錄數據的更新。 
面向大規模數據分析,數據檢查點操作成本很高,需要通過數據中心的網絡連接在機器之間複製龐大的數據集,而網絡帶寬往往比內存帶寬低得多,同時還需要消耗更多的存儲資源。 
因此,Spark選擇記錄更新的方式。但是,如果更新粒度太細太多,那麼記錄更新成本也不低。因此,RDD只支持粗粒度轉換,即只記錄單個塊上執行的單個操作,然後將創建RDD的一系列變換序列(每個RDD都包含了他是如何由其他RDD變換過來的以及如何重建某一塊數據的信息。因此RDD的容錯機制又稱“血統(Lineage)”容錯)記錄下來,以便恢復丟失的分區。
Lineage本質上很類似於數據庫中的重做日誌(Redo Log),只不過這個重做日誌粒度很大,是對全局數據做同樣的重做進而恢復數據。

1、Lineage機制

Lineage簡介

相比其他系統的細顆粒度的內存數據更新級別的備份或者LOG機制,RDD的Lineage記錄的是粗顆粒度的特定數據Transformation操作(如filter、map、join等)行爲。當這個RDD的部分分區數據丟失時,它可以通過Lineage獲取足夠的信息來重新運算和恢復丟失的數據分區。因爲這種粗顆粒的數據模型,限制了Spark的運用場合,所以Spark並不適用於所有高性能要求的場景,但同時相比細顆粒度的數據模型,也帶來了性能的提升。

兩種依賴關係

RDD在Lineage依賴方面分爲兩種:窄依賴(Narrow Dependencies)與寬依賴(Wide Dependencies,源碼中稱爲Shuffle
Dependencies),用來解決數據容錯的高效性。

  • 窄依賴是指父RDD的每一個分區最多被一個子RDD的分區所用,表現爲一個父RDD的分區對應於一個子RDD的分區 
    或多個父RDD的分區對應於一個子RDD的分區,也就是說一個父RDD的一個分區不可能對應一個子RDD的多個分區。 
    1個父RDD分區對應1個子RDD分區,這其中又分兩種情況:1個子RDD分區對應1個父RDD分區(如map、filter等算子),1個子RDD分區對應N個父RDD分區(如co-paritioned(協同劃分)過的Join)。
  • 寬依賴是指子RDD的分區依賴於父RDD的多個分區或所有分區,即存在一個父RDD的一個分區對應一個子RDD的多個分區。 
    1個父RDD分區對應多個子RDD分區,這其中又分兩種情況:1個父RDD對應所有子RDD分區(未經協同劃分的Join)或者1個父RDD對應非全部的多個RDD分區(如groupByKey)。

 spark 依賴的實現:

abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {

//返回子RDD的partitionId依賴的所有的parent RDD的Partition(s)

def getParents(partitionId: Int): Seq[Int]

override def rdd: RDD[T] = _rdd

}

(1)窄依賴是有兩種具體實現,分別如下:

     一種是一對一的依賴,即OneToOneDependency:

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {

override def getParents(partitionId: Int) = List(partitionId)

}

    通過getParents的實現不難看出,RDD僅僅依賴於parent RDD相同ID的Partition。
 
 
    還有一個是範圍的依賴,即RangeDependency,它僅僅被org.apache.spark.rdd.UnionRDD使用。UnionRDD是把多個RDD合成一個RDD,這些RDD是被拼接而成,即每個parent RDD的Partition的相對順序不會變,只不過每個parent RDD在UnionRDD中的Partition的起始位置不同。因此它的getPartents如下:

override def getParents(partitionId: Int) = {

if(partitionId >= outStart && partitionId < outStart + length) {

List(partitionId - outStart + inStart)

} else {

Nil

}

}

  其中,inStart是parent RDD中Partition的起始位置,outStart是在UnionRDD中的起始位置,length就是parent RDD中Partition的數量。

(2)寬依賴的實現

  寬依賴的實現只有一種:ShuffleDependency。子RDD依賴於parent RDD的所有Partition,因此需要Shuffle過程:

class ShuffleDependency[K, V, C](

@transient _rdd: RDD[_ <: Product2[K, V]],

val partitioner: Partitioner,

val serializer: Option[Serializer] = None,

val keyOrdering: Option[Ordering[K]] = None,

val aggregator: Option[Aggregator[K, V, C]] = None,

val mapSideCombine: Boolean = false)

extends Dependency[Product2[K, V]] {


override def rdd = _rdd.asInstanceOf[RDD[Product2[K, V]]]

//獲取新的shuffleId

val shuffleId: Int = _rdd.context.newShuffleId()

//向ShuffleManager註冊Shuffle的信息

val shuffleHandle: ShuffleHandle =

_rdd.context.env.shuffleManager.registerShuffle(

shuffleId, _rdd.partitions.size, this)


_rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))

}

  注意:寬依賴支持兩種Shuffle Manager。即org.apache.spark.shuffle.hash.HashShuffleManager(基於Hash的Shuffle機制)和org.apache.spark.shuffle.sort.SortShuffleManager(基於排序的Shuffle機制)。

 

本質理解:根據父RDD分區是對應1個還是多個子RDD分區來區分窄依賴(父分區對應一個子分區)和寬依賴(父分區對應多個子分 
區)。如果對應多個,則當容錯重算分區時,因爲父分區數據只有一部分是需要重算子分區的,其餘數據重算就造成了冗餘計算。

對於寬依賴,Stage計算的輸入和輸出在不同的節點上,對於輸入節點完好,而輸出節點死機的情況,通過重新計算恢復數據這種情況下,這種方法容錯是有效的,否則無效,因爲無法重試,需要向上追溯其祖先看是否可以重試(這就是lineage,血統的意思),窄依賴對於數據的重算開銷要遠小於寬依賴的數據重算開銷。

窄依賴和寬依賴的概念主要用在兩個地方:一個是容錯中相當於Redo日誌的功能;另一個是在調度中構建DAG作爲不同Stage的劃分點。

依賴關係的特性

第一,窄依賴可以在某個計算節點上直接通過計算父RDD的某塊數據計算得到子RDD對應的某塊數據;寬依賴則要等到父RDD所有數據都計算完成之後,並且父RDD的計算結果進行hash並傳到對應節點上之後才能計算子RDD。
第二,數據丟失時,對於窄依賴只需要重新計算丟失的那一塊數據來恢復;對於寬依賴則要將祖先RDD中的所有數據塊全部重新計算來恢復。所以在長“血統”鏈特別是有寬依賴的時候,需要在適當的時機設置數據檢查點。也是這兩個特性要求對於不同依賴關係要採取不同的任務調度機制和容錯恢復機制。

容錯原理

在容錯機制中,如果一個節點死機了,而且運算窄依賴,則只要把丟失的父RDD分區重算即可,不依賴於其他節點。而寬依賴需要父RDD的所有分區都存在,重算就很昂貴了。可以這樣理解開銷的經濟與否:在窄依賴中,在子RDD的分區丟失、重算父RDD分區時,父RDD相應分區的所有數據都是子RDD分區的數據,並不存在冗餘計算。在寬依賴情況下,丟失一個子RDD分區重算的每個父RDD的每個分區的所有數據並不是都給丟失的子RDD分區用的,會有一部分數據相當於對應的是未丟失的子RDD分區中需要的數據,這樣就會產生冗餘計算開銷,這也是寬依賴開銷更大的原因。

2、Checkpoint機制

 

我們應該都很熟悉 checkpoint 這個概念, 就是把內存中的變化刷新到持久存儲,斬斷依賴鏈 在存儲中 checkpoint 是一個很常見的概念, 舉幾個例子

  • 數據庫 checkpoint 過程中一般把內存中的變化進行持久化到物理頁, 這時候就可以斬斷依賴鏈, 就可以把 redo 日誌刪掉了, 然後更新下檢查點,
  • hdfs namenode 的元數據 editlog, Secondary namenode 會把 edit log 應用到 fsimage, 然後刷到磁盤上, 也相當於做了一次 checkpoint, 就可以把老的 edit log 刪除了。
  • spark streaming 中對於一些 有狀態的操作, 這在某些 stateful 轉換中是需要的,在這種轉換中,生成 RDD 需要依賴前面的 batches,會導致依賴鏈隨着時間而變長。爲了避免這種沒有盡頭的變長,要定期將中間生成的 RDDs 保存到可靠存儲來切斷依賴鏈, 必須隔一段時間進行一次進行一次 checkpoint。

cache 和 checkpoint 是有顯著區別的, 緩存把 RDD 計算出來然後放在內存中, 但是RDD 的依賴鏈(相當於數據庫中的redo 日誌), 也不能丟掉, 當某個點某個 executor 宕了, 上面cache 的RDD就會丟掉, 需要通過 依賴鏈重放計算出來, 不同的是, checkpoint 是把 RDD 保存在 HDFS中, 是多副本可靠存儲,所以依賴鏈就可以丟掉了,就斬斷了依賴鏈, 是通過複製實現的高容錯。但是有一點要注意, 因爲checkpoint是需要把 job 重新從頭算一遍, 最好先cache一下, checkpoint就可以直接保存緩存中的 RDD 了, 就不需要重頭計算一遍了, 對性能有極大的提升。

 

checkpoint 的正確使用姿勢

val data = sc.textFile("/tmp/spark/1.data").cache() // 注意要cache

sc.setCheckpointDir("/tmp/spark/checkpoint")

data.checkpoint

data.count

使用很簡單, 就是設置一下 checkpoint 目錄,然後再rdd上調用 checkpoint 方法, action 的時候就對數據進行了 checkpoint

checkpoint 寫流程

RDD checkpoint 過程中會經過以下幾個狀態,

[ Initialized –> marked for checkpointing –> checkpointing in progress –> checkpointed ]

我們看下狀態轉換流程

  •  首先 driver program 需要使用 rdd.checkpoint() 去設定哪些 rdd 需要 checkpoint,設定後,該 rdd 就接受 RDDCheckpointData 管理。用戶還要設定 checkpoint 的存儲路徑,一般在 HDFS 上。
  • marked for checkpointing:初始化後,RDDCheckpointData 會將 rdd 標記爲 MarkedForCheckpoint。
  • checkpointing in progress:每個 job 運行結束後會調用 finalRdd.doCheckpoint(),finalRdd 會順着 computing chain 回溯掃描,碰到要 checkpoint 的 RDD 就將其標記爲 CheckpointingInProgress,然後將寫磁盤(比如寫 HDFS)需要的配置文件(如 core-site.xml 等)broadcast 到其他 worker 節點上的 blockManager。完成以後,啓動一個 job 來完成 checkpoint(使用 rdd.context.runJob(rdd, CheckpointRDD.writeToFile(path.toString, broadcastedConf)))。
  • checkpointed:job 完成 checkpoint 後,將該 rdd 的 dependency 全部清掉,並設定該 rdd 狀態爲 checkpointed。然後,爲該 rdd 強加一個依賴,設置該 rdd 的 parent rdd 爲 CheckpointRDD,該 CheckpointRDD 負責以後讀取在文件系統上的 checkpoint 文件,生成該 rdd 的 partition。

checkpoint 讀流程

如果一個RDD 我們已經 checkpoint了那麼是什麼時候用呢, checkpoint 將 RDD 持久化到 HDFS 或本地文件夾,如果不被手動 remove 掉,是一直存在的,也就是說可以被下一個 driver program 使用。 比如 spark streaming 掛掉了, 重啓後就可以使用之前 checkpoint 的數據進行 recover (這個流程我們在下面一篇文章會講到) , 當然在同一個 driver program 也可以使用。 我們講下在同一個 driver program 中是怎麼使用 checkpoint 數據的。

如果 一個 RDD 被checkpoint了, 如果這個 RDD 上有 action 操作時候,或者回溯的這個 RDD 的時候,這個 RDD 進行計算的時候,裏面判斷如果已經 checkpoint 過, 對分區和依賴的處理都是使用的 RDD 內部的 checkpointRDD 變量。

具體細節如下,

如果 一個 RDD 被checkpoint了, 那麼這個 RDD 中對分區和依賴的處理都是使用的 RDD 內部的 checkpointRDD 變量, 具體實現是 ReliableCheckpointRDD 類型。 這個是在 checkpoint 寫流程中創建的。依賴和獲取分區方法中先判斷是否已經checkpoint, 如果已經checkpoint了, 就斬斷依賴, 使用ReliableCheckpointRDD, 來處理依賴和獲取分區。

如果沒有,才往前回溯依賴。 依賴就是沒有依賴, 因爲已經斬斷了依賴, 獲取分區數據就是讀取 checkpoint 到 hdfs目錄中不同分區保存下來的文件。

整個 checkpoint 讀流程就完了。

 

在以下兩種情況下,RDD需要加檢查點。

  1. DAG中的Lineage過長,如果重算,則開銷太大(如在PageRank中)。
  2. 在寬依賴上做Checkpoint獲得的收益更大。

由於RDD是隻讀的,所以Spark的RDD計算中一致性不是主要關心的內容,內存相對容易管理,這也是設計者很有遠見的地方,這樣減少了框架的複雜性,提升了性能和可擴展性,爲以後上層框架的豐富奠定了強有力的基礎。
在RDD計算中,通過檢查點機制進行容錯,傳統做檢查點有兩種方式:通過冗餘數據和日誌記錄更新操作。在RDD中的doCheckPoint方法相當於通過冗餘數據來緩存數據,而之前介紹的血統就是通過相當粗粒度的記錄更新操作來實現容錯的。

檢查點(本質是通過將RDD寫入Disk做檢查點)是爲了通過lineage做容錯的輔助,lineage過長會造成容錯成本過高,這樣就不如在中間階段做檢查點容錯,如果之後有節點出現問題而丟失分區,從做檢查點的RDD開始重做Lineage,就會減少開銷。

發佈了25 篇原創文章 · 獲贊 14 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章