Spark 3.x 的 Exchange 体系源码解析

本文深入探讨了Spark中的数据交换机制,主要关注两种类型:Shuffle和Broadcast。ShuffleExchangeExec详细解释了shuffle过程,包括分区、序列化和性能指标。BroadcastExchangeExec则介绍了广播交换的实现,涉及广播变量的创建、缓存和执行。内容涵盖了数据处理的关键步骤,对理解Spark并行处理有重要价值。
摘要由CSDN通过智能技术生成

前言

本文隶属于专栏《大数据技术体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和参考文献请见大数据技术体系


WHAT

Spark 中的 Exchange 指的是那些可以在多线程/进程之间交换数据的算子,它是 Spark 能够实现并行处理的关键所在。

Spark 中的数据交换只有 2 种类型:

  1. Shuffle
  2. Broadcast

类图

在这里插入图片描述


Exchange

我们先来看看Exchange抽象类的源码:

abstract class Exchange extends UnaryExecNode {
  // 输出
  override def output: Seq[Attribute] = child.output
  // 节点模式匹配
  final override val nodePatterns: Seq[TreePattern] = Seq(EXCHANGE)
  // 参数字符串方便打印
  override def stringArgs: Iterator[Any] = super.stringArgs ++ Iterator(s"[id=#$id]")
}

Exchange 是单一执行的节点(UnaryExecNode),这表示只需要单独一个节点就能执行。

Exchange 抽象类中重写了 3 个方法:

  1. QueryPlan.output:表示查询计划的输出
  2. TreeNode.nodePatterns:这是用来标识当前的节点是属于哪一类的
  3. TreeNode.stringArgs:节点的参数字符串方便打印

Shuffle

ShuffleExchangeLike

ShuffleExchangeLike 是所有 shuffle exchange 实现的共同接口,便于模式匹配。

trait ShuffleExchangeLike extends Exchange {

  /**
   * 返回当前 shuffle 的 mapper 的数量 
   */
  def numMappers: Int

  /**
   * 返回 shuffle 分区数
   */
  def numPartitions: Int

  /**
   * 当前 shuffle 算子的起源
   */
  def shuffleOrigin: ShuffleOrigin

  /**
   * 实现 shuffle 的异步作业。
   * 它还做准备工作,比如等待子查询
   */
  final def submitShuffleJob: Future[MapOutputStatistics] = executeQuery {
    mapOutputStatisticsFuture
  }

  /**
   * map 输出的统计信息
   */
  protected def mapOutputStatisticsFuture: Future[MapOutputStatistics]

  /**
   * 返回具有指定分区规格的 shuffle RDD。
   */
  def getShuffleRDD(partitionSpecs: Array[ShufflePartitionSpec]): RDD[_]

  /**
   * 返回 shuffle 物化后的运行时统计信息。
   */
  def runtimeStatistics: Statistics
}

ShuffleExchangeExec

ShuffleExchangeExec 是具体执行的 shuffle exchange,用来产生想要的分区。

case class ShuffleExchangeExec(
                                override val outputPartitioning: Partitioning,
                                child: SparkPlan,
                                shuffleOrigin: ShuffleOrigin = ENSURE_REQUIREMENTS)
  extends ShuffleExchangeLike {

  /**
   * 写相关的 metrics,统计:
   * shuffle bytes written,
   * shuffle records written,
   * shuffle write time。
   *
   * 通过 Spark UI 可以查看。
   */
  private lazy val writeMetrics =
    SQLShuffleWriteMetricsReporter.createShuffleWriteMetrics(sparkContext)
  /**
   * 读相关的 metrics,统计下面的内容:
   * remote blocks read,
   * local blocks read,
   * remote bytes read,
   * remote bytes read to disk,
   * local bytes read,
   * fetch wait time,
   * records read。
   *
   * 通过 Spark UI 可以查看。
   */
  private[sql] lazy val readMetrics =
    SQLShuffleReadMetricsReporter.createShuffleReadMetrics(sparkContext)
  /**
   * 所有的 metrics。
   * 除了之前的读写相关 metrics,还包括:
   * data size,
   * number of partitions。
   *
   * 通过 Spark UI 可以查看。
   */
  override lazy val metrics = Map(
    "dataSize" -> SQLMetrics.createSizeMetric(sparkContext, "data size"),
    "numPartitions" -> SQLMetrics.createMetric(sparkContext, "number of partitions")
  ) ++ readMetrics ++ writeMetrics

  /**
   * 节点名称
   */
  override def nodeName: String = "Exchange"

  /**
   * shuffle 期间序列化 `UnsafeRow` 的序列化器
   */
  private lazy val serializer: Serializer =
    new UnsafeRowSerializer(child.output.size, longMetric("dataSize"))

  /**
   * 输入的 RDD,子节点在准备工作完成后将查询的结果作为 RDD[InternalRow] 返回。
   */
  @transient lazy val inputRDD: RDD[InternalRow] = child.execute()

  // 'mapOutputStatisticsFuture'只会在开启 AQE 的时候用到。
  @transient
  override lazy val mapOutputStatisticsFuture: Future[MapOutputStatistics] = {
    if (inputRDD.getNumPartitions == 0) {
      Future.successful(null)
    } else {
      sparkContext.submitMapStage(shuffleDependency)
    }
  }

  /**
   * 返回当前 shuffle 的 mapper 的数量
   */
  override def numMappers: Int = shuffleDependency.rdd.getNumPartitions

  /**
   * 返回 shuffle 分区数
   */
  override def numPartitions: Int = shuffleDependency.partitioner.numPartitions

  /**
   * 返回具有指定分区规格的 shuffle RDD。
   */
  override def getShuffleRDD(partitionSpecs: Array[ShufflePartitionSpec]): RDD[InternalRow] = {
    new ShuffledRowRDD(shuffleDependency, readMetrics, partitionSpecs)
  }

  /**
   * 返回 shuffle 物化后的运行时统计信息。
   */
  override def runtimeStatistics: Statistics = {
    val dataSize = metrics("dataSize").value
    val rowCount = metrics(SQLShuffleWriteMetricsReporter.SHUFFLE_RECORDS_WRITTEN).value
    Statistics(dataSize, Some(rowCount))
  }

  /**
   * 一种`ShuffleDependence`,它将根据`newPartitioning`中定义的分区方案对其子级的行进行分区。
   * 返回的`ShuffleDependence`的那些分区将作为 shuffle 的输入。
   */
  @transient
  lazy val shuffleDependency: ShuffleDependency[Int, InternalRow, InternalRow] = {
    val dep = ShuffleExchangeExec.prepareShuffleDependency(
      inputRDD,
      child.output,
      outputPartitioning,
      serializer,
      writeMetrics)
    metrics("numPartitions").set(dep.partitioner.numPartitions)
    val executionId = sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)
    SQLMetrics.postDriverMetricUpdates(
      sparkContext, executionId, metrics("numPartitions") :: Nil)
    dep
  }

  /**
   * 缓存创建的 ShuffleRowRDD,以便我们可以重用它。
   */
  private var cachedShuffleRDD: ShuffledRowRDD = null

  /**
   * 将查询结果生成为 RDD[InternalRow]
   */
  protected override def doExecute(): RDD[InternalRow] = {
    // 如果此计划被多个计划使用,则返回相同的 ShuffleRowRDD。
    if (cachedShuffleRDD == null) {
      cachedShuffleRDD = new ShuffledRowRDD(shuffleDependency, readMetrics)
    }
    cachedShuffleRDD
  }

  /**
   * 生成一个新的子节点的过程中顺便会对当前子节点做的事情
   */
  override protected def withNewChildInternal(newChild: SparkPlan): ShuffleExchangeExec =
    copy(child = newChild)
}

object ShuffleExchangeExec {

  /**
   * 确定记录在发送到 shuffle 之前是否必须进行保护性的拷贝。
   * Spark 的几个 shuffle 组件将在内存中缓冲反序列化的 Java 对象。
   * shuffle 代码假定对象是不可变的,因此不会执行自己的保护性拷贝。
   * 然而,在 Spark SQL 中,算子的迭代器返回相同的可变`Row`对象。
   * 为了正确地 shuffle 这些算子的输出,我们需要在将记录发送到 shuffle 之前执行我们自己的拷贝。
   * 这种拷贝很昂贵,所以我们尽量避免。
   * 此方法封装了选择何时拷贝的逻辑。
   * 从长远来看,我们可能希望将这种逻辑推广到 core 的 shuffle API 中,这样我们就不必依赖 SQL 中的核心内部知识。
   *
   * @param partitioner – shuffle 的 分区器
   * @return 如果在 shuffle 前应拷贝行,则为 true,否则为 false
   */
  private def needToCopyObjectsBeforeShuffle(partitioner: Partitioner): Boolean = {
    // 注意: 尽管我们只使用 partitioner 的'numPartitions'字段,但我们要求不是直接传递分区数,
    // 以防止在某些情况下,使用'numPartitions'分区构造的分区程序可能会输出更少的分区(例如RangePartitioner)。
    val conf = SparkEnv.get.conf
    val shuffleManager = SparkEnv.get.shuffleManager
    val sortBasedShuffleOn = shuffleManager.isInstanceOf[SortShuffleManager]
    val bypassMergeThreshold = conf.get(config.SHUFFLE_SORT_BYPASS_MERGE_THRESHOLD)
    val numParts = partitioner.numPartitions
    if (sortBasedShuffleOn) {
      if (numParts <= bypassMergeThreshold) {
        // 如果我们使用原始的 SortShuffleManager,并且输出分区的数量是如果足够小,
        // Spark 将返回到基于 HASH 的 shuffle 写入路径,该路径不缓冲反序列化的记录。
        false
      } else if (numParts <= SortShuffleManager.MAX_SHUFFLE_OUTPUT_PARTITIONS_FOR_SERIALIZED_MODE) {
        // SPARK-4550 和 SPARK-7081 扩展了基于排序的 shuffle ,这是为了在排序前序列化独立的记录。
        // 这种优化只适用于 shuffle 依赖没有指定聚合或者排序算子,记录序列化器有着确定的配置,并且分区数没有达到上限。
        // 如果启用了这项优化,我们可以安全地避免拷贝。
        // Exchange 永远不会使用聚合或者键排序的 shuffle RDD,Spark SQL 中的序列化器总是会满足这些属性
        // 所以我们只需要检查分区的数量是否超过了限制。
        false
      } else {
        // Spark 的 SortShuffleManager 使用 `ExternalSorter` 在内存中缓存记录,因此我们必须复制。
        true
      }
    } else {
      // 捕获所有的情况, 以安全地处理任何未来的 ShuffleManager 实现。
      true
    }
  }

  /**
   * 返回一个 ShuffleDependence,它将根据 newPartitioning 中定义的分区方案对其子级的行进行分区。
   * 返回的 ShuffleDependence 的那些分区将作为 shuffle 的输入。
   */
  def prepareShuffleDependency(
                                rdd: RDD[InternalRow],
                                outputAttributes: Seq[Attribute],
                                newPartitioning: Partitioning,
                                serializer: Serializer,
                                writeMetrics: Map[String, SQLMetric])
  : ShuffleDependency[Int, InternalRow, InternalRow] = {
    val part: Partitioner = newPartitioning match {
      case RoundRobinPartitioning(numPartitions) => new HashPartitioner(numPartitions)
      case HashPartitioning(_, n) =>
        new Partitioner {
          override def numPartitions: Int = n

          // 对于哈希分区,分区键已经是一个有效的分区 ID,
          // 正如我们使用的`HashPartitioning.partitionIdExpression`来生成分区键。
          override def getPartition(key: Any): Int = key.asInstanceOf[Int]
        }
      case RangePartitioning(sortingExpressions, numPartitions) =>
        // 在`RangePartitioner`中决定分区界限时,仅提取用于排序的字段,以避免收集对排序结果没有影响的大型字段
        val rddForSampling = rdd.mapPartitionsInternal { iter =>
          val projection =
            UnsafeProjection.create(sortingExpressions.map(_.child), outputAttributes)
          val mutablePair = new MutablePair[InternalRow, Null]()
          // 在内部,RangePartitioner 在 RDD 上运行一个作业,这会为了计算分区边界采样分区键。
          // 为了得到准确的样本,我们需要复制可变键。
          iter.map(row => mutablePair.update(projection(row).copy(), null))
        }
        // 在抽取的排序键上构建排序
        val orderingAttributes = sortingExpressions.zipWithIndex.map { case (ord, i) =>
          ord.copy(child = BoundReference(i, ord.dataType, ord.nullable))
        }
        implicit val ordering = new LazilyGeneratedOrdering(orderingAttributes)
        new RangePartitioner(
          numPartitions,
          rddForSampling,
          ascending = true,
          // spark.sql.execution.rangeExchange.sampleSizePerPartition
          samplePointsPerPartitionHint = SQLConf.get.rangeExchangeSampleSizePerPartition)
      case SinglePartition =>
        new Partitioner {
          override def numPartitions: Int = 1

          override def getPartition(key: Any): Int = 0
        }
      case _ => sys.error(s"Exchange not implemented for $newPartitioning")
    }

    def getPartitionKeyExtractor(): InternalRow => Any = newPartitioning match {
      case RoundRobinPartitioning(numPartitions) =>
        // 从一个随机分区开始,在输出分区中均匀分布元素。
        var position = new Random(TaskContext.get().partitionId()).nextInt(numPartitions)
        (row: InternalRow) => {
          // 哈希分区器会根据分区数量来进行 mod
          position += 1
          position
        }
      case h: HashPartitioning =>
        val projection = UnsafeProjection.create(h.partitionIdExpression :: Nil, outputAttributes)
        row => projection(row).getInt(0)
      case RangePartitioning(sortingExpressions, _) =>
        val projection = UnsafeProjection.create(sortingExpressions.map(_.child), outputAttributes)
        row => projection(row)
      case SinglePartition => identity
      case _ => sys.error(s"Exchange not implemented for $newPartitioning")
    }

    val isRoundRobin = newPartitioning.isInstanceOf[RoundRobinPartitioning] &&
      newPartitioning.numPartitions > 1

    val rddWithPartitionIds: RDD[Product2[Int, InternalRow]] = {
      // [SPARK-23207] 必须确保生成的 RoundRobinPartitioning 是确定性的,
      // 否则,重试任务可能会输出不同的行,从而导致数据丢失。
      // 目前,我们遵循最直接的方法来,那就是在分区前执行本地排序。
      // 请注意,如果新分区只有1个分区,则在在这种情况下,所有输出行都会转到同一个分区。
      val newRdd = if (isRoundRobin && SQLConf.get.sortBeforeRepartition) {
        rdd.mapPartitionsInternal { iter =>
          val recordComparatorSupplier = new Supplier[RecordComparator] {
            override def get: RecordComparator = new RecordBinaryComparator()
          }
          // 比较器来比较 row 的哈希码,这应该总是整型值。
          val prefixComparator = PrefixComparators.LONG

          // prefixComputer 生成 row 哈希码作为前缀,
          // 因此我们可以减少当输入行从一个受限的范围内中选择列值时,前缀相等的概率
          val prefixComputer = new UnsafeExternalRowSorter.PrefixComputer {
            private val result = new UnsafeExternalRowSorter.PrefixComputer.Prefix

            override def computePrefix(row: InternalRow):
            UnsafeExternalRowSorter.PrefixComputer.Prefix = {
              // 哈希码基于 [[UnsafeRow]] 的二进制格式生成,不应该为 null.
              result.isNull = false
              result.value = row.hashCode()
              result
            }
          }
          val pageSize = SparkEnv.get.memoryManager.pageSizeBytes

          val sorter = UnsafeExternalRowSorter.createWithRecordComparator(
            StructType.fromAttributes(outputAttributes),
            recordComparatorSupplier,
            prefixComparator,
            prefixComputer,
            pageSize,
            // 我们在这里比较二进制,它不支持基数排序。
            false)
          sorter.sort(iter.asInstanceOf[Iterator[UnsafeRow]])
        }
      } else {
        rdd
      }

      // 如果我们不排序输入的话,round-robin 函数是排序敏感的。
      val isOrderSensitive = isRoundRobin && !SQLConf.get.sortBeforeRepartition
      if (needToCopyObjectsBeforeShuffle(part)) {
        newRdd.mapPartitionsWithIndexInternal((_, iter) => {
          val getPartitionKey = getPartitionKeyExtractor()
          iter.map { row => (part.getPartition(getPartitionKey(row)), row.copy()) }
        }, isOrderSensitive = isOrderSensitive)
      } else {
        newRdd.mapPartitionsWithIndexInternal((_, iter) => {
          val getPartitionKey = getPartitionKeyExtractor()
          val mutablePair = new MutablePair[Int, InternalRow]()
          iter.map { row => mutablePair.update(part.getPartition(getPartitionKey(row)), row) }
        }, isOrderSensitive = isOrderSensitive)
      }
    }

    // 现在,我们手动创建一个 ShuffleDependence。
    // 因为 rddWithPartitionId 中的配对以(partitionId,row)的形式出现,
    // 并且每个 partitionId 都在 [0,part.numPartitions-1] 预期范围内
    // 当前的分区器是是 PartitionIdPassthrough
    val dependency =
    new ShuffleDependency[Int, InternalRow, InternalRow](
      rddWithPartitionIds,
      new PartitionIdPassthrough(part.numPartitions),
      serializer,
      shuffleWriterProcessor = createShuffleWriteProcessor(writeMetrics))

    dependency
  }

  /**
   * 为 SQL 创建一个定制的 ShuffleWriteProcessor,
   * 将默认的 metrics reporter 与 SQLShuffleWriteMetricsReporter 包装为 ShuffleWriteProcessor 的新报告程序。
   */
  def createShuffleWriteProcessor(metrics: Map[String, SQLMetric]): ShuffleWriteProcessor = {
    new ShuffleWriteProcessor {
      override protected def createMetricsReporter(
                                                    context: TaskContext): ShuffleWriteMetricsReporter = {
        new SQLShuffleWriteMetricsReporter(context.taskMetrics().shuffleWriteMetrics, metrics)
      }
    }
  }
}

Broadcast

BroadcastExchangeLike

BroadcastExchangeLike 是所有 broadcast exchange 实现的共同接口,便于模式匹配。

trait BroadcastExchangeLike extends Exchange {

  /**
   * 广播作业的分组 ID
   */
  def runId: UUID = UUID.randomUUID

  /**
   * 准备广播关系的异步作业
   */
  def relationFuture: Future[broadcast.Broadcast[Any]]

  /**
   * 实现广播的异步作业。
   * 它用于注册`relationFuture`上的回调。
   * 请注意,调用此方法可能不会启动广播作业的执行。
   * 它还做准备工作,比如等待子查询。   
   */
  final def submitBroadcastJob: scala.concurrent.Future[broadcast.Broadcast[Any]] = executeQuery {
    completionFuture
  }

  protected def completionFuture: scala.concurrent.Future[broadcast.Broadcast[Any]]

  /**
   * 返回广播物化后的运行时统计信息。
   */
  def runtimeStatistics: Statistics
}

BroadcastExchangeExec

BroadcastExchangeExec 是具体执行的 broadcast exchange,用来收集、转换并最终广播转换后的 SparkPlan 的结果。

case class BroadcastExchangeExec(
    mode: BroadcastMode,
    child: SparkPlan) extends BroadcastExchangeLike {
  import BroadcastExchangeExec._
  /**
   * 广播作业的分组 ID
   */
  override val runId: UUID = UUID.randomUUID

  /**
   * metrics 统计信息
   */
  override lazy val metrics = Map(
    "dataSize" -> SQLMetrics.createSizeMetric(sparkContext, "data size"),
    "numOutputRows" -> SQLMetrics.createMetric(sparkContext, "number of output rows"),
    "collectTime" -> SQLMetrics.createTimingMetric(sparkContext, "time to collect"),
    "buildTime" -> SQLMetrics.createTimingMetric(sparkContext, "time to build"),
    "broadcastTime" -> SQLMetrics.createTimingMetric(sparkContext, "time to broadcast"))

  /**
   * 指定如何在集群中的不同节点之间对数据进行分区。
   */
  override def outputPartitioning: Partitioning = BroadcastPartitioning(mode)

  /**
   * 定义规范化如何适用于当前计划。
   */
  override def doCanonicalize(): SparkPlan = {
    BroadcastExchangeExec(mode.canonicalized, child.canonicalized)
  }

  /**
   * 返回广播物化后的运行时统计信息。
   */
  override def runtimeStatistics: Statistics = {
    val dataSize = metrics("dataSize").value
    val rowCount = metrics("numOutputRows").value
    Statistics(dataSize, Some(rowCount))
  }

  @transient
  private lazy val promise = Promise[broadcast.Broadcast[Any]]()

  @transient
  override lazy val completionFuture: scala.concurrent.Future[broadcast.Broadcast[Any]] =
    promise.future

  /**
   * 用于 broadcast join 中的广播超时时间
   */
  @transient
  private val timeout: Long = conf.broadcastTimeout

  @transient
  private lazy val maxBroadcastRows = mode match {
    case HashedRelationBroadcastMode(key, _)
      // 注意:LongHashedRelation 用于 LongType 的单个键。
      // 这个应该保持着和 HashedRelation.apply 一致。
      if !(key.length == 1 && key.head.dataType == LongType) =>
      // 由于 BytesToBytesMap 支持的最大键数为 1 << 29,
      // 在 UnsafeHashedRelation 之前,只有70%的 slot 可以使用,
      // 这里的限制不应超过 3.41 亿。
      (BytesToBytesMap.MAX_CAPACITY / 1.5).toLong
    case _ => 512000000
  }

  @transient
  override lazy val relationFuture: Future[broadcast.Broadcast[Any]] = {
    SQLExecution.withThreadLocalCaptured[broadcast.Broadcast[Any]](
      session, BroadcastExchangeExec.executionContext) {
          try {
            // 在此处设置作业组,以便在必要时可以通过 groupId 取消作业组。
            sparkContext.setJobGroup(runId.toString, s"broadcast exchange (runId $runId)",
              interruptOnCancel = true)
            val beforeCollect = System.nanoTime()
            // 使用 executeCollect/executeCollectIterator 来避免转换成 Scala 类型
            val (numRows, input) = child.executeCollectIterator()
            longMetric("numOutputRows") += numRows
            if (numRows >= maxBroadcastRows) {
              throw QueryExecutionErrors.cannotBroadcastTableOverMaxTableRowsError(
                maxBroadcastRows, numRows)
            }

            val beforeBuild = System.nanoTime()
            longMetric("collectTime") += NANOSECONDS.toMillis(beforeBuild - beforeCollect)

            // 构建关系
            val relation = mode.transform(input, Some(numRows))

            val dataSize = relation match {
              case map: HashedRelation =>
                map.estimatedSize
              case arr: Array[InternalRow] =>
                arr.map(_.asInstanceOf[UnsafeRow].getSizeInBytes.toLong).sum
              case _ =>
                throw new SparkException("[BUG] BroadcastMode.transform returned unexpected " +
                  s"type: ${relation.getClass.getName}")
            }

            longMetric("dataSize") += dataSize
            if (dataSize >= MAX_BROADCAST_TABLE_BYTES) {
              throw QueryExecutionErrors.cannotBroadcastTableOverMaxTableBytesError(
                MAX_BROADCAST_TABLE_BYTES, dataSize)
            }

            val beforeBroadcast = System.nanoTime()
            longMetric("buildTime") += NANOSECONDS.toMillis(beforeBroadcast - beforeBuild)

            // 广播关系
            val broadcasted = sparkContext.broadcast(relation)
            longMetric("broadcastTime") += NANOSECONDS.toMillis(
              System.nanoTime() - beforeBroadcast)
            val executionId = sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)
            SQLMetrics.postDriverMetricUpdates(sparkContext, executionId, metrics.values.toSeq)
            promise.trySuccess(broadcasted)
            broadcasted
          } catch {
            case oe: OutOfMemoryError =>
              val ex = new SparkFatalException(
                QueryExecutionErrors.notEnoughMemoryToBuildAndBroadcastTableError(oe))
              promise.tryFailure(ex)
              throw ex
            case e if !NonFatal(e) =>
              val ex = new SparkFatalException(e)
              promise.tryFailure(ex)
              throw ex
            case e: Throwable =>
              promise.tryFailure(e)
              throw e
          }
    }
  }

  /**
   * 保证在任何 SparkPlan 执行之前运行。
   * 如果我们想在执行查询之前设置一些状态,例如,BroadcastHashJoin 使用它异步广播,这将非常有用。
   */
  override protected def doPrepare(): Unit = {
    // 具象化 future
    relationFuture
  }

  /**
   * 将查询结果生成为 RDD[InternalRow]
   */
  override protected def doExecute(): RDD[InternalRow] = {
    throw QueryExecutionErrors.executeCodePathUnsupportedError("BroadcastExchange")
  }

  /**
   * 将查询结果作为广播变量生成。
   */
  override protected[sql] def doExecuteBroadcast[T](): broadcast.Broadcast[T] = {
    try {
      relationFuture.get(timeout, TimeUnit.SECONDS).asInstanceOf[broadcast.Broadcast[T]]
    } catch {
      case ex: TimeoutException =>
        logError(s"Could not execute broadcast in $timeout secs.", ex)
        if (!relationFuture.isDone) {
          sparkContext.cancelJobGroup(runId.toString)
          relationFuture.cancel(true)
        }
        throw QueryExecutionErrors.executeBroadcastTimeoutError(timeout, Some(ex))
    }
  }

  override protected def withNewChildInternal(newChild: SparkPlan): BroadcastExchangeExec =
    copy(child = newChild)
}

object BroadcastExchangeExec {
  /**
   * 最大广播表的字节数
   */
  val MAX_BROADCAST_TABLE_BYTES = 8L << 30

  private[execution] val executionContext = ExecutionContext.fromExecutorService(
      ThreadUtils.newDaemonCachedThreadPool("broadcast-exchange",
        SQLConf.get.getConf(StaticSQLConf.BROADCAST_EXCHANGE_MAX_THREAD_THRESHOLD)))
}
  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Spark 3.x与Spark 2.x的区别主要有以下几点: 1. 支持Python 3:Spark 3.x支持Python 3,而Spark 2.x只支持Python 2。 2. 更好的性能:Spark 3.x在性能方面有所提升,包括更快的查询速度和更高的并行度。 3. 更好的SQL支持:Spark 3.x引入了一些新的SQL功能,包括ANSI SQL支持、更好的窗口函数支持和更好的类型推断。 4. 更好的流处理支持:Spark 3.x引入了一些新的流处理功能,包括更好的状态管理和更好的容错性。 5. 更好的机器学习支持:Spark 3.x引入了一些新的机器学习功能,包括更好的特征工程支持和更好的模型解释性。 总的来说,Spark 3.x相对于Spark 2.x来说是一个更加成熟和功能更加丰富的版本。 ### 回答2: Spark 3.x与Spark 2.x有很多显著的不同之处。 首先,Spark 3.x通过引入新的API和更好的优化器提高了性能和可伸缩性。 其次,它更易于使用,使开发人员更容易使用Spark构建复杂的应用程序。以下是Spark 3.x与Spark 2.x的主要区别: 1.新的API: Spark 3.x引入了一些新的API,如Delta Lake、Kubernetes、Pandas UDF等。Delta Lake是一个开源数据湖解决方案,使数据管理、可靠性和性能变得更加容易。有了Kubernetes,Spark可以更好地与容器化环境集成。同时,Pandas UDF支持Python的Pandas库,可以处理大量的数据。 2.优化器的改进: Spark 3.x引入了新的优化器(称为Spark 3.0 Optimizer),可显著提高查询性能。这个优化器使用基于规则的优化技术和成本模型,通过优化查询来提高查询性能。 3.支持更多的数据源: Spark 3.x做了很多工作来改进数据源API。它提供了更广泛的数据源支持,包括Apache Kafka、Amazon S3、Google BigQuery等。 4.增强了机器学习功能: Spark 3.x提供了更多的基于机器学习的库和工具,包括Python的Pandas和Scikit-Learn库的元数据集成,支持PySpark的PythonML库等。 5.交互式查询支持: Spark 3.x引入了新的交互式查询API,这使得Spark变得更加友好。您可以使用Spark SQL进行查询,该工具支持批处理和流处理查询。 总之,Spark 3.x相比Spark 2.x更加强大和易于使用。它提供了更多的API、更好的优化器和更好的可扩展性。这些变化使得Spark在处理大数据方面更加卓越,让开发人员更轻松地构建复杂的应用程序。 ### 回答3: Apache Spark是一个快速、通用,基于内存的分布式计算系统,已成为大数据领域中最受欢迎的计算框架之一。Spark 3.x是Apache Spark计算框架的最新版本,相比于之前的版本有很多新的特性和功能,以下是Spark 3.x与Spark 2.x的主要区别。 1. Python API重构 Python是Apache Spark中最受欢迎的编程语言,但它在之前的版本中没有得到很好的支持。在Spark 3.x中,Python API被重构,在性能和易用性方面都有了大幅改善。 2. 完全支持SQL ANSI标准 Spark 3.x从核心到应用都支持SQL ANSI标准。这意味着,Spark 3.x支持更多的SQL函数和操作,并且更加符合SQL标准。 3. 兼容性增强 Spark 3.x不再依赖于Hadoop,这意味着它能够更好地与其他数据源进行集成。同时,它也支持Kubernetes和Docker的容器化部署方式。 4. AI支持增加 Spark 3.x引入了许多新的机器学习和深度学习算法,例如支持自动编码器和多标签分类器的模型,以及更好的分布式模型训练功能。 5. 其它特性 Spark 3.x还支持Delta Lake,这是一个可靠、高性能的事务性存储。同时,它还提供性能更好的Spark流式处理API和更好的结构化API,这些API在处理大规模结构化数据时更加高效。 总之,Spark 3.x相比于Spark 2.x在性能、兼容性、AI支持和其它特性方面都有很大的改进。无论是开发人员还是数据科学家,Spark 3.x都能够提供更好的用户体验和更高的数据处理效率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值