Spark原始碼分析之Checkpoint機制

happy19870612發表於2017-11-11

對於一個複雜的RDD,我們如果擔心某些關鍵的,會在後面反覆使用的RDD,可能會因為節點的故障,導致持久化資料的丟失,就可以針對該RDD啟動checkpoint機制,實現容錯和高可用。

 

在進行checkpoint之前,最好先對RDD執行持久化操作,比如persist(StorageLevel.DISK_ONLY)如果持久化了,就不用再重新計算;否則如果沒有持久化RDD,還設定了checkpoint,那麼本來job都結束了,但是由於中間的RDD沒有持久化,那麼checkpoint job想要將RDD資料寫入外部檔案系統,還得從RDD之前的所有的RDD全部重新計算一次,再進行checkpoint。然後從持久化的RDD磁碟檔案讀取資料

 

 

一 RDD的checkpoint方法

# 如果SparkContext沒有設定checkpointDir,則丟擲異常

# 如果設定了,則建立RDDCheckpointData,這個類主要負責管理RDD的checkpoint的程式和狀態等

# 建立RDDCheckpointData的時候,會初始化checkpoint狀態為Initialized

 

def checkpoint(): Unit = RDDCheckpointData.synchronized {
  if (context.checkpointDir.isEmpty) {
    throw new SparkException("Checkpoint directory has not been set in the SparkContext")
  } else if (checkpointData.isEmpty) {
    checkpointData = Some(new ReliableRDDCheckpointData(this))
  }
}
 

二persist 持久化RDD

# 如果該RDD已經有了storage level,但是還和指定的storage level不相等,那麼丟擲異常,不支援在一個RDD分配了storage level之後再分配一個storage level

# 標記這個RDD為persisting

# 設定RDD的storage level

private def persist(newLevel:StorageLevel, allowOverride: Boolean):this.type = {
  if (storageLevel!= StorageLevel.NONE&& newLevel != storageLevel&& !allowOverride) {
    throw new UnsupportedOperationException(
      "Cannotchange storage level of an RDD after it was already assigned a level")
  }
  // If this isthe first time this RDD is marked for persisting, register it
  // with the SparkContext for cleanupsand accounting. Do this only once.
 
if (storageLevel== StorageLevel.NONE) {
    sc.cleaner.foreach(_.registerRDDForCleanup(this))
    sc.persistRDD(this)
  }
  storageLevel = newLevel
 
this
}

 

三 RDD的doCheckpoint方法

當呼叫DAGScheduler的runJob的時候,開始呼叫RDD的doCheckpoint方法

# 該rdd是否已經呼叫doCheckpoint,如果還沒有,則開始處理

# 檢視是否需要把該rdd的所有依賴即血緣全部checkpoint,如果需要,血緣上的每一個rdd遞迴呼叫該方法

# 呼叫RDDCheckpointData的checkpoint方法

private[spark] def doCheckpoint(): Unit = {
  RDDOperationScope.withScope(sc, "checkpoint", allowNesting = false, ignoreParent = true) {
    // rdd是否已經呼叫doCheckpoint,如果還沒有,則開始處理
    if (!doCheckpointCalled) {
      doCheckpointCalled = true
      // 判斷RDDCheckpointData是否已經定義了,如果已經定義了
      if (checkpointData.isDefined) {
        // 檢視是否需要把該rdd的所有依賴即血緣全部checkpoint
        if (checkpointAllMarkedAncestors) {
          // 血緣上的每一個rdd遞迴呼叫該方法
          dependencies.foreach(_.rdd.doCheckpoint())
        }
        // 呼叫RDDCheckpointDatacheckpoint方法
        checkpointData.get.checkpoint()
      } else {
        dependencies.foreach(_.rdd.doCheckpoint())
      }
    }
  }
}

 

四RDDCheckpointData的checkpoint

# 將checkpoint的狀態從Initialized置為CheckpointingInProgress

# 呼叫子類的doCheckpoint,建立一個新的CheckpointRDD

# 將checkpoint狀態置為Checkpointed狀態,並且改變rdd之前的依賴,設定父rdd為新建立的CheckpointRDD

final def checkpoint(): Unit = {
  // checkpoint的狀態從Initialized置為CheckpointingInProgress
  RDDCheckpointData.synchronized {
    if (cpState == Initialized) {
      cpState = CheckpointingInProgress
    } else {
      return
    }
  }
  // 呼叫子類的doCheckpoint,我們以ReliableCheckpointRDD為例,建立一個新的CheckpointRDD
  val newRDD = doCheckpoint()

  // checkpoint狀態置為Checkpointed狀態,並且改變rdd之前的依賴,設定父rdd為新建立的CheckpointRDD
  RDDCheckpointData.synchronized {
    cpRDD = Some(newRDD)
    cpState = Checkpointed
    rdd.markCheckpointed()
  }
}

 

五RDDCheckpointData的doCheckpoint

我們以ReliableCheckpointRDD為例,將rdd的資料寫入HDFS中checkpoint目錄,並且建立CheckpointRDD

protected override def doCheckpoint(): CheckpointRDD[T] = {
  // rdd的資料寫入HDFScheckpoint目錄,並且建立CheckpointRDD
  val newRDD = ReliableCheckpointRDD.writeRDDToCheckpointDirectory(rdd, cpDir)

  if (rdd.conf.getBoolean("spark.cleaner.referenceTracking.cleanCheckpoints", false)) {
    rdd.context.cleaner.foreach { cleaner =>
      cleaner.registerRDDCheckpointDataForCleanup(newRDD, rdd.id)
    }
  }

  logInfo(s"Done checkpointing RDD ${rdd.id} to $cpDir, new parent is RDD ${newRDD.id}")
  newRDD
}

 

六ReliableCheckpointRDD的writeRDDToCheckpointDirectory

將rdd的資料寫入HDFS中checkpoint目錄,並且建立CheckpointRDD

def writeRDDToCheckpointDirectory[T: ClassTag](
    originalRDD: RDD[T],
    checkpointDir: String,
    blockSize: Int = -1): ReliableCheckpointRDD[T] = {

  val sc = originalRDD.sparkContext

  // 建立checkpoint輸出目錄
  val checkpointDirPath = new Path(checkpointDir)
  // 獲取HDFS檔案系統API介面
  val fs = checkpointDirPath.getFileSystem(sc.hadoopConfiguration)
  // 建立目錄
  if (!fs.mkdirs(checkpointDirPath)) {
    throw new SparkException(s"Failed to create checkpoint path $checkpointDirPath")
  }

  // 將配置檔案資訊廣播到所有節點
  val broadcastedConf = sc.broadcast(
    new SerializableConfiguration(sc.hadoopConfiguration))
  // 重新啟動一個job,rdd的分割槽資料寫入HDFS
  sc.runJob(originalRDD,
    writePartitionToCheckpointFile[T](checkpointDirPath.toString, broadcastedConf) _)
  // 如果rddpartitioner不為空,則將partitioner寫入checkpoint目錄
  if (originalRDD.partitioner.nonEmpty) {
    writePartitionerToCheckpointDir(sc, originalRDD.partitioner.get, checkpointDirPath)
  }
  // 建立一個CheckpointRDD,該分割槽數目應該和原始的rdd的分割槽數是一樣的
  val newRDD = new ReliableCheckpointRDD[T](
    sc, checkpointDirPath.toString, originalRDD.partitioner)
  if (newRDD.partitions.length != originalRDD.partitions.length) {
    throw new SparkException(
      s"Checkpoint RDD $newRDD(${newRDD.partitions.length}) has different " +
        s"number of partitions from original RDD $originalRDD(${originalRDD.partitions.length})")
  }
  newRDD
}

 

七 RDD的iterator方法

# 當持久化RDD的時候,執行task的時候,會遍歷RDD指定分割槽的資料,在持久的時候,因為指定了storage level,所以我們會呼叫getOrCompute獲取資料,由於第一次還沒有持久化過,所以會先計算。但是資料還沒有被持久化,所以此時先把資料持久化到磁碟(假設持久化時就指定了StorageLevel=DISK_ONLY),然後再把block資料快取到本地記憶體

 

# 進行checkpoint操作時,會啟動一個新的job來處理checkpoint任務。當執行checkpoint的任務來執行RDD的iterator方法時,此時我們知道該RDD的持久化級別不為空,則從BlockManager獲取出結果來,因為已經持久化過了所以不需要進行計算。如果持久化的資料此時已經丟失呢,怎麼辦呢?即storage level為空了,這此時就會呼叫computeOrReadCheckpoint方法,重新計算結果,然後寫入checkpoint目錄

 

# 如果已經持久化和checkpoint了,那麼此時如果有任務在iterator獲取不到block,那麼就會呼叫computeOrReadCheckpoint方法,此時已經物化過了,所以直接從原始RDD對應的父RDD(CheckpointRDD)的iterator方法,此時已經沒有持久化級別,所以CheckpointRDD的iterator方法就會呼叫CheckpointRDD的compute方法從checkpoint檔案讀取資料

 

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  // 如果StorageLevel不為空,表示該RDD已經持久化過了,可能是在記憶體,也有可能是在磁碟,
  // 如果是磁碟獲取的,需要把block快取在記憶體中
  if (storageLevel != StorageLevel.NONE) {
    getOrCompute(split, context)
  } else {
    // 進行rdd partition的計算或者根據checkpoint讀取資料
    computeOrReadCheckpoint(split, context)
  }
}

 

 

八  RDD的computeOrReadCheckpoint方法

# 如果checkpoint狀態已經置為checkpointed了,表示checkpoint已經完成,這時候從checkpoint獲取;如果還是checkpointInProgress,則表示持久化資料丟失,或者根本就沒有持久化,所以需要原來的RDD的compute方法重新計算結果

private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
{
  // 當前rdd是否已經checkpoint和物化了,如果已經checkpoint,則呼叫父類的CheckpointRDDiterator方法獲取
  // 如果沒有則開始計算
  if (isCheckpointedAndMaterialized) {
    firstParent[T].iterator(split, context)
  } else {
    // 則呼叫rddcompute方法開始計算,返回一個Iterator物件
    compute(split, context)
  }
}

 

九CheckpointRDD的compute方法

# 建立checkpoint檔案

#從HDFS上的checkpoint檔案讀取checkpoint過的資料

override def compute(split: Partition, context: TaskContext): Iterator[T] = {
  // 建立checkpoint檔案
  val file = new Path(checkpointPath, ReliableCheckpointRDD.checkpointFileName(split.index))
  // HDFS上的checkpoint檔案讀取checkpoint過的資料
  ReliableCheckpointRDD.readCheckpointFile(file, broadcastedConf, context)
}

 

相關文章