Spark技术内幕笔记 RDD计算执行

RDD在经过转换之后,会出发一个动作生成Job。从而产生一批计算任务在Executor执行。

Task分为两种类型:
org.apache.spark.scheduler.ShuffleMapTask
org.apache.spark.scheduler.ResultTask

DAG在最后的阶段会给每个Partition生成一个ResultTask,其他阶段生成ShuffleMapTask。

Task执行

Task的执行会调用Task的runTask,runTask会获得RDD的iterator。

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
	if(storageLevel != StorageLevel.NONE) {
	// 如果存储级别不是 NONE , 那么先检查是否有缓存; 没有缓存则要进行计算
		SparkEnv.get.cacheManager.getOrCompute(this, split, context,storageLevel)
	} else { // 如果有 checkpoint , 那么直接读取结果; 否则直接进行计算
		computeOrReadCheckpoint(split, context)
	}
}

CacheManager管理RDD的缓存,因此可以获知该RDD是否已计算过并缓存。

缓存处理

要判断RDD是否需要计算,会先检查缓存,如果缓存不存在则需要计算。
具体步骤:
cacheManager会通过RDD的ID和当前计算的Partition的ID向Storage模块的BlockManager发起查询请求, 如果能够获得Block的信息, 会直接返回Block的信息,这里先在缓存查找再检查checkpoint。 否则, 代表该RDD是需要计算的。 这个RDD以前可能计算过并且被存储到了内存中, 但是后来由于内存紧张, 这部分内存被清理了。 在计算结束后, 计算结果会根据用户定义的存储级别, 写入BlockManager中。 这样, 下次就可以不经过计算而直接读取该RDD的计算结果了。

代码实现:

    def getOrCompute[T](rdd: RDD[T],
                         partition: Partition,
                         context: TaskContext,
                         storageLevel: StorageLevel): Iterator[T] = {
      // 获取 RDD 的 BlockId
      val key = RDDBlockId(rdd.id, partition.index)
      logDebug(s"Looking for partition $key")
      blockManager.get(key) match { // 向 BlockManager 查询是否有缓存
        case Some(blockResult) => // 缓存命中
          // 更新统计信息, 将缓存作为结果返回
          context.taskMetrics.inputMetrics = Some(blockResult.inputMetrics)
          new InterruptibleIterator(context, blockResult.data.asInstanceOf[Iterator[T]])
        case None => // 没有缓存命中, 需要计算
          // 判断当前是否有线程在处理当前的 Partition , 如果有那么等待它结束后, 直接从 Block
          // Manager 中读取处理结果如果没有线程在计算, 那么 storedValues 就是 None , 否则
          // 就是计算的结果
          val storedValues = acquireLockForPartition[T](key)
          if (storedValues.isDefined) { // 已经被其他线程处理了, 直接返回结果
            return new InterruptibleIterator[T](context, storedValues.get)
          } /
            / 需要计算
          try {
            // 如果被 checkpoint 过, 那么读取 checkpoint 的数据; 否则调用 rdd 的 compute() 开始
            // 计算val computedValues = rdd.computeOrReadCheckpoint(partition,context)
            // Task 是在 Driver 端执行的话就不需要缓存结果, 这个主要是为了 first() 或者 take()
            // 这种仅仅有一个执行阶段的任务的快速执行。 这类任务由于没有 Shuffle 阶段, 直接运行
            // 在 Driver 端可能会更省时间
            if (context.isRunningLocally) {
              return computedValues
            } /
              / 将计算结果写入到 BlockManager
            val updatedBlocks = new ArrayBuffer[(BlockId, BlockStatus)]
            val cachedValues =
              putInBlockManager(key, computedValues, storageLevel, updatedBlocks)
            // 更新任务的统计信息
            val metrics = context.taskMetrics
            val lastUpdatedBlocks = metrics.updatedBlocks.getOrElse(
              Seq[(BlockId, BlockStatus)]())
            metrics.updatedBlocks = Some(lastUpdatedBlocks ++ updatedBlocks.toSeq)
            new InterruptibleIterator(context, cachedValues)
          } finally {
            loading.synchronized {
              loading.remove(key)
              // 如果有其他的线程在等待该 Partition 的处理结果, 那么通知它们计算已经完成, 结果已
              // 经存到 BlockManager 中(注意前面那类不会写入 BlockManager 的本地任务)
              // loading.notifyAll()
            }
          }
      }
    }

从检查点获取

首先在Job结束后, 会判断是否需要checkpoint。 如果需要, 就调用org.apache.spark.rdd.RDDCheckpointData#doCheckpoint。
doCheckpoint首先为数据创建一个目录; 然后启动一个新的Job来计算, 并且将计算结果写入新创建的目录; 接着创建一个org.apache.spark.rdd.CheckpointRDD; 最后, 原始RDD的所有依赖被清除, 这就意味着RDD的转换的计算链(compute chain) 等信息都被清除。

实现代码

// 创建一个保存 checkpoint 数据的目录
val path = new Path(rdd.context.checkpointDir.get, "rdd-" + rdd.id)
val fs = path.getFileSystem(rdd.context.hadoopConfiguration)
if (!fs.mkdirs(path)) {
	throw new SparkException("Failed to create checkpoint path " + path)
} /
/ 创建广播变量
val broadcastedConf = rdd.context.broadcast(
new SerializableWritable(rdd.context.hadoopConfiguration))
// 开始一个新的 Job 进行计算, 计算结果存入路径 path 中
rdd.context.runJob(rdd, CheckpointRDD.writeToFile[T](path.toString, broadcastedConf) _)
// 根据结果的路径 path 来创建 CheckpointRDD
val newRDD = new CheckpointRDD[T](rdd.context, path.toString)
// 保存结果, 清除原始 RDD 的依赖、 Partition 信息等
RDDCheckpointData.synchronized {
	cpFile = Some(path.toString)
	cpRDD = Some(newRDD) // RDDCheckpointData 对应的 CheckpointRDD
	rdd.markCheckpointed(newRDD) // 清除原始 RDD 的依赖, Partition
	cpState = Checkpointed // 标记 checkpoint 的状态为完成
 }

RDD的dependencies属性,表示RDD是否已经checkpoint,如果有,则其依赖为CheckPointRDD。
代码:

  private def checkpointRDD: Option[RDD[T]]=checkpointData.flatMap(_.checkpointRDD)
  final def dependencies: Seq[Dependency[_]] = {
    checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {
      if (dependencies_ == null) { // 没有 checkpointdependencies_ = getDependencies
      } 
      dependencies_
    }
  }

在RDD的iterator和CacheManager的getOrCompute方法都会经过如上步骤。
方法如下:

扫描二维码关注公众号,回复: 8712778 查看本文章
private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext)
: Iterator[T] =
{
//这里iterator
if (isCheckpointed) 
	firstParent[T].iterator(split, context) else compute(split, context)
}

RDD计算实现

每个特定的RDD都会实现compute。 比如前面提到的CheckpointRDD的compute就是直接读取checkpoint数据。 HadoopRDD就是读取指定Partition的数据。 MapPartitionsRDD就是将用户的转换逻辑作用到指定的Partition上。

发布了79 篇原创文章 · 获赞 3 · 访问量 5262

猜你喜欢

转载自blog.csdn.net/SW_LCC/article/details/102773532