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 端进行聚合或排序

 

 

 

 

 

 

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