暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

基于Spark RDD对Spark Task的运行分析

144

     目前网络上以及书籍里,对RDD的分析停留在RDD本身,对于Task的分析依然停留在DAG Scheduler, Task Scheduler这样高级调度器,低级调度器的层面。但是很少有把Task的运行和RDD的结构结合起来进行分析的。这样在宏观上确实是可以说清楚例如宽依赖,窄依赖如何区别,DAG中的Stage如何划分。但是对于划分好的Stage中TaskSet如何运行,也就是说如何通过一个Stage内窄依赖,由输入的RDD一层一层得到输出的RDD这块的运算流转逻辑却很少提及。从而造成宏观上的运行机制一想就通,微观上的运行链条总是觉得哪里有问题。[注: DAG Stage是依据RDD的宽依赖为划分标准,通过广度优先搜索以及嵌套递归的方式来划分的]

    本文结合网络上看的一些技术帖子以及部分Spark源代码,从SparkRDD的角度出发,简单分析一下Spark Task的运行。

(1)首先还是从DAGScheduler,  TaskScheduler出发,看一下每一个Stage中任务是如何触发的。[注: 这里只是从逻辑上阐述一下Stage的划分,没过去深入探究Stage划分中的广度优先以及递归等算法.

     先是根据RDD之间的依赖关系划分Stage, 以宽依赖作为划分的依据。如下图所示:

而这三个Stage是如何描述的呢?

private[scheduler] abstract class Stage(
    val id: Int,
    val rdd: RDD[_],
    val numTasks: Int,
    val parents: List[Stage],
    val firstJobId: Int,
    val callSite: CallSite)
  extends Logging {...


我们看一下Stage的抽象类,发现其中的参数有rdd, numTasks, parent,这里的rdd就是每一个stage最后的一个RDD, 为什么呢? 这个是由Stage切分算法决定的,从后往前推,一个宽依赖的父RDD就是是新的Stage中最后的RDD。

如果图中Stage2的RDD就是RDD F

在spark调用action的时候DAG图从前往后进行计算,例如要计算Stage3则先要计算Stage1和Stage2.

以Stage2的提交计算举例:

而在提交计算Stage2的时候,其对应的RDD就是RDD F,其中还有4个partition, 所以其对应的TaskSet中有4个Task. 在给每个task分配具体的core之后,会将每个task对应的TaskDescription发送到Executor上,然后被反序列之后进行执行(注意提交的时候是一个task一个task的提交,所以executor上反序列化后的也是一个task一个task的执行)

  

在ShuffleMapTask的runTask中有如下代码:


override def runTask(context: TaskContext): MapStatus = {
  // Deserialize the RDD using the broadcast variable.
  val deserializeStartTime = System.currentTimeMillis()
  val ser = SparkEnv.get.closureSerializer.newInstance()
  val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
    ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
  _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime

  var writer: ShuffleWriter[Any, Any] = null
  try {
    val manager = SparkEnv.get.shuffleManager
    writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
    writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])

    writer.stop(success = true).get
  } catch {
    case e: Exception =>
      try {
        if (writer != null) {
          writer.stop(success = false)
        }
      } catch {
        case e: Exception =>
          log.debug("Could not stop writer", e)
      }
      throw e
  }
}


如上图,高亮部分,通过SparkEnv拿到ShuffleManager,从而拿到ShuffleWriter(例如SortShuffleWriter)。writer.write负责计算结果的缓存处理以及持久化,所以其参数

rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]] 就是某个Task计算的结果。

下面我们就从SparkRDD的角度来看一下,task的计算:

注意writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])中的参数rdd, 这个rdd是,SparkEnv.get.closureSerializer.newInstance()反序列化后得到的。其中的dep是本stage对应的shuffle依赖,也就是是否需要从上一个stage的持久化的shuffle文件中读数据。


(2)

通过上面的图我们可以看到,一个Stage里的task可能会跨越好几个窄依赖的RDD,例如从hdfs中读取文件,然后经过map操作,再经过filter操作,例如:

var textRDD = sc.textFile(“xxx”)

var result = textRDD.map(word = > (word, 1)).filter(xxxxxx)

这样一个特定task,例如ShuffleMapTask 是和一个特定的数据分片想对应的,有就是一个partition。

一个Stage的TaskSets对应着一系列数据分片不同但是数据流转算法相同的的task

taskSets中的每一个task是与一个RDD的一个partition相对应的。

换句话说Driver会把一个stage中的TaskSet分配好资源之后全部发送给Executor, 每个Task对应的partiton不同。在Executor上反序列化之后的Task是包含有对应的partition的信息的。


rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]] 就是某个Task计算的结果。


下来我们看一下RDD里相关的方法。

在抽象类RDD中(参见RDD.scala)

这里需要说明一下,RDD是一种抽象的概念,是一个对分布式数据的操作管理的集合,其本身并不代表数据本身。数据本身是在RDD映射下的流转。

有如下的虚函数:

/**
 * :: DeveloperApi ::
 * Implemented by subclasses to compute a given partition.
 */
@DeveloperApi
def compute(split: Partition, context: TaskContext): Iterator[T]

具体特定的RDD会覆写这个方法。例如MapPartitionsRDD,HadoopRDD, UnionRDD

等等(参见package org.apache.spark.rdd)

通过上面论述可以得知在提交Stage2中高亮部分的代码对应的是RDD F(stage2通过宽依赖划分的最后一个RDD).

那接着我们来看

rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]]。

我们假设RDD F是一个UnionRDD



则rdd.iterator先调用其父类RDD中的

/**
 * Internal method to this RDD; will read from cache if applicable, or otherwise compute it.
 * This should ''not'' be called by users directly, but is available for implementors of custom
 * subclasses of RDD.
 */
final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  if (storageLevel != StorageLevel.NONE) {
    getOrCompute(split, context)
  } else {
    computeOrReadCheckpoint(split, context)
  }
}

我们先不考虑有Checkpoint的情况,所以直接来看getOrCompute

/**
 * Gets or computes an RDD partition. Used by RDD.iterator() when an RDD is cached.
 */
private[spark] def getOrCompute(partition: Partition, context: TaskContext): Iterator[T] = {
  val blockId = RDDBlockId(id, partition.index)
  var readCachedBlock = true
  // This method is called on executors, so we need call SparkEnv.get instead of sc.env.
  SparkEnv.get.blockManager.getOrElseUpdate(blockId, storageLevel, elementClassTag, () => {
    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]])
  }
}


看高亮部分

/**
 * Compute an RDD partition or read it from a checkpoint if the RDD is checkpointing.
 */
private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
{
  if (isCheckpointedAndMaterialized) {
    firstParent[T].iterator(split, context)
  } else {
    compute(split, context)
  }
}


最终会调用compute(split, context),而compute是子类UnionRDD提供的,

UnionRDD.scala中


override def compute(s: Partition, context: TaskContext): Iterator[T] = {
  val part = s.asInstanceOf[UnionPartition[T]]
  parent[T](part.parentRddIndex).iterator(part.parentPartition, context)
}


这里可以看到F1的4个task任务,前两个会调用RDD D的iterator, 后两个会调用RDD E的iterator, 然后没有做其他任何工作


RDD D是一个MapPartitionsRDD, RDD E例如是一个HadoopRDD。整个方法调用顺序和上面类似,实时HadoopRDD的compute方法不需要再调用其依赖的RDD的iterator方法了,因为其就是数据的开始来源。而RDD D


的compute是MapPartitionsRDD提供的,

MapPartitionsRDD.scala中


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

类的这RDD D的compute方法会调用RDD C的iterator方法,同理一个task映射一个partition。得到结果后用f来做一个map映射。

RDD C的iterator方法,如果C也是一个数据的源头则其肯定自己提供了一个读数据的compute方法。如果其实一个shuffle过程的mapper开始的数据,则其实一个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)]]
}


调用了shuffleManager.getReader从shuffle文件中读取数据。


通过上述分析,可以看到

(1)一个Stage中的task的计算过程实质是RDD的compute方法的链式调用的过程,是从按照宽依赖划分的最后一个RDD开始一直向父RDD的compute进行调用的过程。

(2)一个Stage里的Task Set中的一个task是个这个Stage中所有的RDD的算子相对应的,一个task需要串型调用这些RDD的compute方法。

(3)Spark中最细粒度,也就是实质性的计算都是定义在RDD中的,通过外界传入的参数,在集群的Executor中会去对特定的数据分片(对应特定的Task)执行RDD中的算子。


加入技术讨论群





《大数据和云计算技术》社区群人数已经3000+,欢迎大家加下面助手微信,拉大家进群,自由交流。

喜欢QQ群的,可以扫描下面二维码:

欢迎大家通过二维码打赏支持技术社区(英雄请留名,社区感谢您,打赏次数超过108+):

文章转载自数据库和 AI 技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论