6. Spark 底层逻辑 -- 简介
导读
从部署图了解
Spark
部署了什么, 有什么组件运行在集群中通过对
WordCount
案例的解剖, 来理解执行逻辑计划的生成通过对逻辑执行计划的细化, 理解如何生成物理计划
如无特殊说明, 以下部分均针对于 Spark Standalone 进行介绍
通过wordCount案例来从“部署情况”、“逻辑执行图”、“物理执行图”来初步了解 spark的底层逻辑
部署情况
在 Spark
部分的底层执行逻辑开始之前, 还是要先认识一下 Spark
的部署情况, 根据部署情况, 从而理解如何调度.
针对于上图, 首先可以看到整体上在集群中运行的角色有如下几个:
-
Master Daemon
负责管理
Master
节点, 协调资源的获取, 以及连接Worker
节点来运行Executor
, 是 Spark 集群中的协调节点 -
Worker Daemon
Workers
也称之为叫Slaves
, 是 Spark 集群中的计算节点, 用于和 Master 交互并管理Executor
.当一个
Spark Job
提交后, 会创建SparkContext
, 后Worker
会启动对应的Executor
. -
Executor Backend
上面有提到
Worker
用于控制Executor
的启停, 其实Worker
是通过Executor Backend
来进行控制的,Executor Backend
是一个进程(是一个JVM
实例), 持有一个Executor
对象
另外在启动程序的时候, 有三种程序需要运行在集群上:
-
Driver
Driver
是一个JVM
实例, 是一个进程, 是Spark Application
运行时候的领导者, 其中运行了SparkContext
.Driver
控制Job
和Task
, 并且提供WebUI
. -
Executor
Executor
对象中通过线程池来运行Task
, 一个Executor
中只会运行一个Spark Application
的Task
, 不同的Spark Application
的Task
会由不同的Executor
来运行
案例
因为要理解执行计划, 重点不在案例, 所以本节以一个非常简单的案例作为入门, 就是我们第一个案例 WordCount
@Test
def wordCount():Unit = {
//1.创建sc对象
val conf = new SparkConf().setMaster("local[6]").setAppName("wordCount_source")
val sc = new SparkContext(conf)
//2.创建数据集
val textRDD: RDD[String] = sc.parallelize(Seq("hadoop spark","hadoop flume","spark sqoop"))
//3.数据处理
//3.1拆词
val splitRDD: RDD[String] = textRDD.flatMap(_.split(" "))
//3.2赋予初始词频
val tupleRDD: RDD[(String, Int)] = splitRDD.map((_,1))
//3.3聚合统计词频
val reduceRDD: RDD[(String, Int)] = tupleRDD.reduceByKey(_ + _)
//3.4将结果转为字符串
val strRDD: RDD[String] = reduceRDD.map(item => s"${item._1},${item._2}")
//4.结果获取
strRDD.collect().foreach(println(_))
//5.关闭sc,执行
sc.stop()
//逻辑执行图:描述数据如何流动,如何计算
// textRDD -> splitRDD -> tupleRDD -> reduceRDD -> strRDD
// 预执行,生成RDD逻辑关系 println(strRDD.toDebugString)
/*
(6) MapPartitionsRDD[4] at map at SourceAnalysis.scala:26 []
| ShuffledRDD[3] at reduceByKey at SourceAnalysis.scala:24 []
+-(6) MapPartitionsRDD[2] at map at SourceAnalysis.scala:22 []
| MapPartitionsRDD[1] at flatMap at SourceAnalysis.scala:20 []
| ParallelCollectionRDD[0] at parallelize at SourceAnalysis.scala:16 []
*/
}
整个案例的运行过程大致如下:
-
通过代码的运行, 生成对应的
RDD
逻辑执行图 -
通过
Action
操作, 根据逻辑执行图生成对应的物理执行图, 也就是Stage
和Task
-
将物理执行图运行在集群中
逻辑执行图
对于上面代码中的 reduceRDD
如果使用 toDebugString
打印调试信息的话, 会显式如下内容
(6) MapPartitionsRDD[4] at map at WordCount.scala:20 []
| ShuffledRDD[3] at reduceByKey at WordCount.scala:19 []
+-(6) MapPartitionsRDD[2] at map at WordCount.scala:18 []
| MapPartitionsRDD[1] at flatMap at WordCount.scala:17 []
| ParallelCollectionRDD[0] at parallelize at WordCount.scala:16 []
根据这段内容, 大致能得到这样的一张逻辑执行图
其实 RDD 并没有什么严格的逻辑执行图和物理执行图的概念, 这里也只是借用这个概念, 从而让整个 RDD 的原理可以解释, 好理解.
对于 RDD 的逻辑执行图, 起始于第一个入口 RDD 的创建, 结束于 Action 算子执行之前, 主要的过程就是生成一组互相有依赖关系的 RDD, 其并不会真的执行, 只是表示 RDD 之间的关系, 数据的流转过程.
物理执行图
当触发 Action 执行的时候, 这一组互相依赖的 RDD 要被处理, 所以要转化为可运行的物理执行图, 调度到集群中执行.
因为大部分 RDD 是不真正存放数据的, 只是数据从中流转, 所以, 不能直接在集群中运行 RDD, 要有一种 Pipeline 的思想, 需要将这组 RDD 转为 Stage 和 Task, 从而运行 Task, 优化整体执行速度.
以上的逻辑执行图会生成如下的物理执行图, 这一切发生在 Action 操作被执行时.
从上图可以总结如下几个点
-
在第一个
Stage
中, 每一个这样的执行流程是一个Task
, 也就是在同一个 Stage 中的所有 RDD 的对应分区, 在同一个 Task 中执行 -
Stage 的划分是由 Shuffle 操作来确定的, 有 Shuffle 的地方, Stage 断开