前言
本文隶属于专栏《大数据技术体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见大数据技术体系
WHAT
Spark 中的 Exchange
指的是那些可以在多线程/进程之间交换数据的算子,它是 Spark 能够实现并行处理的关键所在。
Spark 中的数据交换只有 2 种类型:
- Shuffle
- 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 个方法:
- QueryPlan.output:表示查询计划的输出
- TreeNode.nodePatterns:这是用来标识当前的节点是属于哪一类的
- 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)))
}