先来看个现象,下图中一个sql任务居然有多个job并行跑,为什么呢?
不错看到这里是不是有很多疑问,下面我就带着这些疑问,从以下几方面一一解答。
1. 看看Spark的调度框架是否支持并行提交多个job(引用了些其他博主的内容)
2. 讲解SparkSQL的ThriftServer入口,为后面SQL并行提交Job做铺垫
3. 讲解在非自适应与自适应情况下SQL的并行提交Job的机制(原创)
此章节为转载,其余部分为原创
df.write.partitionBy("type", "interval").mode("append").parquet("s3://data")
var df = spark.read.json("person.json").repartition(55)
// df.cache()
// val c = df.count()
// println(s"${c}")
val jobExecutor = Executors.newFixedThreadPool(5)
for( _ <- Range(0, 5) ) {
jobExecutor.execute(new Runnable {
override def run(): Unit = {
val id = UUID.randomUUID().toString()
df.coalesce(11).write.json(s"hdfs://ns1/user/root/data/test/${id}")
}
})
}
Thrift 是一种接口描述语言和二进制通信协议,由 Facebook 开发并贡献到 Apache 开源社区,用来定义和创建跨语言的服务 。 Thrift包含的代码生成引擎可以应用于多种语言中,包括C ++、 Java 、 Python 等 。其数据传输采用二进制格式,相对常用的 XML 和 JSON 格式体积更小,在多语言、高并发和大数据场景下更具优势 。
Thrift 框架支持使用 IDL (Interface Definition Language)定义服务接口,然后利用提供的编译器将服务接口编译成不同语言的实现代码,从而实现服务端和客户端跨语言的支持 。 SparkThriftServer 中定义的 Thrift的协议在 if 目录下的 TCLIService. thrift文件中 。客户端与服务端工作的原理如下图所示,协议层( Protocol)、传输层( Transport)乃至底层 IO传输的具体实现都不需要用户关心 。
Spark 中启动ThriftServer 的主要流程 :
整个服务的生命周期从执行。sbin 文件夹下的 start-thriftserver.sh 脚本开始直到执行 stop-thriftserver脚本结束。
最终调用 sparkSubmit 接口提交org.apache.spark.sql.hive.thriftserver.HiveThriftServer2应用。
def execute() : Unit = {
****************************************
result = sqlContext.sql (statement) // 构造逻辑计划阶段和物理计划阶段, 最终得到 的是 DataFrame 数据类型
****************************************
iter = {
val useincrementalCollect = sqlContext.getConf(
" spark.sql.thriftServer.incrementalCollect","false").toBoolean
if (useincrementalCollect) {
result.toLocaliterator.asScala
} else {
****************************************
result.collect().iterator // 启动runJob
****************************************
val (itra, itrb) = iter. duplicate
IterHead = itra
iter = itrb
dataTypes = result.queryExecution.analyzed.output.map(_.dataType).toArray
…………………………………………………………………………………………………………………………………………
}
举个例子TPC-DS中标准SQL第一个sql为例子来说明并行Job:
with customer_total_return as
(select sr_customer_sk as ctr_customer_sk
,sr_store_sk as ctr_store_sk
,sum(SR_FEE) as ctr_total_return
from store_returns
,date_dim
where sr_returned_date_sk = d_date_sk
and d_year =2001
group by sr_customer_sk
,sr_store_sk)
select c_customer_id
from customer_total_return ctr1
,store
,customer
where ctr1.ctr_total_return > (select avg(ctr_total_return)*1.2
from customer_total_return ctr2
where ctr1.ctr_store_sk = ctr2.ctr_store_sk)
and s_store_sk = ctr1.ctr_store_sk
and s_state = 'MI'
and ctr1.ctr_customer_sk = c_customer_sk
order by c_customer_id
limit 100;
在开启与关闭自适应情况下来比对对比生成的并行Job数:
上图中看到明显开启spark.sql.adaptor.enabled=true情况下生成的并行Job更多,下面我们分析一下两种情况的执行计划。
关闭自适应情况下执行计划如下,根节点为TakeOrderAndProject,如下图所示(由于DAG图比较庞大,只截取了一部分):
开启自适应情况下,根节点为AdaptiveSparkPlan,他的子节点才为TakeOrderAndProject,如下图所示(DAG部分截图)。
有上一章节中已经指定SQL的提交过程,并且SparkExecuteStatementOperation#execute主方法中执行了sqlContext.sql()进行了构造逻辑计划阶段和物理计划阶段, 最终得到 的是 DataFrame 数据类型。调用***result.collect()***真正启动了一个job。流程如下图所示:
从上图中可以看到主Job是由HiveThriftServer2驱动的DataSet.collect来触发的,上面例子用跟节点为TakeOrderAndProjectExec来走的流程,实际后期调用的还是RDD#takeOrdered来触发的。
SparkPlan是一颗庞大的树,上一章节中提到DataSet#collectFormPlan调用到SparkPlan#executeCollect此方法可以是其他类型的跟节点,目前继承的有下图这些,当开启自适应则调用的是AdaptiveSparkPlanExec#executeCollect方法:
其中自适应查询包adaptive的QueryStageExec有两个继承类BroadcastQueryStageExec与ShuffleQueryStageExec。
子Job并行启动的所有流程,如下图所示:
以上就是对SparkSQL并行执行多个Job的所有探索,与一个Job转成DAG从而划分层多个Stage不是同层次的东东,希望能帮助到大家!后面我会陆续补上Spark-Core的相关文章。
参考引用:
[1]: 《SparkSQL内核剖析》
[2]: http://spark.apache.org/
[3]: https://blog.csdn.net/zwgdft/article/details/88349295