Spark中几种重要的结构类型

本文深入探讨了Apache Spark中Stage和Task的概念及其内部工作原理。包括Stage的创建、ShuffleMapStage与ResultStage的区别,以及Task的执行流程。此外,还详细介绍了RDD之间的依赖关系及其在Stage划分中的作用。

Stage:

private[spark] abstract class Stage(
    val id: Int,   //Stage的序号数值越大,优先级越高
    val rdd: RDD[_],    //归属于本Stage的最后一个rdd
    val numTasks: Int,     //等于父RDD的输出Partition数目,该初始值来自 DAGScheduler.newOrUsedShuffleStage()中  val numTasks = rdd.partitions.length
    val parents: List[Stage],  //父Stage列表
    val firstJobId: Int,
    val callSite: CallSite)  //作业ID
  extends Logging   

  val numPartitions = rdd.partitions.size   分区数,即本Stage的最后一个rdd的 Partition 数目, why????


ShuffleMapStage :
ShuffleMapStage(
    id: Int,    
    rdd: RDD[_],
    numTasks: Int,   
    parents: List[Stage],   
    firstJobId: Int,
    callSite: CallSite,
    val shuffleDep: ShuffleDependency[_, _, _])
  extends Stage(id, rdd, numTasks, parents, firstJobId, callSite)

来源于 DAGScheduler.handleJobSubmitted 中
    val (parentStages: List[Stage], id: Int) = getParentStagesAndId(rdd, firstJobId)
    val stage = new ShuffleMapStage(id, rdd, numTasks, parentStages, firstJobId, callSite, shuffleDep)

ResultStage(
    id: Int,
    rdd: RDD[_],
    numTasks: Int,
    parents: List[Stage],
    firstJobId: Int,
    callSite: CallSite)
  extends Stage(id, rdd, numTasks, parents, firstJobId, callSite)

RDD:

贴上几种典型的,这类型的子类太多了

HadoopRDD[K, V](
    @transient sc: SparkContext,
    broadcastedConf: Broadcast[SerializableConfiguration],
    initLocalJobConfFuncOpt: Option[JobConf => Unit],
    inputFormatClass: Class[_ <: InputFormat[K, V]],
    keyClass: Class[K],
    valueClass: Class[V],
    minPartitions: Int)
  extends RDD
// 说明: HadoopRDD 来源于 jsc.textFile  而产生


MapPartitionsRDD[U: ClassTag, T: ClassTag](
    prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false)
  extends RDD
// 说明: MapPartitionsRDD来源于rdd.map 、filter 、 flatMap、 glom、mapPartitions、mapPartitionsWithIndex、 mapPartitionsWithContext 这些操作,Transformation操作大部分就是这种rdd。这rdd未实现 getPreferredLocations 方法

ShuffledRDD[K, V, C](
    @transient var prev: RDD[_ <: Product2[K, V]],
    part: Partitioner)
  extends RDD
// 说明: 来源 action动作

Dependency

先列下它的定义及几个子孙类:


@DeveloperApi
abstract class Dependency[T] extends Serializable {
  def rdd: RDD[T]
}

@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  def getParents(partitionId: Int): Seq[Int]
  override def rdd: RDD[T] = _rdd
}

@DeveloperApi
class ShuffleDependency[K, V, C](
    @transient _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Option[Serializer] = None,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None, // 这个变量在shuffle中用到,做combine
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {

  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

  val shuffleId: Int = _rdd.context.newShuffleId()

  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.size, this)

 _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

@DeveloperApi
class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
  extends NarrowDependency[T](rdd) {

  override def getParents(partitionId: Int): List[Int] = {
    if (partitionId >= outStart && partitionId < outStart + length) {
      List(partitionId - outStart + inStart)
    } else {
      Nil
    }
  }
}

Dependency 类用于标示rdd之间的依赖关系。从上面RDD的定义可以看到 deps: Seq[Dependency[_]] 是RDD的主构造参数。那它是如何初始化的呢?流程如下:

以 rdd_A.map(…) 为例:

map()
{
    // 新建 MapPartitionsRDD 对象,将 rdd_A 本身作为第一个参数
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
}

再看 MapPartitionsRDD 的构造 :

class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  
    preservesPartitioning: Boolean = false) extends RDD[U](prev)

MapPartitionsRDD 第一个参数 prev 来自父类 RDD,构造 MapPartitionsRDD 时先调用 super(prev), 即调RDD的构造函数

super(prev) 
=> 调 RDD私有构造函数:
    def this(@transient oneParent: RDD[_]) = this(oneParent.context , List(new OneToOneDependency(oneParent)))
=> 调 RDD主构造函数:
    RDD[T: ClassTag]( @transient private var _sc: SparkContext, @transient private var deps: Seq[Dependency[_]]) 

可以看到封装为List传入RDD主构造函数的第二个参数即其 deps ;所以RDD的deps在执行map等算子时即在生成的子RDD里初始化。

现在再来看下ShuffleDependencyaggregator这个变量
这个变量在 shuffle阶段用到,用来做combine,比较重要。
它的定义为:

case class Aggregator[K, V, C] (
    createCombiner: V => C,
    mergeValue: (C, V) => C,
    mergeCombiners: (C, C) => C) {...}

ShuffleDependency中 aggregator 的产生过程:
在调用RDD的shuffle算子时就会在子RDD中生成 ShuffleDependency成员变量。
流程如下:

rdd.reduceByKey(func) =>
=> rdd.reduceByKey(partitioner, func)
=> combineByKey[V]((v: V) => v, func, func, partitioner)

combineByKey函数的定义如下:(省掉了些七七八八)

def combineByKey[C](createCombiner: V => C,
                      mergeValue: (C, V) => C,
                      mergeCombiners: (C, C) => C,
                      partitioner: Partitioner,
                      mapSideCombine: Boolean = true,
                      serializer: Serializer = null): RDD[(K, C)]
{
    val aggregator = new Aggregator[K, V, C](
          self.context.clean(createCombiner),
          self.context.clean(mergeValue),
          self.context.clean(mergeCombiners))

    if (self.partitioner == Some(partitioner)) {
    }else{
        new ShuffledRDD[K, V, C](self, partitioner)
            .setSerializer(serializer)
            .setAggregator(aggregator)
            .setMapSideCombine(mapSideCombine)
    }
}

从源码来看aggregator的构造参数就来自reduceByKey中的’函数参数’。

Task

任务的最终执行者
父类:

private[spark] abstract class Task[T](
    val stageId: Int,
    val stageAttemptId: Int,
    val partitionId: Int,
    internalAccumulators: Seq[Accumulator[Long]]) extends Serializable
{
    final def run() 
    {
        ...
        (runTask(context), context.collectAccumulators()) // runTask方法为子类实现
        ...
    }
    def runTask(context: TaskContext): T // 抽象方法
}

子类:

private[spark] class ShuffleMapTask(
    stageId: Int,
    stageAttemptId: Int,
    taskBinary: Broadcast[Array[Byte]],   // 该成员为序列化的任务信息
    partition: Partition,
    @transient private var locs: Seq[TaskLocation],
    internalAccumulators: Seq[Accumulator[Long]])
  extends Task[MapStatus](stageId, stageAttemptId, partition.index, internalAccumulators)
 {
    override def runTask(context: TaskContext): MapStatus = {...} 
 }


private[spark] class ResultTask[T, U](
    stageId: Int,
    stageAttemptId: Int,
    taskBinary: Broadcast[Array[Byte]],
    partition: Partition,
    @transient locs: Seq[TaskLocation],
    val outputId: Int,
    internalAccumulators: Seq[Accumulator[Long]])
  extends Task[U](stageId, stageAttemptId, partition.index, internalAccumulators)
  with Serializable {

task 轨迹
从 Executor端接收到driver发来的任务执行请求后:

CoarseGrainedExecutorBackend
{
    receive()
    {
        case LaunchTask(data) 
            val taskDesc = ser.deserialize[TaskDescription](data.value) // 反序列化
        => executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,taskDesc.name, taskDesc.serializedTask)   // Executor 启动任务
        => { 
              val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,serializedTask) 
              threadPool.execute(tr)  // 执行TaskRunner线程 
            }
    }
}

任务线程内:

TaskRunner.run()
{
    task = ser.deserialize[Task[Any]](taskBytes, Thread.currentThread.getContextClassLoader) // 继续反序列化
    task.run(...) 
    => task.runTask() // 子类执行 runTask()

}

Partition

Partition extends Serializable {
  def index: Int
  override def hashCode(): Int = index
}

TaskLocation

任务最佳位置

private[spark] sealed trait TaskLocation {
  def host: String
}


private [spark]
case class ExecutorCacheTaskLocation(override val host: String, executorId: String)
  extends TaskLocation


private [spark] case class HostTaskLocation(override val host: String) extends TaskLocation {
  override def toString: String = host
}

private [spark] case class HDFSCacheTaskLocation(override val host: String) extends TaskLocation {
  override def toString: String = TaskLocation.inMemoryLocationTag + host
}

private[spark] object TaskLocation {
    val inMemoryLocationTag = "hdfs_cache_"

  def apply(host: String, executorId: String): TaskLocation = {
    new ExecutorCacheTaskLocation(host, executorId)
  }

  def apply(str: String): TaskLocation = {
    val hstr = str.stripPrefix(inMemoryLocationTag)
    if (hstr.equals(str)) {
      new HostTaskLocation(str)
    } else {
      new HostTaskLocation(hstr)
    }
  }
}
<think>嗯...用户问的是Spark支持的集群模式类型,看来需要整理一份清晰的分类说明。之前提供的引用材料里其实已经包含了一些线索:引用[2]明确提到了本地模式和集群模式的区别,引用[1]和[5]展示了Standalone集群的配置示例。用户可能正在搭建Spark环境,需要根据场景选择集群模式。从问题表述看,ta应该有一定基础但需要系统梳理。需要特别注意引用[4]提到的YARN相关配置参数,这说明除了Standalone之外还有其它集群管理模式。集群模式的讲解需要结构化,我决定按"自带管理"和"第三方管理"两大类别展开。对于Standalone模式要强调master和worker的组件关系(引用[1]中的7077端口很典型),对于YARN则要区分cluster和client部署差异(引用[4]的yarn.jars配置可佐证)。Mesos虽然现在用得少,但作为历史方案也该提一下。Kubernetes模式越来越重要,虽然引用材料里没提,但根据技术发展趋势应该补充。最后用对比表格总结会很直观,用户可能需要横向比较资源调度、适用场景等维度。突发想到用户可能在准备面试,所以最后附加的源码说明(Master.scala)能体现深度。相关问题设计成阶梯状:从部署到监控再到云原生,覆盖ta可能继续追问的方向。</think> Spark 支持多种集群管理模式,可根据资源管理和调度需求灵活选择。以下是主要的集群模式类型及核心特点: --- ### **1. Standalone 模式(自带集群管理)** Spark 内置的轻量级集群模式,无需依赖第三方框架。 - **架构**: - `Master`:主节点,管理集群资源(如引用 `[1]` 中的 `spark://node1:7077`)。 - `Worker`:从节点,执行任务并汇报资源状态。 - **特点**: - 部署简单(如引用 `[5]` 的 `spark-standalone` 安装)。 - 适合中小规模集群,但缺乏高级资源调度功能。 --- ### **2. Apache Hadoop YARN 模式** 利用 Hadoop YARN 作为资源调度器,适用于 Hadoop 生态集成。 - **工作模式**: - `Cluster Mode`:Driver 运行在 YARN 的 ApplicationMaster 中(资源统一管理)。 - `Client Mode`:Driver 运行在提交任务的客户端(调试更方便)。 - **核心配置**(如引用 `[4]`): ```properties spark.eventLog.enabled true spark.yarn.jars hdfs://... # 指定依赖包路径 ``` --- ### **3. Apache Mesos 模式(已逐步淘汰)** 通用集群管理器,支持动态资源共享(Spark 作为 Mesos 的任务运行)。 - **适用场景**:混合负载环境(如同时运行 Spark、MPI 等)。 --- ### **4. Kubernetes 模式(云原生首选)** 在 Kubernetes 上原生调度 Spark 应用,适合容器化环境。 - **核心组件**: - `Driver Pod`:作为 Kubernetes Pod 启动。 - `Executor Pods`:动态创建/销毁。 - **优势**:弹性伸缩、资源隔离、云平台集成。 --- ### **5. Local 模式(本地测试)** 非集群模式,单机运行(如引用 `[2]` 所述)。 - **配置示例**:`--master local[*]`(`*` 表示使用所有核心)。 - **用途**:开发调试,**不可用于生产**。 --- ### **模式对比总结** | **模式** | **资源调度器** | **适用场景** | **优势** | |----------------|--------------------|----------------------------------|----------------------------------| | Standalone | Spark Master | 中小规模独立集群 | 部署简单,无外部依赖 | | YARN | Hadoop YARN | Hadoop 生态集成 | 资源调度丰富,支持多租户 | | Kubernetes | Kubernetes | 云原生/容器化环境 | 弹性伸缩,云平台兼容性好 | | Mesos | Apache Mesos | 混合负载场景(逐步淘汰) | 动态资源共享 | | Local | 无(单机) | 开发/测试 | 快速验证逻辑 | --- ### **选择建议** - 若已有 Hadoop 集群 → **YARN 模式**(资源利用率高)。 - 需独立运行或轻量级部署 → **Standalone 模式**(如引用 `[1][5]` 的部署)。 - 容器化/云平台部署 → **Kubernetes 模式**(趋势方向)。 > **附加说明**:无论选择哪种模式,Spark 程序代码无需修改(仅需调整 `--master` 参数和集群配置)。历史服务器配置(如引用 `[4]` 的 `spark.eventLog.dir`)可跨模式通用,便于监控任务日志[^1][^4]。 --- **相关问题** 1. Standalone 模式下如何配置高可用(HA)? 2. YARN 的 Cluster Mode 与 Client Mode 性能差异如何? 3. Kubernetes 模式中如何动态调整 Executor 数量? 4. 如何通过 Spark 历史服务器(History Server)分析任务日志? > 注:Standalone HA 通常通过 ZooKeeper 实现多 Master 热备;YARN Cluster 模式更稳定但日志访问稍复杂;Kubernetes 可通过 `spark.dynamicAllocation.enabled` 实现弹性伸缩。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值