1.Spark 底层逻辑
导读
- 从部署图了解
Spark部署了什么, 有什么组件运行在集群中 - 通过对
WordCount案例的解剖, 来理解执行逻辑计划的生成 - 通过对逻辑执行计划的细化, 理解如何生成物理计划
如无特殊说明, 以下部分均针对于 Spark Standalone 进行介绍 |
|---|
部署情况
在 Spark 部分的底层执行逻辑开始之前, 还是要先认识一下 Spark 的部署情况, 根据部署情况, 从而理解如何调度.
针对于上图, 首先可以看到整体上在集群中运行的角色有如下几个:
-
Master Daemon负责管理
Master节点, 协调资源的获取, 以及连接Worker节点来运行Executor, 是 Spark 集群中的协调节点 -
Worker DaemonWorkers也称之为叫Slaves, 是 Spark 集群中的计算节点, 用于和 Master 交互并管理Executor.当一个
Spark Job提交后, 会创建SparkContext, 后Worker会启动对应的Executor. -
Executor Backend上面有提到
Worker用于控制Executor的启停, 其实Worker是通过Executor Backend来进行控制的,Executor Backend是一个进程(是一个JVM实例), 持有一个Executor对象
另外在启动程序的时候, 有三种程序需要运行在集群上:
-
DriverDriver是一个JVM实例, 是一个进程, 是Spark Application运行时候的领导者, 其中运行了SparkContext.Driver控制Job和Task, 并且提供WebUI. -
ExecutorExecutor对象中通过线程池来运行Task, 一个Executor中只会运行一个Spark Application的Task, 不同的Spark Application的Task会由不同的Executor来运行
案例
因为要理解执行计划, 重点不在案例, 所以本节以一个非常简单的案例作为入门, 就是我们第一个案例 WordCount
val sc = ...
val textRDD = sc.parallelize(Seq("Hadoop Spark", "Hadoop Flume", "Spark Sqoop"))
val splitRDD = textRDD.flatMap(_.split(" "))
val tupleRDD = splitRDD.map((_, 1))
val reduceRDD = tupleRDD.reduceByKey(_ + _)
val strRDD = reduceRDD.map(item => s"${item._1}, ${item._2}")
println(strRDD.toDebugString)
strRDD.collect.foreach(item => println(item))
整个案例的运行过程大致如下:
- 通过代码的运行, 生成对应的
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 断开
1.1. 逻辑执行图生成
导读
- 如何生成 RDD
- 如何控制 RDD 之间的关系
6.1.1. RDD 的生成
重点内容
本章要回答如下三个问题
- 如何生成 RDD
- 生成什么 RDD
- 如何计算 RDD 中的数据
val sc = ...
val textRDD = sc.parallelize(Seq("Hadoop Spark", "Hadoop Flume", "Spark Sqoop"))
val splitRDD = textRDD.flatMap(_.split(" "))
val tupleRDD = splitRDD.map((_, 1))
val reduceRDD = tupleRDD.reduceByKey(_ + _)
val strRDD = reduceRDD.map(item => s"${item._1}, ${item._2}")
println(strRDD.toDebugString)
strRDD.collect.foreach(item => println(item))
明确逻辑计划的边界
在 Action 调用之前, 会生成一系列的 RDD, 这些 RDD 之间的关系, 其实就是整个逻辑计划
例如上述代码, 如果生成逻辑计划的, 会生成如下一些 RDD, 这些 RDD 是相互关联的, 这些 RDD 之间, 其实本质上生成的就是一个 计算链
接下来, 采用迭代渐进式的方式, 一步一步的查看一下整体上的生成过程
textFile 算子的背后
研究 RDD 的功能或者表现的时候, 其实本质上研究的就是 RDD 中的五大属性, 因为 RDD 透过五大属性来提供功能和表现, 所以如果要研究 textFile 这个算子, 应该从五大属性着手, 那么第一步就要看看生成的 RDD 是什么类型的 RDD
-
textFile生成的是HadoopRDD除了上面这一个步骤以外, 后续步骤将不再直接基于代码进行讲解, 因为从代码的角度着手容易迷失逻辑, 这个章节的初心有两个, 一个是希望大家了解 Spark 的内部逻辑和原理, 另外一个是希望大家能够通过本章学习具有代码分析的能力 -
HadoopRDD的Partitions对应了HDFS的Blocks其实本质上每个
HadoopRDD的Partition都是对应了一个Hadoop的Block, 通过InputFormat来确定Hadoop中的Block的位置和边界, 从而可以供一些算子使用 -
HadoopRDD的compute函数就是在读取HDFS中的Block本质上,
compute还是依然使用InputFormat来读取HDFS中对应分区的Block -
textFile这个算子生成的其实是一个MapPartitionsRDDtextFile这个算子的作用是读取HDFS上的文件, 但是HadoopRDD中存放是一个元组, 其Key是行号, 其Value是Hadoop中定义的Text对象, 这一点和MapReduce程序中的行为是一致的但是并不适合
Spark的场景, 所以最终会通过一个map算子, 将(LineNum, Text)转为String形式的一行一行的数据, 所以最终textFile这个算子生成的RDD并不是HadoopRDD, 而是一个MapPartitionsRDD
map 算子的背后
-
map算子生成了MapPartitionsRDD由源码可知, 当
val rdd2 = rdd1.map()的时候, 其实生成的新RDD是rdd2,rdd2的类型是MapPartitionsRDD, 每个RDD中的五大属性都会有一些不同, 由map算子生成的RDD中的计算函数, 本质上就是遍历对应分区的数据, 将每一个数据转成另外的形式 -
MapPartitionsRDD的计算函数是collection.map( function )真正运行的集群中的处理单元是
Task, 每个Task对应一个RDD的分区, 所以collection对应一个RDD分区的所有数据, 而这个计算的含义就是将一个RDD的分区上所有数据当作一个集合, 通过这个Scala集合的map算子, 来执行一个转换操作, 其转换操作的函数就是传入map算子的function -
传入
map算子的函数会被清理这个清理主要是处理闭包中的依赖, 使得这个闭包可以被序列化发往不同的集群节点运行
flatMap 算子的背后
flatMap 和 map 算子其实本质上是一样的, 其步骤和生成的 RDD 都是一样, 只是对于传入函数的处理不同, map 是 collect.map( function ) 而 flatMap 是 collect.flatMap( function )
从侧面印证了, 其实 Spark 中的 flatMap 和 Scala 基础中的 flatMap 其实是一样的
textRDD → splitRDD → tupleRDD
由 textRDD 到 splitRDD 再到 tupleRDD 的过程, 其实就是调用 map 和 flatMap 算子生成新的 RDD 的过程, 所以如下图所示, 就是这个阶段所生成的逻辑计划
总结
如何生成 RDD ?
生成 RDD 的常见方式有三种
- 从本地集合创建
- 从外部数据集创建
- 从其它
RDD衍生
通过外部数据集创建 RDD, 是通过 Hadoop 或者其它外部数据源的 SDK 来进行数据读取, 同时如果外部数据源是有分片的话, RDD 会将分区与其分片进行对照
通过其它 RDD 衍生的话, 其实本质上就是通过不同的算子生成不同的 RDD 的子类对象, 从而控制 compute 函数的行为来实现算子功能
生成哪些 RDD ?
不同的算子生成不同的 RDD, 生成 RDD 的类型取决于算子, 例如 map 和 flatMap 都会生成 RDD 的子类 MapPartitions 的对象
如何计算 RDD 中的数据 ?
虽然前面我们提到过 RDD 是偏向计算的, 但是其实 RDD 还只是表示数据, 纵观 RDD 的五大属性中有三个是必须的, 分别如下
Partitions List分区列表Compute function计算函数Dependencies依赖
虽然计算函数是和计算有关的, 但是只有调用了这个函数才会进行计算, RDD 显然不会自己调用自己的 Compute 函数, 一定是由外部调用的, 所以 RDD 更多的意义是用于表示数据集以及其来源, 和针对于数据的计算
所以如何计算 RDD 中的数据呢? 一定是通过其它的组件来计算的, 而计算的规则, 由 RDD 中的 Compute 函数来指定, 不同类型的 RDD 子类有不同的 Compute 函数