如何知道(广播)连接查询中的Spark作业和阶段的数量?

时间:2018-03-20 13:36:56

标签: apache-spark apache-spark-sql

我使用Spark 2.1.2。

我正在尝试了解各种火花UI标签显示作为一个工作运行。我使用spark-shell --master local并执行以下join查询:

val df = Seq(
  (55, "Canada", -1, "", 0),
  (77, "Ontario", 55, "/55", 1),
  (100, "Toronto", 77, "/55/77", 2),
  (104, "Brampton", 100, "/55/77/100", 3)
).toDF("id", "name", "parentId", "path", "depth")

val dfWithPar = df.as("df1").
  join(df.as("df2"), $"df1.parentId" === $"df2.Id", "leftouter").
  select($"df1.*", $"df2.name" as "parentName")

dfWithPar.show

这是物理查询计划:

== Physical Plan ==
*Project [Id#11, name#12, parentId#13, path#14, depth#15, name#25 AS parentName#63]
+- *BroadcastHashJoin [parentId#13], [Id#24], LeftOuter, BuildRight
   :- LocalTableScan [Id#11, name#12, parentId#13, path#14, depth#15]
   +- BroadcastExchange HashedRelationBroadcastMode(List(input[0, string, true]))
      +- LocalTableScan [Id#24, name#25]

我有两个关于查询执行的问题。

  1. 为什么查询有两个作业?

    Spark job view

  2. 为什么两个作业的舞台视图相同?下面是作业ID 1的阶段视图的截图,它与作业ID 0完全相同。为什么?

    Stage view of Stage 1 which is exactly same as Stage 0

1 个答案:

答案 0 :(得分:10)

我使用 Spark 2.3.0 来回答您的问题(实际上是 2.3.1-SNAPSHOT ),因为它是撰写本文时的最新版本。由于2.1.2和我的2.3.0中的物理查询计划完全相同(圆括号中的per-query codegen stage ID除外),因此查询执行(如果有任何重要)的变化很小。

dfWithPar.show结构化查询(使用Spark SQL的Scala数据集API构建)优化后面的物理查询计划(我在答案中包含它以便更好地理解) )。

scala> dfWithPar.explain
== Physical Plan ==
*(1) Project [Id#11, name#12, parentId#13, path#14, depth#15, name#24 AS parentName#58]
+- *(1) BroadcastHashJoin [parentId#13], [Id#23], LeftOuter, BuildRight
   :- LocalTableScan [Id#11, name#12, parentId#13, path#14, depth#15]
   +- BroadcastExchange HashedRelationBroadcastMode(List(cast(input[0, int, false] as bigint)))
      +- LocalTableScan [Id#23, name#24]

Spark作业数量

  

为什么查询有两个作业?

我说甚至还有三个Spark工作。

Spark jobs of the broadcast join query in web UI

tl; dr 一个Spark作业适用于BroadcastHashJoinExec物理运算符,而另外两个适用于Dataset.show

为了理解查询执行和结构化查询的Spark作业数量,了解结构化查询(使用数据集API描述)和RDD API之间的区别非常重要。

Spark SQL的数据集和Spark Core的RDD都描述了Spark中的分布式计算。 RDD是Spark"汇编程序"语言(类似于JVM字节码),而数据集是使用类似SQL语言的结构化查询的更高级描述(类似于Scala或Java之类的JVM语言,与我之前使用的JVM字节码相比)。

重要的是,使用数据集API的结构化查询最终会最终成为基于RDD的分布式计算(可以与Java或Scala编译器将高级语言转换为JVM字节码的方式进行比较)。

数据集API是对RDD API的抽象,当您在DataFrame或数据集上调用该操作时,该操作会将其转换为RDD。

有了这个,你不应该感到惊讶Dataset.show最终会调用RDD操作,而RDD操作又会运行零个,一个或多个Spark工作。

Dataset.show(默认情况下numRows等于20)最后调用showStringtake(numRows + 1)获得Array[Row]

val takeResult = newDf.select(castCols: _*).take(numRows + 1)

换句话说,dfWithPar.show()相当于dfWithPar.take(21),而dfWithPar.head(21)相当于show,就Spark作业的数量而言。

您可以在SQL选项卡中查看它们及其作业数。它们都应该是平等的。

SQL tab in web UI

takeheadBroadcastHashJoinExec都会导致collectFromPlan触发Spark工作(通过调用executeCollect)。

您应该确定回答关于作业数量的问题是了解查询中的所有物理运算符是如何工作的。您只需要知道它们在运行时的行为以及它们是否会触发Spark作业。

enter image description here

BroadcastHashJoin和BroadcastExchangeExec物理运营商

当可以广播联接的右侧时,使用

spark.sql.autoBroadcastJoinThreshold二进制物理运算符(默认情况下,10M正好是BroadcastExchangeExec。)

BroadcastHashJoinExec一元物理运算符用于向工作节点广播(关系的)行(以支持BroadcastHashJoinExec)。

执行RDD[InternalRow]时(生成BroadcastExchangeExec),creates a broadcast variable执行// Just a single Spark job for the broadcast variable val r = dfWithPar.rdd separate thread上)。

这就是运行运行ThreadPoolExecutor.java:1149 Spark作业0的原因。

如果执行以下操作,您可以看到单个Spark作业0已运行:

show

这要求执行结构化查询以生成RDD,然后该RDD是提供最终结果的操作的目标。

enter image description here

如果你没有结束广播连接查询,你就不会有Spark工作。

RDD.take运算符

我在回答问题的第一时刻错过的是数据集运算符,即takeheadRDD.take,最终会导致{{1} }。

  

take(num:Int):Array [T] 获取RDD的前几个num元素。它首先扫描一个分区,然后使用该分区的结果来估计满足限制所需的额外分区数。

请注意take"它首先扫描一个分区,并使用该分区的结果来估算满足限制所需的其他分区数。&#34 ; 这是了解广播联接查询中Spark作业数量的关键。

每次迭代(在上面的描述中)都是separate Spark job starting with the very first partition and 4 times as many every following iteration

// RDD.take
def take(num: Int): Array[T] = withScope {
  ...
  while (buf.size < num && partsScanned < totalParts) {
    ...
    val res = sc.runJob(this, (it: Iterator[T]) => it.take(left).toArray, p)
    ...
  }
}

请查看以下RDD.take行,包含21行。

// The other two Spark jobs
r.take(21)

您将在查询中获得2个Spark职位。

enter image description here

猜测执行dfWithPar.show(1)时您将拥有多少个Spark作业。

为什么阶段相同?

  

为什么两个作业的舞台视图相同?下面是作业ID 1的阶段视图的截图,它与作业ID 0完全相同。为什么?

这很容易回答,因为两个Spark工作都来自RDD.take(20)

第一个Spark作业是扫描第一个分区,因为没有足够的行导致另一个Spark作业扫描更多分区。