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 的定義是:(L,R)=>(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 方法的執行步驟如下。
- 如果 checkpointData 中保存了 RDDCheckpointData,調用RDDCheckpointData 的 checkpoint 方法(見代碼清單 10-11)保存檢查點。如果需要對祖先 RDD 保存檢查點,那麼還會調用每個依賴的 RDD 的doCheckpoint 方法。由於在啓用檢查點時,保存到 check-pointData 中的是RDDCheckpointData 的子類 ReliableRDDCheckpointData,因此RDDCheck-pointData 的 checkpoint 方法中將調用ReliableRDDCheckpointData 的 doCheckpoint 方法(見代碼清單 10-14)。
- 如果 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 方法的執行步驟如下。
- 如果 RDD 的存儲級別(StorageLevel)不是 NONE,那麼根據對StorageLevel 的分析,StorageLevel 的構造器是私有的。這些內置的存儲級別除 NONE 外,至少會使用磁盤、堆內內存、堆外內存三者之一,因此可以調用 getOrCompute 方法從這些存儲中嘗試獲取計算結果。
- 如果 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 方法的執行步驟如下。
- 調用 BlockManager 的 getOrElseUpdate 方法(見代碼清單 6-81)先嚐試從存儲體系中獲取 RDD 分區的 Block,否則調用 computeOrReadCheckpoint 方法從檢查點讀取或計算。
- 對 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 方法的執行步驟如下。
- 如果 checkpointData 中保存了 RDDCheckpointData 且其檢查點的狀態(cpState)是 Checkpointed,那麼調用 firstParent 方法找到其父 RDD,然後調用父 RDD 的 iterator 方法。由於 firstParent 中調用了dependencies,且當前 RDD 的父 RDD 實際是 ReliableCheckpointRDD,那麼對 ReliableCheckpointRDD 的 iterator 方法的調用最終將轉變爲對ReliableCheckpointRDD 的 compute 方法的調用,從而從檢查點文件讀取之前保存的計算結果。
- 如果 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 端進行聚合或排序。