1004RDD分析二

RDD有與調度系統相關的 API 還提供了很多其他類型的 API包括對 RDD 進行轉換的 API對 RDD 進行計算動作的 API 及 RDD 檢查點相關的 API轉換 API 裏的計算是延遲的也就是說調用轉換 API 不會向 Spark 集羣提交 Job更不會執行轉換計算只有調用了動作 API纔會提交 Job 並觸發對轉換計算的執行

轉換 API

轉換transform是指對現有 RDD 執行某個函數後轉換爲新的 RDD 的過程轉換前的 RDD 與轉換後的 RDD 之間具有依賴和血緣關係RDD 的多次轉換將創建出多個 RDD這些 RDD 構成了一張單向依賴的圖也就是 DAG

mapPartitions

mapPartitions 方法用於將 RDD 轉換爲 MapPartitionsRDD其實現如代碼清單 

def mapPartitions[U: ClassTag](
    f: Iterator[T] => Iterator[U],
    preservesPartitioning: Boolean = false): RDD[U] = withScope {
  val cleanedF = sc.clean(f)
  new MapPartitionsRDD(
    this,
    (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(iter),
    preservesPartitioning)
}

 

爲便於理解這裏假設函數 f 的作用是過濾出大於 0 的數字那麼 mapPartitions 方法的執行可以用圖表示

 

mapPartitionsWithIndex

mapPartitionsWithIndex 方法用於創建一個將與分區索引相關的函數應用到 RDD 的每個分區的 MapPartitionsRDD

def mapPartitionsWithIndex[U: ClassTag](
    f: (Int, Iterator[T]) => Iterator[U],
    preservesPartitioning: Boolean = false): RDD[U] = withScope {
  val cleanedF = sc.clean(f)
  new MapPartitionsRDD(
    this,
    (context: TaskContext, index: Int, iter: Iterator[T]) => cleanedF(index, iter),
    preservesPartitioning)
}

mapPartitionsWithIndex mapPartitions 相似區別在於多接收分區索引的參數我們假設函數 f 的作用是將每個分區的數字累加並且與分區索引以逗號分隔輸出那麼 mapPartitionsWithIndex 方法的執行可以用圖表示

mapPartitionsWithIndexInternal

mapPartitionsWithIndexInternal 方法用於創建一個將函數應用到 RDD 的每個分區的 MapPartitionsRDD由於此方法是私有的所以只在Spark SQL 內部使用

private[spark] def mapPartitionsWithIndexInternal[U: ClassTag](
    f: (Int, Iterator[T]) => Iterator[U],
    preservesPartitioning: Boolean = false): RDD[U] = withScope {
  new MapPartitionsRDD(
    this,
    (context: TaskContext, index: Int, iter: Iterator[T]) => f(index, iter),
    preservesPartitioning)
}

flatMap

flatMap 方法用於向 RDD 中的所有元素應用函數並對結果扁平化處理

def flatMap[U: ClassTag](f: T => TraversableOnce[U]): RDD[U] = withScope {
  val cleanF = sc.clean(f)
  new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.flatMap(cleanF))
}

flatMap 方法也將返回 MapPartitionsRDD我們假設函數 f 的作用是將給每個數字加上 5那麼 flatMap 方法的執行可以用圖表示

map

map 方法用於向 RDD 中的所有元素應用函數

def map[U: ClassTag](f: T => U): RDD[U] = withScope {
  val cleanF = sc.clean(f)
  new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}

我們假設函數 f 的作用是將給每個數字加上 5那麼 map 方法的執行可以用圖表示

toJavaRDD

toJavaRDD 方法用於將 RDD 自己轉換爲 JavaRDD其實現如所示

def toJavaRDD() : JavaRDD[T] = {
  new JavaRDD(this)(elementClassTag)
}

動作 API

由於轉換 API 都是預先編織好但是不會執行的所以 Spark 需要一些 API 來觸發對轉換的執行動作 API 觸發對數據的轉換後將接收到一些結果數據動作 API 因此還具備對這些數據進行收集遍歷疊加的功能

collect

collect 方法將調用 SparkContext 的 runJob 方法提交基於 RDD 的所有分區上的作業並返回數組形式的結果

def collect(): Array[T] = withScope {
  val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
  Array.concat(results: _*)
}

foreach

foreach 方法將調用 SparkContext 的 runJob 方法提交將函數應用到 RDD 中所有元素的作業

def foreach(f: T => Unit): Unit = withScope {
  val cleanF = sc.clean(f)
  sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}

reduce

reduce 方法按照指定的函數對 RDD 中的元素進行疊加操作

def reduce(f: (T, T) => T): T = withScope {
  val cleanF = sc.clean(f)
  val reducePartition: Iterator[T] => Option[T] = iter => {
    if (iter.hasNext) {
      Some(iter.reduceLeft(cleanF))
    } else {
      None
    }
  }
  var jobResult: Option[T] = None
  val mergeResult = (index: Int, taskResult: Option[T]) => {
    if (taskResult.isDefined) {
      jobResult = jobResult match {
        case Some(value) => Some(f(value, taskResult.get))
        case None => taskResult
      }
    }
  }
  sc.runJob(this, reducePartition, mergeResult)
  jobResult.getOrElse(throw new UnsupportedOperationException("empty collection"))
}

爲了便於說明 reduce 的作用這裏假設函數 f 的定義是LR=>L+R那麼可以用圖來表示 reduce 的效果

 

 

 

檢查點 API 的實現分析

RDD 中提供了很多與檢查點相關的 API通過對這些 API 的使用Spark 應用程序才能夠啓用保存及使用檢查點提高應用程序的容災和容錯能力

檢查點的啓用

用戶提交的 Spark 作業必須主動調用 RDD 的 checkpoint 方法纔會啓動檢查點功能

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))
  }
}

SparkContext 指定 checkpointDir 是啓用檢查點機制的前提可以使用 SparkContext setCheckpointDir 方法設置checkpointDir如果沒有指定 RDDCheckpointData那麼創建ReliableRDDCheckpointData

檢查點的保存

RDD 的 doCheckpoint 方法用於將 RDD 的數據保存到檢查點由於此方法是私有的只能在 RDD 內部使用

private[spark] def doCheckpoint(): Unit = {
  RDDOperationScope.withScope(sc, "checkpoint", allowNesting = false, ignore-Parent = true) {
    if (!doCheckpointCalled) {
      doCheckpointCalled = true
      if (checkpointData.isDefined) {
        if (checkpointAllMarkedAncestors) {
          dependencies.foreach(_.rdd.doCheckpoint())
        }
        checkpointData.get.checkpoint()
      } else {
        dependencies.foreach(_.rdd.doCheckpoint())
      }
    }
  }
}

doCheckpoint 方法的執行步驟如下。

  1. 如果 checkpointData 中保存了 RDDCheckpointData,調用RDDCheckpointData 的 checkpoint 方法(見代碼清單 10-11)保存檢查點。如果需要對祖先 RDD 保存檢查點,那麼還會調用每個依賴的 RDD 的doCheckpoint 方法。由於在啓用檢查點時,保存到 check-pointData 中的是RDDCheckpointData 的子類 ReliableRDDCheckpointData,因此RDDCheck-pointData 的 checkpoint 方法中將調用ReliableRDDCheckpointData 的 doCheckpoint 方法(見代碼清單 10-14)。
  2. 如果 checkpointData 中沒有保存 RDDCheckpointData,那麼調用每個依賴的 RDD 的 doCheckpoint 方法。

檢查點的使用

曾介紹過獲取 RDD 的分區數組的 partitions 方法、獲取指定分區的偏好位置的 preferredLocations 方法、獲取當前 RDD 的所有依賴的 dependencies 方法。雖然這幾個方法的作用不同,但是實現方式卻是類似的,即首先從 RDD 關聯的 CheckpointRDD 中查找對應信息。

根據對檢查點的啓用和保存的分析,負責爲 RDD 提供檢查點服務的實際是 Reli-ableCheckpointRDD。因此當調用 RDD 的 partitions 方法時,會首先調用ReliableCheck-pointRDD 的 partitions 方法,進而調用 ReliableCheckpointRDD 的 getPartitions 方法,最後才調用 RDD 自己的 getPartitions 方法。當調用 RDD 的 preferred-Locations 方法時,首先會調用ReliableCheckpointRDD 的 getPreferredLocations 方法,當調用 RDD 的 dependencies 方法時,首先會嘗試將 ReliableCheck-pointRDD 封裝爲 OneToOneDependency。

除了以上場景外,對 RDD 的迭代計算也涉及對檢查點的使用,其中將調用 Reliable-CheckpointRDD 的 compute 方法。迭代計算的內容將在下一小節介紹。

 

迭代計算

分析 ShuffleMapTask ResultTask 的 runTask 方法時已經看到Task 的執行離不開對 RDD 的 iterator 方法的調用RDD 的 iterator 方法是迭代計算的入口

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  if (storageLevel != StorageLevel.NONE) {
    getOrCompute(split, context)
  } else {
    computeOrReadCheckpoint(split, context)
  }
}

iterator 方法的執行步驟如下。

  1. 如果 RDD 的存儲級別(StorageLevel)不是 NONE,那麼根據對StorageLevel 的分析,StorageLevel 的構造器是私有的。這些內置的存儲級別除 NONE 外,至少會使用磁盤、堆內內存、堆外內存三者之一,因此可以調用 getOrCompute 方法從這些存儲中嘗試獲取計算結果。
  2. 如果 RDD 的存儲級別(StorageLevel)是 NONE,那麼說明分區任務可能是初次執行且存儲中還沒有任務的執行結果,所以會調用computeOrReadCheckpoint 方法計算或者從檢查點恢復。

小貼士: 這裏需要說說 iterator 方法的容錯處理過程如果某個分區任務執行失敗但是其他分區任務執行成功可以利用 DAGScheduler 對 Stage 重新調度失敗的分區任務將從檢查點恢復狀態而那些執行成功的分區任務由於其執行結果已經緩存到存儲體系所以調用 getOrCompute 方法獲取即可不需要再次執行

 

private[spark] def getOrCompute(partition: Partition, context: TaskContext): Iterator[T] = {
  val blockId = RDDBlockId(id, partition.index)
  var readCachedBlock = true
  SparkEnv.get.blockManager.getOrElseUpdate(blockId, storageLevel, elementClass-Tag, () => {
    readCachedBlock = false
    computeOrReadCheckpoint(partition, context)
  }) match {
    case Left(blockResult) =>
      if (readCachedBlock) {
        val existingMetrics = context.taskMetrics().inputMetrics
        existingMetrics.incBytesRead(blockResult.bytes)
        new InterruptibleIterator[T](context, blockResult.data.asInstanceOf[Iterator-[T]]) {
          override def next(): T = {
            existingMetrics.incRecordsRead(1)
            delegate.next()
          }
        }
      } else {
        new InterruptibleIterator(context, blockResult.data.asInstanceOf[Iterator[T]])
      }
    case Right(iter) =>
      new InterruptibleIterator(context, iter.asInstanceOf[Iterator[T]])
  }
}

getOrCompute 方法的執行步驟如下。

  1. 調用 BlockManager 的 getOrElseUpdate 方法(見代碼清單 6-81)先嚐試從存儲體系中獲取 RDD 分區的 Block,否則調用 computeOrReadCheckpoint 方法從檢查點讀取或計算。
  2. 對 getOrElseUpdate 方法返回的結果進行匹配,將返回的 BlockResult 的 data 屬性或返回的 Iterator 封裝爲 InterruptibleIterator。

computeOrReadCheckpoint 方法在存在檢查點時直接從檢查點讀取數據否則需要調用 compute 繼續計算computeOrReadCheckpoint 方法的實現如下所示

private[spark] def computeOrReadCheckpoint(split: Partition, context: Task-Context): Iterator[T] =
{
  if (isCheckpointedAndMaterialized) {
    firstParent[T].iterator(split, context)
  } else {
    compute(split, context)
  }
}

private[spark] def isCheckpointedAndMaterialized: Boolean =
checkpointData.exists(_.isCheckpointed)

computeOrReadCheckpoint 方法的執行步驟如下。

  1. 如果 checkpointData 中保存了 RDDCheckpointData 且其檢查點的狀態(cpState)是 Checkpointed,那麼調用 firstParent 方法找到其父 RDD,然後調用父 RDD 的 iterator 方法。由於 firstParent 中調用了dependencies,且當前 RDD 的父 RDD 實際是 ReliableCheckpointRDD,那麼對 ReliableCheckpointRDD 的 iterator 方法的調用最終將轉變爲對ReliableCheckpointRDD 的 compute 方法的調用,從而從檢查點文件讀取之前保存的計算結果。
  2. 如果 checkpointData 中沒有保存 RDDCheckpointData 或其檢查點的狀態(cpState)不是 Checkpointed,那麼調用 compute 方法進行計算。

找到父親 RDD

protected[spark] def firstParent[U: ClassTag]: RDD[U] = {
  dependencies.head.rdd.asInstanceOf[RDD[U]]
}

每個 RDD 實現的 compute 方法都不相同。曾經介紹了Reliable-CheckpointRDD 的 compute 方法。此處再以 MapPartitionsRDD 和ShuffledRDD 爲例,來看看它們各自實現的 compute 方法。

MapPartitionsRDD 實現的 compute 方法如下所示。

override def compute(split: Partition, context: TaskContext): Iterator[U] =
  f(context, split.index, firstParent[T].iterator(split, context))

ShuffledRDD 實現的 compute 方法如代碼下所示

 

override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
  val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
  SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context)
    .read()
    .asInstanceOf[Iterator[(K, C)]]
}

可以看到ShuffledRDD 的 compute 方法首先調用 SortShuffleManager getReader 方法獲取 BlockStoreShuffleReader然後調用BlockStoreShuffleReader 的 read 方法獲取 map 任務輸出的 Block 並在 reduce 端進行聚合或排序

 

 

 

 

 

 

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