spark高频面试题100题源码解答【建议收藏】---持续更新中

spark高频面试题100题源码解答【建议收藏】—持续更新中

1.RDD五个主要特性

RDD(弹性分布式数据集)是Spark中最基本的抽象数据类型,具有以下五个主要特性:

五个特性

(1)分区列表(a list of partitions)

Spark RDD是被分区的,每一个分区都会被一个计算任务(Task)处理,分区数决定并行计算数量,RDD的并行度默认从父RDD传给子RDD。默认情况下,一个HDFS上的数据分片就是一个Partition,RDD分片数决定了并行计算的力度,可以在创建RDD时指定RDD分片个数,如果不指定分区数量,当RDD从集合创建时,则默认分区数量为该程序所分配到的资源的CPU核数(每个Core可以承载2~4个Partition),如果是从HDFS文件创建,默认为文件的Block数。

(2)每一个分区都有一个计算函数(a function for computing each split)。

每个分区都会有计算函数,Spark的RDD的计算函数是以分片为基本单位的,每个RDD都会实现compute函数,对具体的分片进行计算,RDD中的分片是并行的,所以是分布式并行计算。有一点非常重要,就是由于RDD有前后依赖关系,遇到宽依赖关系,例如,遇到reduceBykey等宽依赖操作的算子,Spark将根据宽依赖划分Stage,Stage内部通过Pipeline操作,通过Block Manager获取相关的数据,因为具体的split要从外界读数据,也要把具体的计算结果写入外界,所以用了一个管理器,具体的split都会映射成BlockManager的Block,而具体split会被函数处理,函数处理的具体形式是以任务的形式进行的。

(3)依赖于其他RDD的列表(a list of dependencies on other RDDs)。

RDD的依赖关系,由于RDD每次转换都会生成新的RDD,所以RDD会形成类似流水线的前后依赖关系,当然,宽依赖就不类似于流水线了,宽依赖后面的RDD具体的数据分片会依赖前面所有的RDD的所有的数据分片,这时数据分片就不进行内存中的Pipeline,这时一般是跨机器的。因为有前后的依赖关系,所以当有分区数据丢失的时候,Spark会通过依赖关系重新计算,算出丢失的数据,而不是对RDD所有的分区进行重新计算。RDD之间的依赖有两种:窄依赖(Narrow Dependency)、宽依赖(Wide Dependency)。RDD是Spark的核心数据结构,通过RDD的依赖关系形成调度关系。通过对RDD的操作形成整个Spark程序。

RDD有Narrow Dependency和Wide Dependency两种不同类型的依赖,其中的Narrow Dependency指的是每一个parent RDD的Partition最多被child RDD的一个Partition所使用,而Wide Dependency指的是多个child RDD的Partition会依赖于同一个parent RDD的Partition。可以从两个方面来理解RDD之间的依赖关系:一方面是该RDD的parent RDD是什么;另一方面是依赖于parent RDD的哪些Partitions;根据依赖于parent RDD的Partitions的不同情况,Spark将Dependency分为宽依赖和窄依赖两种。Spark中宽依赖指的是生成的RDD的每一个partition都依赖于父RDD的所有partition,宽依赖典型的操作有groupByKey、sortByKey等,宽依赖意味着shuffle操作,这是Spark划分Stage边界的依据,Spark中宽依赖支持两种Shuffle Manager,即HashShuffleManager和SortShuffleManager,前者是基于Hash的Shuffle机制,后者是基于排序的Shuffle机制。Spark 2.2现在的版本中已经没有Hash Shuffle的方式。

(4)key-value数据类型的RDD有一个分区器(Optionally,a Partitioner for key-value RDDS),控制分区策略和分区数。

每个key-value形式的RDD都有Partitioner属性,它决定了RDD如何分区。当然,Partition的个数还决定每个Stage的Task个数。RDD的分片函数,想控制RDD的分片函数的时候可以分区(Partitioner)传入相关的参数,如HashPartitioner、RangePartitioner,它本身针对key-value的形式,如果不是key-value的形式,它就不会有具体的Partitioner。Partitioner本身决定了下一步会产生多少并行的分片,同时,它本身也决定了当前并行(parallelize)Shuffle输出的并行数据,从而使Spark具有能够控制数据在不同节点上分区的特性,用户可以自定义分区策略,如Hash分区等。Spark提供了“partitionBy”运算符,能通过集群对RDD进行数据再分配来创建一个新的RDD。

(5)每个分区都有一个优先位置列表(-Optionally,a list of preferred locations to compute each split on)。

它会存储每个Partition的优先位置,对于一个HDFS文件来说,就是每个Partition块的位置。观察运行spark集群的控制台会发现Spark的具体计算,具体分片前,它已经清楚地知道任务发生在什么节点上,也就是说,任务本身是计算层面的、代码层面的,代码发生运算之前已经知道它要运算的数据在什么地方**(在哪一台机器上)**,有具体节点的信息。这就符合大数据中数据不动代码动的特点。数据不动代码动的最高境界是数据就在当前节点的内存中。这时有可能是memory级别或Alluxio级别的,Spark本身在进行任务调度时候,会尽可能将任务分配到处理数据的数据块所在的具体位置。据Spark的RDD.Scala源码函数getPreferredLocations可知,每次计算都符合完美的数据本地性。

代码示例

package org.example.spark

 import org.apache.spark.{SparkConf, SparkContext}

object RDDDemo {
  def main(args: Array[String]): Unit = {
       // 创建SparkConf对象并设置应用程序名称
      val conf = new SparkConf().setAppName("RDD Demo").setMaster("local[*]")

      // 创建SparkContext对象
      val sc = new SparkContext(conf)

      // 1. 分区 - A list of partitions
      val data = sc.parallelize(Seq(1, 2, 3, 4, 5), 2) // 将数据分为两个分区
    println("Partitions: " + data.getNumPartitions)
//    Partitions: 2


    // 2. 每一个分区都有一个计算函数 - A function for computing each split
      val transformedData = data.map(_ * 2) // 对每个元素进行转换操作,生成新的RDD
    println("Compute Function: " + transformedData.toDebugString)
//    Compute Function: (2) MapPartitionsRDD[1] at map at RDDDemo.scala:17 []
//    |  ParallelCollectionRDD[0] at parallelize at RDDDemo.scala:14 []


      // 3. 依赖于其他RDD的列表(a list of dependencies on other RDDs)
      val otherData = sc.parallelize(Seq(6, 7, 8, 9, 10))
      val combinedData = transformedData.union(otherData) // 将两个RDD合并成一个新的RDD
    println("otherData Dependencies: " + otherData.dependencies)
    println("combinedData Dependencies: " + combinedData.dependencies)
//    otherData Dependencies: List()
//    combinedData Dependencies: ArrayBuffer(org.apache.spark.RangeDependency@57b75756, org.apache.spark.RangeDependency@5327a06e)

    // 4. 可选项:key-value数据类型的RDD有个分区器 Partitioner for key-value RDDs
      val keyValueData = combinedData.keyBy(_.%(2)).partitionBy(new org.apache.spark.HashPartitioner(3)) // 将RDD中的元素以奇偶数为键进行分组
    println("Partitioner: " + keyValueData.partitioner)
//    Partitioner: Some(org.apache.spark.HashPartitioner@3)

    // 5. 可选项:每个分区都有一个优先位置列表 Preferred locations to compute each split
      val splitLocations = data.preferredLocations(data.partitions(0)) // 获取第一个分区的首选计算位置
    println("Preferred Locations: " + splitLocations.mkString(", "))
//    Preferred Locations:

      // 关闭SparkContext对象
      sc.stop()
  }
}

源码

/**
 * Resilient Distributed Dataset(RDD)是Spark中的基本抽象概念。它代表一个不可变、分区的元素集合,可以并行操作。
 * 这个类包含了所有RDD都可用的基本操作,比如`map`、`filter`和`persist`。另外,[[org.apache.spark.rdd.PairRDDFunctions]]
 * 包含了仅适用于键值对RDD的操作,比如`groupByKey`和`join`;
 * [[org.apache.spark.rdd.DoubleRDDFunctions]]包含了仅适用于Double类型RDD的操作;而
 * [[org.apache.spark.rdd.SequenceFileRDDFunctions]]包含了可以保存为SequenceFiles的RDD的操作。
 * 所有这些操作在正确类型的RDD上都会自动可用(例如,RDD[(Int, Int)]),通过隐式转换实现。
 *
 * 在内部,每个RDD由五个主要属性描述:
 *
 *  - 分区列表
 *  - 用于计算每个分区的函数
 *  - 对其他RDD的依赖列表
 *  - 可选的键值对RDD的Partitioner(例如,指明RDD是哈希分区的)
 *  - 可选的计算每个分区的首选位置列表(例如,HDFS文件的块位置)
 *
 * Spark中的调度和执行都是基于这些方法完成的,允许每个RDD实现自己的计算方式。
 * 实际上,用户可以通过重写这些函数来实现自定义的RDD(例如,用于从新的存储系统读取数据)。
 * 有关RDD内部的更多细节,请参阅<a href="http://people.csail.mit.edu/matei/papers/2012/nsdi_spark.pdf">Spark论文</a>。
 */
abstract class RDD[T: ClassTag](
    @transient private var _sc: SparkContext,
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging {
    
    
  /**
   * 由子类实现,返回此RDD中的分区集合。这个方法只会被调用一次,因此可以在其中实现耗时的计算。
   *
   * 此数组中的分区必须满足以下属性:
   *   `rdd.partitions.zipWithIndex.forall { case (partition, index) => partition.index == index }`
   */
  protected def getPartitions: Array[Partition]

  /**
   * 由子类实现,返回此RDD对父RDD的依赖关系。这个方法只会被调用一次,因此可以在其中实现耗时的计算。
   */
  protected def getDependencies: Seq[Dependency[_]] = deps

  /**
   * 可以由子类选择性地重写,指定位置偏好。
   */
  protected def getPreferredLocations(split: Partition): Seq[String] =
    Nil // 默认情况下没有位置偏好  

2.Spark重分区 Repartition Coalesce关系区别

关系区别

1)关系:

​ 两者都是用来改变RDD的partition数量的,repartition底层调用的就是coalesce方法:coalesce(numPartitions, shuffle = true)

​ 2)区别:

​ repartition一定会发生shuffle,coalesce 根据传入的参数来判断是否发生shuffle。

​ 一般情况下增大rdd的partition数量使用repartition,减少partition数量时使用coalesce。

综上所述,需要根据具体的需求和数据情况来选择使用哪个算子。如果需要增加分区数、进行全量洗牌或调整数据分布,可以使用repartition();如果需要减少分区数、避免全量洗牌开销或简单调整数据分布,可以使用coalesce()

源码

  /**
   * 返回一个具有恰好numPartitions个分区的新RDD。
   *
   * 可以增加或减少此RDD中的并行级别。在内部,这使用shuffle操作重新分发数据。
   *
   * 如果你要减少此RDD中的分区数,请考虑使用`coalesce`,它可以避免执行shuffle操作。
   *
   * TODO 修复SPARK-23207中描述的Shuffle+Repartition数据丢失问题。
   */
  def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
    coalesce(numPartitions, shuffle = true)
  }

  /**
   * 返回一个新的RDD,它被合并为`numPartitions`个分区。
   *
   * 这会导致一个窄依赖关系,例如如果从1000个分区缩减到100个分区,
   * 将不会有shuffle操作,而是每个新分区将占用10个当前分区。
   * 如果请求更大数量的分区,则会保持当前分区数不变。
   *
   * 然而,如果你进行了激进的合并,例如numPartitions = 1,
   * 这可能导致你的计算在比你想要的更少的节点上执行(例如,在numPartitions = 1的情况下只有一个节点)。
   * 为了避免这种情况,你可以传递shuffle = true。这将添加一个shuffle步骤,
   * 但意味着当前的上游分区将并行执行(根据当前的分区方式)。
   *
   * @note 使用shuffle = true时,实际上可以合并到更多的分区。
   * 这在有少量分区(例如100个)且其中一些分区异常大的情况下非常有用。
   * 调用coalesce(1000, shuffle = true)将导致使用哈希分区器分发数据的1000个分区。
   * 传入的可选分区合并器必须是可序列化的。
   */
  def coalesce(numPartitions: Int, shuffle: Boolean = false,
               partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
              (implicit ord: Ordering[T] = null)
      : RDD[T] = withScope {
    require(numPartitions > 0, s"分区数($numPartitions)必须为正数。")
    if (shuffle) {
      /** 在输出分区上均匀分布元素,从一个随机分区开始。 */
      val distributePartition = (index: Int, items: Iterator[T]) => {
        var position = new Random(hashing.byteswap32(index)).nextInt(numPartitions)
        items.map { t =>
          // 注意,键的哈希码将是键本身。HashPartitioner将使用总分区数对其进行取模。
          position = position + 1
          (position, t)
        }
      } : Iterator[(Int, T)]

      // 包含一个shuffle步骤,以便我们的上游任务仍然是分布式的
      new CoalescedRDD(
        new ShuffledRDD[Int, T, T](
          mapPartitionsWithIndexInternal(distributePartition, isOrderSensitive = true),
          new HashPartitioner(numPartitions)),
        numPartitions,
        partitionCoalescer).values
    } else {
      new CoalescedRDD(this, numPartitions, partitionCoalescer)
    }
  }



/**
 * 表示一个具有比其父RDD更少分区的合并RDD
 * 此类使用PartitionCoalescer类来找到父RDD的良好分区,
 * 以便每个新分区大致具有相同数量的父分区,并且每个新分区的首选位置与其父分区的尽可能多的首选位置重叠
 * @param prev 要合并的RDD
 * @param maxPartitions 合并后的RDD中所需分区的数量(必须为正数)
 * @param partitionCoalescer 用于合并的[[PartitionCoalescer]]实现
 */
private[spark] class CoalescedRDD[T: ClassTag](
    @transient var prev: RDD[T],
    maxPartitions: Int,
    partitionCoalescer: Option[PartitionCoalescer] = None)
  extends RDD[T](prev.context, Nil) {  // Nil表示我们实现了getDependencies

  require(maxPartitions > 0 || maxPartitions == prev.partitions.length,
    s"分区数($maxPartitions)必须为正数。")
  if (partitionCoalescer.isDefined) {
    require(partitionCoalescer.get.isInstanceOf[Serializable],
      "传入的分区合并器必须可序列化。")
  }

  override def getPartitions: Array[Partition] = {
    val pc = partitionCoalescer.getOrElse(new DefaultPartitionCoalescer())

    pc.coalesce(maxPartitions, prev).zipWithIndex.map {
      case (pg, i) =>
        val ids = pg.partitions.map(_.index).toArray
        new CoalescedRDDPartition(i, prev, ids, pg.prefLoc)
    }
  }

  override def compute(partition: Partition, context: TaskContext): Iterator[T] = {
    partition.asInstanceOf[CoalescedRDDPartition].parents.iterator.flatMap { parentPartition =>
      firstParent[T].iterator(parentPartition, context)
    }
  }

  override def getDependencies: Seq[Dependency[_]] = {
    Seq(new NarrowDependency(prev) {
      def getParents(id: Int): Seq[Int] =
        partitions(id).asInstanceOf[CoalescedRDDPartition].parentsIndices
    })
  }

  override def clearDependencies() {
    super.clearDependencies()
    prev = null
  }

  /**
   * 返回分区的首选机器。如果分区的类型是CoalescedRDDPartition,
   * 则首选机器将是大多数父分区也喜欢的机器。
   * @param partition
   * @return split最喜欢的机器
   */
  override def getPreferredLocations(partition: Partition): Seq[String] = {
    partition.asInstanceOf[CoalescedRDDPartition].preferredLocation.toSeq
  }
}

3.reduceByKey与groupByKey的区别,哪一种更具优势?

区别

reduceByKey:按照key进行聚合,在shuffle之前有combine(预聚合)操作,返回结果是RDD[k,v]。

groupByKey:按照key进行分组,直接进行shuffle

​ 所以,在实际开发过程中,reduceByKey比groupByKey,更建议使用。但是需要注意是否会影响业务逻辑。

源码

reduceByKey和reduceByKey都使用combineByKeyWithClassTag函数。combineByKeyWithClassTag有个一个参数mapSideCombine是否map阶段预聚合每个节点上的数据,reduceByKey中使用的true,groupByKey则是false。

groupByKey reduceByKey

  /**
   * 将RDD中每个键的值分组成单个序列。允许通过传递Partitioner来控制结果键值对RDD的分区。
   * 每个组内元素的顺序不能保证,并且甚至在每次评估结果RDD时可能会有所不同。
   *
   * @note 此操作可能非常昂贵。如果您要对每个键执行聚合(例如求和或平均值),使用`PairRDDFunctions.aggregateByKey`
   * 或`PairRDDFunctions.reduceByKey`将提供更好的性能。
   *
   * @note 目前实现的groupByKey必须能够在内存中保存所有键值对的所有值。如果一个键有太多的值,可能会导致`OutOfMemoryError`。
   */
  def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])] = self.withScope {
    // groupByKey不应该使用map side combine,因为map side combine不会减少洗牌的数据量,
    // 并且需要将所有的map side数据插入到哈希表中,导致老年代中的对象更多。
    val createCombiner = (v: V) => CompactBuffer(v)
    val mergeValue = (buf: CompactBuffer[V], v: V) => buf += v
    val mergeCombiners = (c1: CompactBuffer[V], c2: CompactBuffer[V]) => c1 ++= c2
    val bufs = combineByKeyWithClassTag[CompactBuffer[V]](
      createCombiner, mergeValue, mergeCombiners, partitioner, mapSideCombine = false)
    bufs.asInstanceOf[RDD[(K, Iterable[V])]]
  }

  /**
   * 使用关联和可交换的reduce函数合并每个键的值。
   * 这也会在每个mapper上本地执行合并,然后将结果发送给reducer,类似于MapReduce中的“combiner”。
   */
  def reduceByKey(partitioner: Partitioner, func: (V, V) => V): RDD[(K, V)] = self.withScope {
    combineByKeyWithClassTag[V]((v: V) => v, func, func, partitioner)
  }

  /**
   * 使用关联和可交换的reduce函数合并每个键的值。
   * 这也会在每个mapper上本地执行合并,然后将结果发送给reducer,类似于MapReduce中的“combiner”。
   * 输出将使用numPartitions个分区进行哈希分区。
   */
  def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)] = self.withScope {
    reduceByKey(new HashPartitioner(numPartitions), func)
  }

  /**
   * 使用关联和可交换的reduce函数合并每个键的值。
   * 这也会在每个mapper上本地执行合并,然后将结果发送给reducer,类似于MapReduce中的“combiner”。
   * 输出将使用现有的分区器/并行度级别进行哈希分区。
   */
  def reduceByKey(func: (V, V) => V): RDD[(K, V)] = self.withScope {
    reduceByKey(defaultPartitioner(self), func)
  }


  /**
   * 通用函数,根据自定义的聚合函数来合并每个键的元素。将RDD[(K, V)]转换为类型为RDD[(K, C)]的结果,
   * 其中C为“combined type”。
   *
   * 用户需要提供三个函数:
   *
   *  - `createCombiner`,将V转换为C(例如,创建一个包含单个元素的列表)
   *  - `mergeValue`,将V合并到C中(例如,将其添加到列表的末尾)
   *  - `mergeCombiners`,将两个C合并为一个C
   *
   * 此外,用户还可以控制输出RDD的分区方式,并决定是否执行map-side聚合(如果一个mapper可以生成具有相同键的多个项)。
   *
   * 注意:V和C可以是不同的类型 - 例如,可以将(Int, Int)类型的RDD分组为(Int, Seq[Int])类型的RDD。
   */
  @Experimental
  def combineByKeyWithClassTag[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C,
      partitioner: Partitioner,
      mapSideCombine: Boolean = true,
      serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
    require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
    if (keyClass.isArray) {
      if (mapSideCombine) {
        throw new SparkException("Cannot use map-side combining with array keys.")
      }
      if (partitioner.isInstanceOf[HashPartitioner]) {
        throw new SparkException("HashPartitioner cannot partition array keys.")
      }
    }
    val aggregator = new Aggregator[K, V, C](
      self.context.clean(createCombiner),
      self.context.clean(mergeValue),
      self.context.clean(mergeCombiners))
    if (self.partitioner == Some(partitioner)) {
      self.mapPartitions(iter => {
        val context = TaskContext.get()
        new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
      }, preservesPartitioning = true)
    } else {
      new ShuffledRDD[K, V, C](self, partitioner)
        .setSerializer(serializer)
        .setAggregator(aggregator)
        .setMapSideCombine(mapSideCombine)
    }
  }

ShuffledRDD

ShuffledRDD是Spark中的一个RDD类型,它是由转换操作(例如groupByKey、reduceByKey等)引起的数据重分区(shuffle)过程产生的结果。当执行需要对数据进行聚合或排序等操作时,Spark会根据键(key)对数据进行重分区,并将相同键的数据集中在同一个分区中。这个过程称为shuffle。ShuffledRDD就是通过shuffle操作生成的一个RDD类型。

private[spark] class ShuffledRDDPartition(val idx: Int) extends Partition {
  override val index: Int = idx
}

/**
 * :: DeveloperApi ::
 * Shuffle操作产生的结果RDD(例如,数据的重新分区)。
 * @param prev 父RDD。
 * @param part 用于对RDD进行分区的分区器。
 * @tparam K 键的类型。
 * @tparam V 值的类型。
 * @tparam C 组合器的类型。
 */
// TODO: 让它返回RDD[Product2[K, C]]或者有一种方式来配置可变的键值对
@DeveloperApi
class ShuffledRDD[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient var prev: RDD[_ <: Product2[K, V]],
    part: Partitioner)
    extends RDD[(K, C)](prev.context, Nil) {

  private var userSpecifiedSerializer: Option[Serializer] = None

  private var keyOrdering: Option[Ordering[K]] = None

  private var aggregator: Option[Aggregator[K, V, C]] = None

  private var mapSideCombine: Boolean = false

  /** 为此RDD的Shuffle设置一个序列化器,如果为null则使用默认值(spark.serializer)。 */
  def setSerializer(serializer: Serializer): ShuffledRDD[K, V, C] = {
    this.userSpecifiedSerializer = Option(serializer)
    this
  }

  /** 设置RDD的Shuffle的键排序。 */
  def setKeyOrdering(keyOrdering: Ordering[K]): ShuffledRDD[K, V, C] = {
    this.keyOrdering = Option(keyOrdering)
    this
  }

  /** 设置RDD的Shuffle的组合器。 */
  def setAggregator(aggregator: Aggregator[K, V, C]): ShuffledRDD[K, V, C] = {
    this.aggregator = Option(aggregator)
    this
  }

  /** 设置RDD的Shuffle的mapSideCombine标志。 */
  def setMapSideCombine(mapSideCombine: Boolean): ShuffledRDD[K, V, C] = {
    this.mapSideCombine = mapSideCombine
    this
  }

  override def getDependencies: Seq[Dependency[_]] = {
    val serializer = userSpecifiedSerializer.getOrElse {
      val serializerManager = SparkEnv.get.serializerManager
      if (mapSideCombine) {
        serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[C]])
      } else {
        serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[V]])
      }
    }
    List(new ShuffleDependency(prev, part, serializer, keyOrdering, aggregator, mapSideCombine))
  }

  override val partitioner = Some(part)

  override def getPartitions: Array[Partition] = {
    Array.tabulate[Partition](part.numPartitions)(i => new ShuffledRDDPartition(i))
  }

  override protected def getPreferredLocations(partition: Partition): Seq[String] = {
    val tracker = SparkEnv.get.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster]
    val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
    tracker.getPreferredLocationsForShuffle(dep, partition.index)
  }

  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)]]
  }

  override def clearDependencies() {
    super.clearDependencies()
    prev = null
  }

  private[spark] override def isBarrier(): Boolean = false
}

分区器


/**
 * 使用Java的`Object.hashCode`实现基于哈希的分区的[[org.apache.spark.Partitioner]]。
 *
 * Java数组的hashCode是基于数组的标识而不是内容的,因此尝试使用HashPartitioner对RDD[Array[_]]或RDD[(Array[_], _)]进行分区将产生意外或错误的结果。
 */
class HashPartitioner(partitions: Int) extends Partitioner {
    require(partitions >= 0, s"分区数($partitions)不能为负数。")

  def numPartitions: Int = partitions

  def getPartition(key: Any): Int = key match {
    case null => 0
    case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
  }

  override def equals(other: Any): Boolean = other match {
    case h: HashPartitioner =>
      h.numPartitions == numPartitions
    case _ =>
      false
  }

  override def hashCode: Int = numPartitions
}


/**
 * 为类似于cogroup的操作选择要使用的分区器。
 *
 * 如果设置了spark.default.parallelism,我们将使用SparkContext defaultParallelism的值作为默认的分区数,否则我们将使用上游分区的最大数量。
 *
 * 当可用时,我们从具有最大分区数的rdds中选择分区器。如果此分区器是合适的(分区数与rdds中的最大分区数相差不多),或者分区数高于默认的分区数 - 我们将使用该分区器。
 *
 * 否则,我们将使用具有默认分区数的新HashPartitioner。
 *
 * 除非设置了spark.default.parallelism,否则分区数将与最大上游RDD中的分区数相同,因为这应该是最不可能导致内存错误的情况。
 *
 * 我们使用两个方法参数(rdd, others)来强制调用者至少传递1个RDD。
 */
def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
    val rdds = (Seq(rdd) ++ others)
    val hasPartitioner = rdds.filter(_.partitioner.exists(_.numPartitions > 0))

  val hasMaxPartitioner: Option[RDD[_]] = if (hasPartitioner.nonEmpty) {
    Some(hasPartitioner.maxBy(_.partitions.length))
  } else {
    None
  }

  val defaultNumPartitions = if (rdd.context.conf.contains("spark.default.parallelism")) {
    rdd.context.defaultParallelism
  } else {
    rdds.map(_.partitions.length).max
  }

  // 如果现有的最大分区器是合适的,或者其分区数大于默认的分区数,则使用现有的分区器。
  if (hasMaxPartitioner.nonEmpty && (isEligiblePartitioner(hasMaxPartitioner.get, rdds) ||
      defaultNumPartitions < hasMaxPartitioner.get.getNumPartitions)) {
    hasMaxPartitioner.get.partitioner.get
  } else {
    new HashPartitioner(defaultNumPartitions)
  }
}

CompactBuffer

一个类似于ArrayBuffer的追加缓冲区,但对于小型缓冲区更节省内存。


/**
 * 一个类似于ArrayBuffer的追加缓冲区,但对于小型缓冲区更节省内存。
 * ArrayBuffer总是分配一个对象数组来存储数据,默认情况下有16个条目,因此它的开销约为80-100字节。
 * 相反,CompactBuffer可以在主对象的字段中保留最多两个元素,并且只有在超过这些元素时才分配一个Array[AnyRef]。
 * 这使得它在像groupBy这样的操作中更高效,我们期望一些键具有非常少的元素。
 */
private[spark] class CompactBuffer[T: ClassTag] extends Seq[T] with Serializable {
    // First two elements
    private var element0: T = _
    private var element1: T = _

  // Number of elements, including our two in the main object
  private var curSize = 0

  // Array for extra elements
  private var otherElements: Array[T] = null

  /**
   * 获取指定位置的元素
   * @param position 元素的位置
   * @return 指定位置的元素值
      */

    def apply(position: Int): T = {
    
    if (position < 0 || position >= curSize) {
      throw new IndexOutOfBoundsException
    }
    if (position == 0) {
      element0
    } else if (position == 1) {
      element1
    } else {
      otherElements(position - 2)
    }
  }

  /**
   * 更新指定位置的元素值
   * @param position 元素的位置
   * @param value 新的元素值
      */

    private def update(position: Int, value: T): Unit = {
    
    if (position < 0 || position >= curSize) {
      throw new IndexOutOfBoundsException
    }
    if (position == 0) {
      element0 = value
    } else if (position == 1) {
      element1 = value
    } else {
      otherElements(position - 2) = value
    }
  }

  /**
   * 向缓冲区追加一个元素
   * @param value 要追加的元素
   * @return 当前的CompactBuffer对象
      */

    def += (value: T): CompactBuffer[T] = {
    
    val newIndex = curSize
    if (newIndex == 0) {
      element0 = value
      curSize = 1
    } else if (newIndex == 1) {
      element1 = value
      curSize = 2
    } else {
      growToSize(curSize + 1)
      otherElements(newIndex - 2) = value
    }
    this
  }

  /**
   * 向缓冲区追加多个元素
   * @param values 要追加的元素集合
   * @return 当前的CompactBuffer对象
      */

    def ++= (values: TraversableOnce[T]): CompactBuffer[T] = {
    
    values match {
      // 优化CompactBuffer的合并,用于cogroup和groupByKey
      case compactBuf: CompactBuffer[T] =>
        val oldSize = curSize
        // 复制其他缓冲区的大小和元素到本地变量,以防它与我们相等
        val itsSize = compactBuf.curSize
        val itsElements = compactBuf.otherElements
        growToSize(curSize + itsSize)
        if (itsSize == 1) {
          this(oldSize) = compactBuf.element0
        } else if (itsSize == 2) {
          this(oldSize) = compactBuf.element0
          this(oldSize + 1) = compactBuf.element1
        } else if (itsSize > 2) {
          this(oldSize) = compactBuf.element0
          this(oldSize + 1) = compactBuf.element1
          // 此时我们的大小也大于2,所以直接将其数组复制到我们的数组中。
          // 注意,由于我们在上面添加了两个元素,我们应该将其复制到this.otherElements中的索引为oldSize的位置。
          System.arraycopy(itsElements, 0, otherElements, oldSize, itsSize - 2)
        }
    
      case _ =>
        values.foreach(e => this += e)
    }
    this
  }

  override def length: Int = curSize

  override def size: Int = curSize

  override def iterator: Iterator[T] = new Iterator[T] {
    private var pos = 0
    override def hasNext: Boolean = pos < curSize
    override def next(): T = {
      if (!hasNext) {
        throw new NoSuchElementException
      }
      pos += 1
      apply(pos - 1)
    }
  }

  /**
   * 将缓冲区的大小增加到newSize,并在需要时扩展支持数组
   * @param newSize 新的缓冲区大小
      */

    private def growToSize(newSize: Int): Unit = {
    
    // 由于两个字段存储在element0和element1中,一个数组存储newSize-2个元素
    val newArraySize = newSize - 2
    val arrayMax = ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH
    if (newSize < 0 || newArraySize > arrayMax) {
      throw new UnsupportedOperationException(s"无法将缓冲区扩展到超过$arrayMax个元素")
    }
    val capacity = if (otherElements != null) otherElements.length else 0
    if (newArraySize > capacity) {
      var newArrayLen = 8L
      while (newArraySize > newArrayLen) {
        newArrayLen *= 2
      }
      if (newArrayLen > arrayMax) {
        newArrayLen = arrayMax
      }
      val newArray = new Array[T](newArrayLen.toInt)
      if (otherElements != null) {
        System.arraycopy(otherElements, 0, newArray, 0, otherElements.length)
      }
      otherElements = newArray
    }
    curSize = newSize
  }
}

private[spark] object CompactBuffer {
  def apply[T: ClassTag](): CompactBuffer[T] = new CompactBuffer[T]

  def apply[T: ClassTag](value: T): CompactBuffer[T] = {
    val buf = new CompactBuffer[T]
    buf += value
  }
}


4. Spark常用的transformation和action算子,有哪些算子会导致Shuffle?

antion算子

在Spark中,Action是一种触发RDD计算并返回结果的操作。当我们调用Action操作时,Spark会对RDD执行计算,并将结果返回给驱动程序或将结果写入外部存储系统。

下面是几个常见的Spark Action操作的定义和说明:

  1. collect(): 将整个RDD的数据收集到驱动程序中,以本地数组的形式返回。
  2. count(): 返回RDD中的元素数量。
  3. first(): 返回RDD中的第一个元素。
  4. take(n: Int): 返回RDD中的前n个元素。
  5. reduce(func: (T, T) => T): 使用指定的二元操作函数对RDD中的元素进行聚合,返回一个单一的值。
  6. foreach(func: T => Unit): 对RDD中的每个元素应用指定的函数。
  7. saveAsTextFile(path: String): 将RDD的内容保存为文本文件。
  8. saveAsObjectFile(path: String): 将RDD的内容保存为序列化的对象文件。
  9. saveAsSequenceFile(path: String): 将RDD的内容保存为Hadoop序列文件。
  10. countByKey(): 对(K, V)类型的RDD执行计数操作,返回一个包含每个键和其对应的出现次数的Map。
  11. countByValue(): 对T类型的RDD执行计数操作,返回一个包含每个元素和其对应的出现次数的Map。
  12. aggregate(zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U): 使用指定的初始值、序列操作函数和组合操作函数对RDD中的元素进行聚合。

这些是Spark中一些常用的Action操作,你可以根据具体的需求选择适当的操作来触发RDD计算并获取结果。

示例

package org.example.spark

import org.apache.spark.{SparkConf, SparkContext}

import java.util.UUID

object ActionDemo extends App{
  // 创建SparkContext
  val conf = new SparkConf().setAppName("ActionDemo").setMaster("local[*]")
  val sc = new SparkContext(conf)

  // 创建一个包含整数的RDD
  val data = sc.parallelize(Seq(1, 2, 3, 4, 5))

  // collect() - 将整个RDD的数据收集到驱动程序中,以本地数组的形式返回
  val collectedData = data.collect()
  println("Collected Data: " + collectedData.mkString(", "))

  // count() - 返回RDD中的元素数量
  val count = data.count()
  println("Count: " + count)

  // first() - 返回RDD中的第一个元素
  val firstElement = data.first()
  println("First Element: " + firstElement)

  // take(n: Int) - 返回RDD中的前n个元素
  val nElements = data.take(3)
  println("First 3 Elements: " + nElements.mkString(", "))

  // reduce(func: (T, T) => T) - 使用指定的二元操作函数对RDD中的元素进行聚合,返回一个单一的值
  val sum = data.reduce((x, y) => x + y)
  println("Sum: " + sum)

  // foreach(func: T => Unit) - 对RDD中的每个元素应用指定的函数
  data.foreach(x => println("Element: " + x))

  // aggregate(zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U) - 使用指定的初始值、序列操作函数和组合操作函数对RDD中的元素进行聚合
  val initialValue = 0
  val sumAggregate = data.aggregate(initialValue)((acc, value) => acc + value, (acc1, acc2) => acc1 + acc2)
  println("Sum Aggregate: " + sumAggregate)

  // saveAsTextFile(path: String) - 将RDD的内容保存为文本文件
  data.saveAsTextFile(UUID.randomUUID()+"output")

  // saveAsObjectFile(path: String) - 将RDD的内容保存为序列化的对象文件
  data.saveAsObjectFile(UUID.randomUUID()+"output")

  // saveAsSequenceFile(path: String) - 将RDD的内容保存为Hadoop序列文件
  data.map(x => (x, x * 2)).saveAsSequenceFile(UUID.randomUUID()+"output")

  // countByKey() - 对(K, V)类型的RDD执行计数操作,返回一个包含每个键和其对应的出现次数的Map
  val keyValueData = sc.parallelize(Seq(("A", 1), ("B", 2), ("C", 3), ("A", 4), ("B", 5)))
  val countByKey = keyValueData.countByKey()
  println("Count by Key: " + countByKey)

  // countByValue() - 对T类型的RDD执行计数操作,返回一个包含每个元素和其对应的出现次数的Map
  val countByValue = data.countByValue()
  println("Count by Value: " + countByValue)

  // 关闭SparkContext
  sc.stop()
}
//Collected Data: 1, 2, 3, 4, 5
//Count: 5
//First Element: 1
//First 3 Elements: 1, 2, 3
//Sum: 15
//Element: 4
//Element: 3
//Element: 1
//Element: 5
//Element: 2
//Sum Aggregate: 15

源码

所有的action行动算子都会调用 runJob方法

/**
   * 返回一个包含此RDD中所有元素的数组。
   *
   * 注意:只有当预期结果数组较小且所有数据都加载到驱动程序的内存中时,才应使用此方法。
   */
  def collect(): Array[T] = withScope {
    val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
    Array.concat(results: _*)
  }

  /**
   * 在RDD的所有分区上运行一个作业,并将结果以数组的形式返回。
   *
   * @param rdd 目标RDD,用于在其上运行任务
   * @param func 在RDD的每个分区上运行的函数
   * @return 内存中的集合,包含作业的结果(每个集合元素将包含来自一个分区的结果)
   */
  def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
    runJob(rdd, func, 0 until rdd.partitions.length)
  }

  /**
   * 在RDD的给定一组分区上运行函数,并将结果作为数组返回。
   *
   * @param rdd 目标RDD,用于在其上运行任务
   * @param func 在RDD的每个分区上运行的函数
   * @param partitions 要运行的分区集合;某些作业可能不希望在目标RDD的所有分区上计算,
   *                   例如对于`first()`等操作
   * @return 内存中的集合,包含作业的结果(每个集合元素将包含来自一个分区的结果)
   */
  def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: Iterator[T] => U,
      partitions: Seq[Int]): Array[U] = {
    val cleanedFunc = clean(func)
    runJob(rdd, (ctx: TaskContext, it: Iterator[T]) => cleanedFunc(it), partitions)
  }



/**
   * 在RDD的给定一组分区上运行函数,并将结果传递给给定的处理函数。这是Spark中所有action操作的主要入口点。
   *
   * @param rdd 目标RDD,用于在其上运行任务
   * @param func 在RDD的每个分区上运行的函数
   * @param partitions 要运行的分区集合;某些作业可能不希望在目标RDD的所有分区上计算,
   *                   例如对于`first()`等操作
   * @param resultHandler 将每个结果传递给的回调函数
   */
  def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {
    if (stopped.get()) {
      throw new IllegalStateException("SparkContext has been shutdown")
    }
    val callSite = getCallSite
    val cleanedFunc = clean(func)
    logInfo("Starting job: " + callSite.shortForm)
    if (conf.getBoolean("spark.logLineage", false)) {
      logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
    }
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    progressBar.foreach(_.finishAll())
    rdd.doCheckpoint()
  }

transformation算子

在Spark中,Transformation算子是惰性求值的。这意味着当我们调用Transformation算子时,并不会立即执行计算,而是记录下操作的转换步骤以构建一个执行计划。

只有当遇到一个Action算子时,Spark才会触发实际的计算。Action算子会触发Spark执行之前定义的Transformation算子,并将结果返回给驱动程序或将结果写入外部系统。

通过延迟计算,Spark能够优化执行计划、自动进行数据分区和任务调度,并提供更高效的数据处理。此外,惰性求值还允许我们构建更复杂的数据流水线,并根据需要动态调整计算步骤。

需要注意的是,在使用Transformation算子时,我们应该避免对数据进行频繁的缓存操作,因为这可能会导致内存消耗过大。相反,我们可以通过合理地选择缓存点来减少计算开销,以及使用持久化机制(如checkpointing)来确保数据的可靠性和容错性。

总结起来,Spark中的Transformation算子采用了惰性求值的策略,通过构建执行计划并在遇到Action算子时进行实际计算,以提高计算效率和灵活性。

常见的Transformation算子:

  • map(func: T => U): RDD[U]:对RDD中的每个元素应用给定的函数,返回一个新的RDD,其中包含应用函数后的结果。
  • filter(func: T => Boolean): RDD[T]:根据给定的条件过滤出符合要求的元素,返回一个新的RDD。
  • flatMap(func: T => TraversableOnce[U]): RDD[U]:对RDD中的每个元素应用给定的函数,并将函数返回的所有元素展平为一个新的RDD。
  • distinct(numPartitions: Int): RDD[T]:返回一个去重后的新的RDD。
  • union(other: RDD[T]): RDD[T]:将当前RDD与另一个RDD进行合并,返回一个包含两个RDD中所有元素的新的RDD。
  • intersection(other: RDD[T]): RDD[T]:返回两个RDD的交集,即包含两个RDD中都存在的元素的新的RDD。
  • subtract(other: RDD[T]): RDD[T]:返回一个新的RDD,其中包含当前RDD中存在但另一个RDD中不存在的元素。
  • cartesian(other: RDD[U]): RDD[(T, U)]:返回一个新的RDD,其中包含当前RDD和另一个RDD所有可能的元素对。

这些Transformation算子用于对RDD进行转换操作,生成一个新的RDD。它们是惰性求值的,只有在遇到一个Action算子时才会触发实际的计算。您可以根据具体的业务逻辑和数据操作需求,选择适合的Transformation算子来处理数据。

示例

object TransformationExample extends App{

  import org.apache.spark.{SparkConf, SparkContext}

  // 创建SparkConf和SparkContext
  val conf = new SparkConf().setAppName("TransformationExample").setMaster("local")
  val sc = new SparkContext(conf)

  // 创建一个RDD
  val rdd = sc.parallelize(Seq(1, 2, 3, 4, 5))

  // 使用map算子对RDD中的每个元素进行加倍操作
  val doubledRDD = rdd.map(x => x * 2)
  println("doubledRDD: " + doubledRDD.collect().mkString(", "))

  // 使用filter算子过滤出偶数元素
  val evenRDD = rdd.filter(x => x % 2 == 0)
  println("evenRDD: " + evenRDD.collect().mkString(", "))



  // 创建另一个RDD
  val otherRDD = sc.parallelize(Seq(4, 5, 6, 7, 8))

  // 使用union算子合并两个RDD
  val unionRDD = rdd.union(otherRDD)
  println("unionRDD: " + unionRDD.collect().mkString(", "))

  // 使用intersection算子获取两个RDD的交集
  val intersectionRDD = rdd.intersection(otherRDD)
  println("intersectionRDD: " + intersectionRDD.collect().mkString(", "))

  // 使用subtract算子获取当前RDD中存在但另一个RDD中不存在的元素
  val subtractRDD = rdd.subtract(otherRDD)
  println("subtractRDD: " + subtractRDD.collect().mkString(", "))

  // 使用cartesian算子获取两个RDD的笛卡尔积
  val cartesianRDD = rdd.cartesian(otherRDD)
  println("cartesianRDD: " + cartesianRDD.collect().mkString(", "))

  // 使用flatMap算子将每个元素拆分成多个单词
  val wordsRDD = rdd.flatMap(x => x.toString.toCharArray.map(_.toString))
  println("wordsRDD: " + wordsRDD.collect().mkString(", "))

  // 使用distinct算子去重
  val distinctRDD = rdd.distinct()
  println("distinctRDD: " + distinctRDD.collect().mkString(", "))

  // 关闭SparkContext
  sc.stop()

}

//doubledRDD: 2, 4, 6, 8, 10
//evenRDD: 2, 4
//unionRDD: 1, 2, 3, 4, 5, 4, 5, 6, 7, 8
//intersectionRDD: 4, 5
//subtractRDD: 1, 2, 3
//cartesianRDD: (1,4), (1,5), (1,6), (1,7), (1,8), (2,4), (2,5), (2,6), (2,7), (2,8), (3,4), (3,5), (3,6), (3,7), (3,8), (4,4), (4,5), (4,6), (4,7), (4,8), (5,4), (5,5), (5,6), (5,7), (5,8)
//wordsRDD: 1, 2, 3, 4, 5

源码

/**
 * Return a new RDD by applying a function to all elements of this 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))
}

map方法是一个Transformation算子,用于对RDD中的每个元素应用给定的函数。它接收一个函数f,该函数将类型为T的元素转换为类型为U的元素,并返回一个新的RDD,其中包含应用函数后的结果。

在内部实现中,map方法创建了一个新的MapPartitionsRDD对象,该对象继承自RDD[U]。它通过迭代每个分区中的元素,并将函数f应用于每个元素来生成转换后的结果。最终,新的MapPartitionsRDD将作为一个新的RDD返回。

注意,在函数应用之前,map方法使用sc.clean方法对函数进行了清理,以确保可以正确地序列化和传输函数。这是因为函数可能会引用外部的变量或对象。

通过使用map方法,我们可以对RDD中的元素进行一对一的转换操作,从而生成一个新的RDD,以满足特定的业务需求。

以下是对MapPartitionsRDD的定义和功能的解释:

/**
 * 对父RDD的每个分区应用提供的函数的RDD。
 *
 * @param prev 父RDD。
 * @param f 用于将元组(TaskContext、分区索引、输入迭代器)映射为输出迭代器的函数。
 * @param preservesPartitioning 输入函数是否保留分区器,除非prev是一个键值对RDD并且输入函数不修改键,否则应为false。
 * @param isFromBarrier 指示此RDD是否是从RDDBarrier转换而来的,包含至少一个RDDBarrier的阶段将转变为barrier阶段。
 * @param isOrderSensitive 函数是否依赖于顺序。如果它依赖于顺序,则当输入顺序改变时可能会返回完全不同的结果。大多数有状态的函数都是order-sensitive的。
 */
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    var prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false,
    isFromBarrier: Boolean = false,
    isOrderSensitive: Boolean = false)
  extends RDD[U](prev) {

  override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None

  override def getPartitions: Array[Partition] = firstParent[T].partitions

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

  override def clearDependencies() {
    super.clearDependencies()
    prev = null
  }

  @transient protected lazy override val isBarrier_ : Boolean =
    isFromBarrier || dependencies.exists(_.rdd.isBarrier())

  override protected def getOutputDeterministicLevel = {
    if (isOrderSensitive && prev.outputDeterministicLevel == DeterministicLevel.UNORDERED) {
      DeterministicLevel.INDETERMINATE
    } else {
      super.getOutputDeterministicLevel
    }
  }
}

MapPartitionsRDD是一个表示对父RDD的每个分区应用函数的RDD。它接收一个父RDD prev,一个函数 f,以及一些额外的参数。函数 f 将一个元组 (TaskContext, partition index, input iterator) 映射为一个输出迭代器。

在内部实现中,MapPartitionsRDD 继承自 RDD[U],其中 U 是转换后的元素类型。它重写了一些方法来实现对父RDD的分区计算和依赖关系管理。通过调用函数 f 来计算每个分区的结果,并将其作为一个新的迭代器返回。

此外,MapPartitionsRDD 还提供了一些其他功能,如处理分区器、清除依赖关系和判断是否是Barrier RDD等。

通过使用 MapPartitionsRDD,我们可以对RDD的每个分区应用函数,从而实现更灵活和高效的数据转换操作。

5.Spark 窄依赖 宽依赖 定义源码(你记不住是全网都说错了!!!)

定义

子RDD依赖小数量的父RDD的分区的依赖叫做窄依赖。依赖的越少越窄。

---------------------------------------记种这句话,就永远不会忘记怎么区分窄依赖宽依赖了

子RDD依赖最多(也就是所有)父RDD的分区的依赖叫做宽依赖

子RDD依赖小数量(也就是所有)父RDD的分区的依赖叫做窄依赖

子RDD依赖最小数量(也就是1个)父RDD的分区的依赖叫做窄依赖

不信可以看下边窄依赖的源码注释!!!!!

/**
 * :: DeveloperApi ::
 * Base class for dependencies where each partition of the child RDD depends on a small number
 * of partitions of the parent RDD. Narrow dependencies allow for pipelined execution.
 */
@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * Get the parent partitions for a child partition.
   * @param partitionId a partition of the child RDD
   * @return the partitions of the parent RDD that the child partition depends upon
   */
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}

示例

  • 宽依赖的算子 :join(非hash-partitioned)、groupByKey、partitionBy

  • 窄依赖的算子 :map、filter、union、join(hash-partitioned)、mapPartitions;

请结合定义思考示例的合理性。

Stage如何划分

在Spark中,DAG(有向无环图)表示了作业的执行计划,由一系列RDD和它们之间的转换操作组成。当遇到宽依赖时,Spark会将DAG划分为多个Stage,以实现并行执行。

每个Stage包含一组任务,这些任务可以并行执行。每个任务对应于输入RDD的一个分区,它执行相同的转换操作,并生成输出RDD的一个分区。这样,每个Stage可以独立地处理其输入分区,而无需等待其他Stage的完成。

Stage的划分是根据宽依赖来进行的。宽依赖表示子RDD的每个分区依赖于父RDD的所有分区,即需要进行数据洗牌和重新分区的情况。在这种情况下,Spark会将具有相同宽依赖的RDD划分到同一个Stage中,以确保在执行Shuffle操作之前,所有必要的数据都已经准备好。

通过将DAG划分为多个Stage,并将任务并行化执行,Spark能够最大程度地提高作业的执行效率和性能。这种并行执行的方式充分利用了集群中的资源,同时还能减少数据传输和等待时间,提高整体计算速度。

源码

package org.apache.spark

import scala.reflect.ClassTag

import org.apache.spark.annotation.DeveloperApi
import org.apache.spark.rdd.RDD
import org.apache.spark.serializer.Serializer
import org.apache.spark.shuffle.ShuffleHandle

/**
 * :: DeveloperApi ::
 * 基础依赖类。
 */
@DeveloperApi
abstract class Dependency[T] extends Serializable {
  def rdd: RDD[T]
}

/**
 * :: DeveloperApi ::
 * 窄依赖类,子RDD的每个分区只依赖于父RDD的几个分区。窄依赖允许流水线执行。
 */
@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * 获取子RDD分区的父RDD分区。
   * @param partitionId 子RDD的一个分区
   * @return 子RDD分区所依赖的父RDD分区
   */
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}

/**
 * :: DeveloperApi ::
 * 表示对shuffle阶段输出的依赖关系。注意,在shuffle的情况下,RDD是瞬态的,因为我们不需要在执行器端使用它。
 *
 * @param _rdd 父RDD
 * @param partitioner 用于划分shuffle输出的Partitioner
 * @param serializer 使用的序列化器。如果没有显式设置,则使用默认的序列化器,由`spark.serializer`配置选项指定。
 * @param keyOrdering RDD的shuffle的键排序
 * @param aggregator RDD的shuffle的map/reduce-side聚合器
 * @param mapSideCombine 是否执行部分聚合(也称为map-side combine)
 */
@DeveloperApi
class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {

  if (mapSideCombine) {
    require(aggregator.isDefined, "Map-side combine without Aggregator specified!")
  }
  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  // 注意:如果使用PairRDDFunctions中的combineByKey方法而不是combineByKeyWithClassTag方法,则组合器类标签可能为null。
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)

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

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

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

/**
 * :: DeveloperApi ::
 * 表示父RDD和子RDD之间的一对一依赖关系。
 */
@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

/**
 * :: DeveloperApi ::
 * 表示父RDD和子RDD之间分区范围的一对一依赖关系。
 * @param rdd 父RDD
 * @param inStart 父RDD中的范围起始位置
 * @param outStart 子RDD中的范围起始位置
 * @param length 范围的长度
 */
@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
    }
  }
}

源码分析

这段代码是Apache Spark中关于依赖关系的基础类和具体实现。

首先,Dependency是所有依赖关系的基类,其中定义了一个抽象方法rdd,用于获取RDD。

接下来,Dependency是窄依赖的基类,表示子RDD的每个分区只依赖于父RDD的几个分区。它有一个抽象方法getParents,用于获取子RDD分区所依赖的父RDD分区。

ShuffleDependency是对shuffle阶段输出的依赖关系的表示。它包含了父RDD、划分shuffle输出的Partitioner、序列化器、键排序、聚合器等信息。它继承自Dependency[Product2[K, V]],表示依赖关系的数据类型为(K, V)

OneToOneDependency 是 窄依赖的子类,表示父RDD和子RDD之间的一对一依赖关系,即每个父分区只对应一个子分区。

xt.cleaner.foreach(_.registerShuffleForCleanup(this))
}

/**
 * :: DeveloperApi ::
 * 表示父RDD和子RDD之间的一对一依赖关系。
 */
@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}

/**
 * :: DeveloperApi ::
 * 表示父RDD和子RDD之间分区范围的一对一依赖关系。
 * @param rdd 父RDD
 * @param inStart 父RDD中的范围起始位置
 * @param outStart 子RDD中的范围起始位置
 * @param length 范围的长度
 */
@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
    }
  }
}

源码分析

这段代码是Apache Spark中关于依赖关系的基础类和具体实现。

首先,Dependency是所有依赖关系的基类,其中定义了一个抽象方法rdd,用于获取RDD。

接下来,Dependency是窄依赖的基类,表示子RDD的每个分区只依赖于父RDD的几个分区。它有一个抽象方法getParents,用于获取子RDD分区所依赖的父RDD分区。

ShuffleDependency是对shuffle阶段输出的依赖关系的表示。它包含了父RDD、划分shuffle输出的Partitioner、序列化器、键排序、聚合器等信息。它继承自Dependency[Product2[K, V]],表示依赖关系的数据类型为(K, V)

OneToOneDependency 是 窄依赖的子类,表示父RDD和子RDD之间的一对一依赖关系,即每个父分区只对应一个子分区。

RangeDependency表示父RDD和子RDD之间分区范围的一对一依赖关系,通过指定起始位置和长度来确定父分区和子分区的对应关系。

  • 13
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BigDataMLApplication

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值