文章目录

 

1.Spark 核心功能

SparkCore核心机制详解_Spark

Spark Core 提供 Spark 最基础的最核心的功能,主要包括:

SparkContext

通常而言,DriverApplication 的执行与输出都是通过 SparkContext 来完成的,在正式提交 Application 之前,首先需要初始化 SparkContext。SparkContext 隐藏了网络通信、分布式 部署、消息通信、存储能力、计算能力、缓存、测量系统、文件服务、Web 服务等内容, 应用程序开发者只需要使用 SparkContext 提供的 API 完成功能开发。

SparkContext 内置的 DAGScheduler 负责创建 Job,将 DAG 中的 RDD 划分到不同的 Stage, 提交 Stage 等功能。

SparkContext 内置的 TaskScheduler 负责资源的申请、任务的提交及请求集群对任务的调度 等工作。

存储体系

Spark 优先考虑使用各节点的内存作为存储,当内存不足时才会考虑使用磁盘,这极大地 减少了磁盘 I/O,提升了任务执行的效率,使得 Spark 适用于实时计算、流式计算等场景。 此外,Spark 还提供了以内存为中心的高容错的分布式文件系统 Tachyon 供用户进行选择。 Tachyon 能够为 Spark 提供可靠的内存级的文件共享服务。

计算引擎

计算引擎由 SparkContext 中的 DAGScheduler、RDD 以及具体节点上的 Executor 负责执行 的 Map 和 Reduce 任务组成。DAGScheduler 和 RDD 虽然位于 SparkContext 内部,但是在 任务正式提交与执行之前将 Job 中的 RDD 组织成有向无关图(简称 DAG)、并对 Stage 进 行划分决定了任务执行阶段任务的数量、迭代计算、shuffle 等过程。

部署模式

由于单节点不足以提供足够的存储及计算能力,所以作为大数据处理的 Spark 在SparkContext 的 TaskScheduler 组件中提供了对 Standalone 部署模式的实现和 YARN、Mesos 等分布式资源管理系统的支持。通过使用 Standalone、YARN、Mesos、kubernetes、Cloud 等部署模式为 Task 分配计算资源,提高任务的并发执行效率。除了可用于实际生产环境 的 Standalone、YARN、Mesos、kubernetes、Cloud 等部署模式外,Spark 还提供了 Local 模式和 local-cluster 模式便于开发和调试

2.Spark 扩展功能

为了扩大应用范围,Spark 陆续增加了一些扩展功能,主要包括:

Spark SQL

由于 SQL 具有普及率高、学习成本低等特点,为了扩大 Spark 的应用面,因此增加了对 SQL 及 Hive 的支持。Spark SQL 的过程可以总结为:首先使用 SQL 语句解析器(SqlParser) 将 SQL 转换为语法树(Tree),并且使用规则执行器(RuleExecutor)将一系列规则(Rule) 应用到语法树,最终生成物理执行计划并执行的过程。其中,规则包括语法分析器 (Analyzer)和优化器(Optimizer)。Hive 的执行过程与 SQL 类似。

Spark Streaming

Spark Streaming与Apache Storm类似,也用于流式计算。Spark Streaming支持Kafka、Flume、 Twitter、MQTT、ZeroMQ、Kinesis 和简单的 TCP 套接字等多种数据输入源。输入流接收器 (Receiver)负责接入数据,是接入数据流的接口规范。Dstream 是 Spark Streaming 中所 有数据流的抽象,Dstream 可以被组织为 DStreamGraph。Dstream 本质上由一系列连续的 RDD 组成

Spark GraphX

Spark 提 供 的 分 布 式 图 计 算 框 架 。 GraphX 主 要 遵 循 整 体 同 步 并 行 计 算 模 式 (BulkSynchronous Parallell,简称 BSP)下的 Pregel 模型实现。GraphX 提供了对图的抽象 Graph,Graph 由顶点(Vertex)、边(Edge)及继承了 Edge 的 EdgeTriplet(添加了 srcAttr 和 dstAttr 用来保存源顶点和目的顶点的属性)三种结构组成。GraphX 目前已经封装了最 短路径、网页排名、连接组件、三角关系统计等算法的实现,用户可以选择使用。

Spark MLlib

Spark 提供的机器学习框架。机器学习是一门涉及概率论、统计学、逼近论、凸分析、算 法复杂度理论等多领域的交叉学科。MLlib 目前已经提供了基础统计、分类、回归、决策 树、随机森林、朴素贝叶斯、保序回归、协同过滤、聚类、维数缩减、特征提取与转型、 频繁模式挖掘、预言模型标记语言、管道等多种数理统计、概率论、数据挖掘方面的数 学算法。

3.Spark 核心概念

大多数应该都要有实际写过 Spark 程序和提交任务到 Spark 集群后才有更好的理解

1、Application:表示你的应用程序,包含一个 Driver Program 和若干 Executor

2、Driver Program:Spark 中的 Driver 即运行上述 Application 的 main()函数并且创建 SparkContext,其中创建 SparkContext 的目的是为了准备 Spark 应用程序的运行环境。由 SparkContext 负责与 ClusterManager 通信,进行资源的申请,任务的分配和监控等。程序执 行完毕后关闭 SparkContext

3、ClusterManager:在 Standalone 模式中即为 Master(主节点),控制整个集群,监控 Worker。 在 YARN 模式中为资源管理器。

4、SparkContext:整个应用的上下文,控制应用程序的生命周期,负责调度各个运算资源, 协调各个 Worker 上的 Executor。初始化的时候,会初始化 DAGScheduler 和 TaskScheduler 两个核心组件。

5、RDD:Spark 的基本计算单元,一组 RDD 可形成执行的有向无环图 RDD Graph。

6、DAGScheduler:根据 Job 构建基于 Stage 的 DAG,并提交 Stage 给 TaskScheduler,其划分 Stage 的依据是 RDD 之间的依赖关系:宽依赖,也叫 shuffle 依赖

7、TaskScheduler:将 TaskSet 提交给 Worker(集群)运行,每个 Executor 运行什么 Task 就 是在此处分配的。

8、Worker:集群中可以运行 Application 代码的节点。在 Standalone 模式中指的是通过 slave 文件配置的 worker 节点,在 Spark on Yarn 模式中指的就是 NodeManager 节点。

9、Executor:某个 Application 运行在 Worker 节点上的一个进程,该进程负责运行某些 task, 并且负责将数据存在内存或者磁盘上。在 Spark on Yarn 模式下,其进程名称为 CoarseGrainedExecutorBackend,一个 CoarseGrainedExecutorBackend 进程有且仅有一个 executor 对象,它负责将 Task 包装成 taskRunner,并从线程池中抽取出一个空闲线程运行 Task, 这样,每个 CoarseGrainedExecutorBackend 能并行运行 Task 的数据就取决于分配给它的 CPU 的个数。

10、Stage:每个 Job 会被拆分很多组 Task,每组作为一个 TaskSet,其名称为 Stage

11、Job:包含多个 Task 组成的并行计算,是由 Action 行为触发的

12、Task:在 Executor 进程中执行任务的工作单元,多个 Task 组成一个 Stage

13、SparkEnv:线程级别的上下文,存储运行时的重要组件的引用。SparkEnv 内创建并包含 如下一些重要组件的引用。

MapOutPutTracker:负责 Shuffle 元信息的存储。

BroadcastManager:负责广播变量的控制与元信息的存储。

BlockManager:负责存储管理、创建和查找块。

MetricsSystem:监控运行时性能指标信息。

SparkConf:负责存储配置信息。

SparkCore核心机制详解_Spark_02

4.Spark 基本架构

从集群部署的角度来看,Spark 集群由以下部分组成:

Cluster Manager:Spark 的集群管理器,主要负责资源的分配与管理。集群管理器分配的资 源属于一级分配,它将各个 Worker 上的内存、CPU 等资源分配给应用程序,但是并不负责 对 Executor 的资源分配。目前,Standalone、YARN、Mesos、K8S,EC2 等都可以作为 Spark 的集群管理器。

Master:Spark 集群的主节点。

Worker:Spark 集群的工作节点。对 Spark 应用程序来说,由集群管理器分配得到资源的 Worker 节点主要负责以下工作:创建 Executor,将资源和任务进一步分配给 Executor,同步 资源信息给 Cluster Manager。

Executor:执行计算任务的一些进程。主要负责任务的执行以及与 Worker、Driver Application 的信息同步。

SparkCore核心机制详解_spark_03

Driver Appication:客户端驱动程序,也可以理解为客户端应用程序,用于将任务程序转换 为 RDD 和 DAG,并与 Cluster Manager 进行通信与调度。

这些组成部分之间的整体关系如下图所示:

SparkCore核心机制详解_Spark_04

Spark 计算平台有两个重要角色,Driver 和 executor,不论是 StandAlone 模式还是 YARN 模式, 都是 Driver 充当 Application 的 master 角色,负责任务执行计划生成和任务分发及调度; executor 充当 worker 角色,负责实际执行任务的 task,计算的结果返回 Driver。

5.Spark 编程模型

Spark 应用程序从编写到提交、执行、输出的整个过程如下图所示:

SparkCore核心机制详解_spark_05

图中描述的步骤如下:

  1. 用户使用 SparkContext 提供的 API(常用的有 textFile、sequenceFile、runJob、stop 等) 编写Driver Application程序。此外SQLContext、HiveContext及StreamingContext对SparkContext 进行封装,并提供了 SQL、Hive 及流式计算相关的 API。

  2. 使用 SparkContext 提交的用户应用程序,首先会使用 BlockManager 和 BroadcastManager 将任务的 Hadoop 配置进行广播。然后由 DAGScheduler 将任务转换为 RDD 并组织成 DAG, DAG 还将被划分为不同的 Stage。最后由 TaskScheduler 借助 ActorSystem 将任务提交给集群 管理器(ClusterManager)。

  3. 集群管理器(ClusterManager)给任务分配资源,即将具体任务分配到 Worker 上,Worker 创建 Executor 来处理任务的运行。Standalone、YARN、Mesos、kubernetes、EC2 等都可以作 为 Spark 的集群管理器。

计算模型:

SparkCore核心机制详解_Spark_06

RDD 可以看做是对各种数据计算模型的统一抽象,Spark 的计算过程主要是 RDD 的迭代计算 过程,如上图。RDD 的迭代计算过程非常类似于管道。分区数量取决于 partition 数量的设 定,每个分区的数据只会在一个 Task 中计算。所有分区可以在多个机器节点的 Executor 上 并行执行。

6.RDD

6.1RDD 概述

6.1.1什么是 RDD

RDD(Resilient Distributed Dataset)叫做分布式数据集,是 Spark 中最基本的数据抽象,它 代表一个不可变、可分区、里面的元素可并行计算的集合。RDD 具有数据流模型的特点: 自动容错、位置感知性调度和可伸缩性。RDD 允许用户在执行多个查询时显式地将工作集缓 存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。

可以从三个方面来理解:

1、数据集 DataSet:故名思议,RDD 是数据集合的抽象,是复杂物理介质上存在数据的一 种逻辑视图。从外部来看,RDD 的确可以被看待成经过封装,带扩展特性(如容错性)的数 据集合。

2、分布式 Distributed:RDD 的数据可能在物理上存储在多个节点的磁盘或内存中,也就是 所谓的多级存储。

3、弹性 Resilient:虽然 RDD 内部存储的数据是只读的,但是,我们可以去修改(例如通 过 repartition 转换操作)并行计算计算单元的划分结构,也就是分区的数量。

你将 RDD 理解为一个大的集合,将所有数据都加载到内存中,方便进行多次重用。第一, 它是分布式的,可以分布在多台机器上,进行计算。第二,它是弹性的,我认为它的弹性体 现在每个 RDD 都可以保存内存中,如果某个阶段的 RDD 丢失,不需要从头计算,只需要提 取上一个 RDD,再做相应的计算就可以了

6.1.2RDD 的属性

SparkCore核心机制详解_spark_07

  1. A list of partitions:一组分片(Partition),即数据集的基本组成单位

    • 一个分区通常与一个计算任务关联,分区的个数决定了并行的粒度;
    • 分区的个数可以在创建 RDD 的时候进行设置。如果没有设置,默认情况下由节点的 cores 个数决定;
    • 每个 Partition 最终会被逻辑映射为 BlockManager 中的一个 Block,而这个 Block 会被下一 个 Task(ShuffleMapTask/ResultTask)使用进行计算
  2. A function for computing each split:一个计算每个分区的函数,也就是算子 分区处理函数-compute

    • 每个 RDD 都会实现 compute,用于对分区进行计算
    • compute 函数会对迭代器进行复合,不需要保存每次计算结果;
    • 该方法负责接收 parent RDDs 或者 data block 流入的 records 并进行计算,然后输出加工 后的 records。
  3. A list of dependencies on other RDDs:RDD 之间的依赖关系:宽依赖和窄依赖 RDD 的每次转换都会生成一个新的 RDD,所以 RDD 之间就会形成类似于流水线一样的前后 依赖关系。在部分分区数据丢失时,Spark 可以通过这个依赖关系重新计算丢失的分区数据, 而不是对 RDD 的所有分区进行重新计算。

    RDDx 依赖的 parent RDD 的个数由不同的转换操作决定,例如二元转换操作 x = a.join(b),RDD x 就会同时依赖于 RDD a 和 RDD b。而具体的依赖关系可以细分为完全依赖和部分依赖,详 细说明如下:

    1. 完全依赖:一个子 RDD 中的分区可以依赖于父 RDD 分区中一个或多个完整分区。 例如,map 操作产生的子 RDD 分区与父 RDD 分区之间是一对一的关系;对于 cartesian 操作产生的子 RDD 分区与父 RDD 分区之间是多对多的关系。
    2. 部分依赖:父 RDD 的一个 partition 中的部分数据与 RDD x 的一个 partition 相关,而另一 部分数据与 RDD x 中的另一个 partition 有关。 例如,groupByKey 操作产生的 ShuffledRDD 中的每个分区依赖于父 RDD 的所有分区中的部分 元素。

SparkCore核心机制详解_Spark_08

在 Spark 中,完全依赖是 NarrowDependency(黑色箭头),部分依赖是 ShuffleDependency ( 红 色 箭 头 ), 而 NarrowDependency 又 可 以 细 分 为 [1:1]OneToOneDependency 、 [N:1]NarrowDependency 和[N:N]NarrowDependency,还有特殊的 RangeDependency (只在 UnionRDD 中使用)。

需要注意的是,对于[N:N]NarrowDependency很少见,最后生成的依赖图和ShuffleDependency 没什么两样。只是对于父 RDD 来说,有一部分是完全依赖,有一部分是部分依赖。所以也 只有[1:1]OneToOneDependency 和[N:1]NarrowDependency 两种情况。

  1. Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned):

    一个 Partitioner,即 RDD 的分片函数。

    当前 Spark 中实现了两种类型的分片函数,一个是基于哈希的 HashPartitioner,另外一个是 基于范围的 RangePartitioner。只有对于于 key-value 的 RDD,才会有 Partitioner,非 key-value的 RDD 的 Parititioner 的值是 None。Partitioner 函数不但决定了 RDD 本身的分片数量,也决 定了 parent RDD Shuffle 输出时的分片数量。

    • 只有键值对 RDD,才会有 Partitioner。其他非键值对的 RDD 的 Partitioner 为 None;
    • 它定义了键值对 RDD 中的元素如何被键分区,能够将每个键映射到对应的分区 ID,从 0 到”numPartitions- 1”;
    • Partitioner 不但决定了 RDD 本身的分区个数,也决定了 parent RDD shuffle 输出的分区个 数。
    • 在分区器的选择上,默认情况下,如果有一组 RDDs(父 RDD)已经有了 Partitioner,则 从中选择一个分区数较大的 Partitioner;否则,使用默认的 HashPartitioner。
    • 对于 HashPartitioner 分区数的设置,如果配置了 spark.default.parallelism 属性,则将分区 数设置为此值,否则,将分区数设置为上游 RDDs 中最大分区数。
  2. Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file):一个列表,存储存取每个 Partition 的优先位置(preferred location)。

    1. 对于一个 HDFS 文件来说,这个列表保存的就是每个 Partition 所在的块的位置。

    2. 按照”移动数据不如移动计算”的理念,Spark 在进行任务调度的时候,会尽可能地将计算 任务分配到其所要处理数据块的存储位置。

    3. 每个子 RDDgetPreferredLocations 的实现中,都会优先选择父 RDD 中对应分区的 preferedLocation,其次才选择自己设置的优先位置。

6.2创建 RDD

创建 RDD 主要有两种方式:官网解释

There are two ways to create RDDs: parallelizing an existing collection in your driver program, or referencing a dataset in an external storage system, such as a shared filesystem, HDFS, HBase, or any data source offering a Hadoop InputFormat.

  1. 由一个已经存在的 Scala 数据集合创建

    val rdd = sc.parallelize(Array(1,2,3,4,5,6,7,8)) val rdd = sc.makeRDD(Array(1,2,3,4,5,6,7,8))

  2. 由外部存储系统的数据集创建,包括本地的文件系统,还有所有 Hadoop 支持的数据集, 比如 HDFS、Cassandra、HBase 等 val rdd = sc.textFile(“hdfs://bdedev/spark/wc/input/words.txt”)

  3. 扩展

    从 HBase 当中读取 从 ElasticSearch 中读取

6.3RDD 的编程 API

官网:

http://spark.apache.org/docs/latest/rdd-programming-guide.html#resilient-distributed-datasets-rdds

 

6.4RDD 的依赖关系

RDD 和它依赖的父 RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和 宽依赖(wide dependency)

SparkCore核心机制详解_Spark_09

6.4.1窄依赖和宽依赖对比

窄依赖指的是每一个父 RDD 的 Partition 最多被子 RDD 的一个 Partition 使用

总结:窄依赖我们形象的比喻为独生子女,窄依赖的函数有:map, filter, union, join(父 RDD 是 hash-partitioned ), mapPartitions, mapValues

宽依赖指的是多个子 RDD 的 Partition 会依赖同一个父 RDD 的 Partition

总结:窄依赖我们形象的比喻为超生,宽依赖的函数有:groupByKey、partitionBy、reduceByKey、 sortByKey、join(父 RDD 不是 hash-partitioned )

6.4.2窄依赖和宽依赖总结

在这里我们是从父 RDD 的 partition 被使用的个数来定义窄依赖和宽依赖,因此可以用一句 话概括下:如果父 RDD 的一个 Partition 被子 RDD 的一个 Partition 所使用就是窄依赖,否则 的话就是宽依赖。因为是确定的 partition 数量的依赖关系,所以 RDD 之间的依赖关系就是 窄依赖;由此我们可以得出一个推论:即窄依赖不仅包含一对一的窄依赖,还包含一对固定 个数的窄依赖。

一对固定个数的窄依赖的理解:即子 RDD 的 partition 对父 RDD 依赖的 Partition 的数量不会 随着 RDD 数据规模的改变而改变;换句话说,无论是有 100T 的数据量还是 1P 的数据量, 在窄依赖中,子 RDD 所依赖的父 RDD 的 partition 的个数是确定的,而宽依赖是 shuffle 级别 的,数据量越大,那么子 RDD 所依赖的父 RDD 的个数就越多,从而子 RDD 所依赖的父 RDD 的 partition 的个数也会变得越来越多。

6.4.3Lineage

RDD 只支持粗粒度转换,即在大量记录上执行的单个操作。将创建 RDD 的一系列 Lineage(即 血统)记录下来,以便恢复丢失的分区。RDD 的 Lineage 会记录 RDD 的元数据信息和转换行 为,当该 RDD 的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据 分区。

6.5DAG 生成

DAG(Directed Acyclic Graph)叫做有向无环图,原始的RDD通过一系列的转换就就形成了DAG, 根据 RDD 之间的依赖关系的不同将 DAG 划分成不同的 Stage,对于窄依赖,partition 的转换 处理在 Stage 中完成计算。对于宽依赖,由于有 Shuffle 的存在,只能在 parent RDD 处理完 成后,才能开始接下来的计算,因此宽依赖是划分 Stage 的依据。

SparkCore核心机制详解_Spark_10

在 spark 中,会根据 RDD 之间的依赖关系将 DAG 图(有向无环图)划分为不同的阶段,对 于窄依赖,由于 partition 依赖关系的确定性,partition 的转换处理就可以在同一个线程里完 成,窄依赖就被 spark 划分到同一个 stage 中,而对于宽依赖,只能等父 RDD shuffle 处理完 成后,下一个 stage 才能开始接下来的计算。

因此 spark 划分 stage 的整体思路是:从后往前推,遇到宽依赖就断开,划分为一个 stage; 遇到窄依赖就将这个 RDD 加入该 stage 中。因此在上图中 RDD C,RDD D,RDD E,RDD F 被 构建在一个 stage 中,RDD A 被构建在一个单独的 Stage 中,而 RDD B 和 RDD G 又被构建在 同一个 stage 中。

在 spark 中,Task 的类型分为 2 种:ShuffleMapTask 和 ResultTask

简单来说,DAG 的最后一个阶段会为每个结果的 partition 生成一个 ResultTask,即每个 Stage 里面的 Task 的数量是由该 Stage 中最后一个 RDD 的 Partition 的数量所决定的!而其余所有 阶段都会生成 ShuffleMapTask;之所以称之为 ShuffleMapTask 是因为它需要将自己的计算结 果通过 shuffle 到下一个 stage 中;也就是说上图中的 stage1 和 stage2 相当于 MapReduce 中 的 Mapper,而 ResultTask 所代表的 stage3 就相当于 MapReduce 中的 reducer。

在之前动手操作了一个 WordCount 程序,因此可知,Hadoop 中 MapReduce 操作中的 Mapper 和 Reducer 在 spark 中的基本等量算子是 map 和 reduceByKey;不过区别在于:Hadoop 中的 MapReduce 天生就是排序的;而 reduceByKey 只是根据 Key 进行 reduce,但 spark 除了这两 个算子还有其他的算子;因此从这个意义上来说,Spark 比 Hadoop 的计算算子更为丰富。

6.6RDD 缓存

Spark 速度非常快的原因之一,就是在不同操作中可以在内存中持久化或缓存个数据集。当 持久化某个 RDD 后,每一个节点都将把计算的分片结果保存在内存中,并在对此 RDD 或衍 生出的 RDD 进行的其他动作中重用。这使得后续的动作变得更加迅速。RDD 相关的持久化 和缓存,是 Spark 最重要的特征之一。可以说,缓存是 Spark 构建迭代式算法和快速交互式 查询的关键。

6.6.1RDD 的缓存方式

RDD 通过 persist 方法或 cache 方法可以将前面的计算结果缓存,但是并不是这两个方法被 调用时立即缓存,而是触发后面的 action 时,该 RDD 将会被缓存在计算节点的内存中,并供后面重用。

SparkCore核心机制详解_Spark_11

通过查看源码发现 cache 最终也是调用了 persist 方法,默认的存储级别都是仅在内存存储一 份,Spark 的存储级别还有好多种,存储级别在 object StorageLevel 中定义的。

SparkCore核心机制详解_spark_12

缓存有可能丢失,或者存储存储于内存的数据由于内存不足而被删除,RDD 的缓存容错机制 保证了即使缓存丢失也能保证计算的正确执行。通过基于 RDD 的一系列转换,丢失的数据 会被重算,由于 RDD 的各个 Partition 是相对独立的,因此只需要计算丢失的部分即可,并 不需要重算全部 Partition。

7.Shared Variables(共享变量)

在 Spark 程序中,当一个传递给 Spark 操作(例如 map 和 reduce)的函数在远程节点上面运行 时,Spark 操作实际上操作的是这个函数所用变量的一个独立副本。这些变量会被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。通常跨任务的读 写变量是低效的,但是,Spark 还是为两种常见的使用模式提供了两种有限的共享变量: 广播变量(Broadcast Variable)和累加器(Accumulator)

官网:http://spark.apache.org/docs/latest/rdd-programming-guide.html#shared-variables

7.1Broadcast Variables(广播变量)

7.1.1为什么要定义广播变量

如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由 Driver 端进行分发,一般来讲,如果这个变量不是广播变量,那么每个 task 就会分发一份, 这在 task 数目十分多的情况下 Driver 的带宽会成为系统的瓶颈,而且会大量消耗 task 服务 器上的资源,如果将这个变量声明为广播变量,那么知识每个 executor 拥有一份,这个 executor 启动的 task 会共享这个变量,节省了通信的成本和服务器的资源。

没有使用广播变量:

SparkCore核心机制详解_Spark_13

使用了广播变量之后:

SparkCore核心机制详解_Spark_14

7.1.2如何定义和还原一个广播变量

定义:

val a = 3 
val broadcast = sc.broadcast(a) 

还原:

val c = broadcast.value 

注意:变量一旦被定义为一个广播变量,那么这个变量只能读,不能修改

7.1.3注意事项
  1. 能不能将一个 RDD 使用广播变量广播出去?

不能,因为 RDD 是不存储数据的。可以将 RDD 的结果广播出去。

  1. 广播变量只能在 Driver 端定义,不能在 Executor 端定义。

  2. 在 Driver 端可以修改广播变量的值,在 Executor 端无法修改广播变量的值。

  3. 如果 executor 端用到了 Driver 的变量,如果不使用广播变量在 Executor 有多少 task 就有 多少 Driver 端的变量副本。

  4. 如果 Executor 端用到了 Driver 的变量,如果使用广播变量在每个 Executor 中都只有一份 Driver 端的变量副本。

7.2Accumulators(累加器)

7.2.1为什么要定义累加器

在 Spark 应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数 据的数目,这种需求都需要用到计数器,如果一个变量不被声明为一个累加器,那么它将在 被改变时不会在 driver 端进行全局汇总,即在分布式运行时每个 task 运行的只是原始变量的 一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分 布式计数的功能。

7.2.2图解累加器

错误的图解:

SparkCore核心机制详解_spark_15

正确的图解:

SparkCore核心机制详解_spark_16

7.2.3如何定义和还原一个累加器

定义累加器:

val a = sc.longAccumulator(0)

还原累加器:

val b = a.value
7.2.4注意事项
  1. 累加器在 Driver 端定义赋初始值,累加器只能在 Driver 端读取最后的值,在 Excutor 端更 新。

  2. 累加器不是一个调优的操作,因为如果不这样做,结果是错的

  3. Spark 的运行流程

8.Spark 的基本运行流程

SparkCore核心机制详解_Spark_17

SparkCore核心机制详解_Spark_18

  1. 构建 DAG

使用算子操作 RDD 进行各种 transformation 操作,最后通过 action 操作触发 Spark 作业运行。 提交之后 Spark 会根据转换过程所产生的 RDD 之间的依赖关系构建有向无环图。

  1. DAG 切割

DAG 切割主要根据 RDD 的依赖是否为宽依赖来决定切割节点,当遇到宽依赖就将任务划分 为一个新的调度阶段(Stage)。每个 Stage 中包含一个或多个 Task。这些 Task 将形成任务集 (TaskSet),提交给底层调度器进行调度运行。

  1. 任务调度

每一个 Spark 任务调度器只为一个 SparkContext 实例服务。当任务调度器收到任务集后负责 把任务集以 Task 任务的形式分发至 Worker 节点的 Executor 进程中执行,如果某个任务失败, 任务调度器负责重新分配该任务的计算。

  1. 执行任务

当 Executor 收到发送过来的任务后,将以多线程(会在启动 executor 的时候就初始化好了 一个线程池)的方式执行任务的计算,每个线程负责一个任务,任务结束后会根据任务的类 型选择相应的返回方式将结果返回给任务调度器。

8.2运行流程图解

  1. 构建 Spark Application 的运行环境(初始化 SparkContext),SparkContext 向资源管理器(可 以是 Standalone、Mesos 或 YARN)注册并申请运行 Executor 资源

  2. 资源管理器分配 Executor 资源并启动 StandaloneExecutorBackend,Executor 运行情况将 随着心跳发送到资源管理器上

  3. SparkContext 构建成 DAG 图,将 DAG 图分解成 Stage,并把 Taskset 发送给 TaskScheduler。 Executor 向 SparkContext 申请 Task,TaskScheduler 将 Task 发放给 Executor 运行同时 SparkContext 将应用程序代码发放给 Executor

  4. Task 在 Executor 上运行,运行完毕释放所有资源。

SparkCore核心机制详解_spark_19

SparkCore核心机制详解_spark_20

SparkCore核心机制详解_Spark_21

SparkCore核心机制详解_Spark_22

8.3SparkContext 初始化

关于 SparkContext:

  1. SparkContext 是用户通往 Spark 集群的唯一入口,可以用来在 Spark 集群中创建 RDD、累 加器 Accumulator 和广播变量 Braodcast Variable

  2. SparkContext 也是整个 Spark 应用程序中至关重要的一个对象,可以说是整个应用程序 运行调度的核心(不是指资源调度)

  3. SparkContext在实例化的过程中会初始化DAGScheduler、TaskScheduler和SchedulerBackend

  4. SparkContext 会调用 DAGScheduler 将整个 Job 划分成几个小的阶段(Stage),TaskScheduler 会调度每个 Stage 的任务(Task)应该如何处理。另外,SchedulerBackend 管理整个集群中为这 个当前的应用分配的计算资源(Executor)

初始化流程:

  1. 处理用户的 jar 或者资源文件,和日志处理相关

SparkCore核心机制详解_Spark_23

  1. 初始化异步监听 bus:监听 spark 事件,用于 SparkUI 的跟踪管理

SparkCore核心机制详解_spark_24

  1. 初始化 Spark 运行环境相关变量

SparkCore核心机制详解_Spark_25

  1. 启动心跳接收器:在创建 taskScheduler 之间需要先注册 HeartbeatReceiver,因为 Executor 在创建时回去检索 HeartbeatReceiver

SparkCore核心机制详解_Spark_26

  1. 创建 SchedulerBackend、TaskScheduler、DAGScheduler

SparkCore核心机制详解_spark_27

  1. 启动 TaskScheduler:在 TaskScheduler 被 DAGScheduler 引用后,就可以进行启动

SparkCore核心机制详解_spark_28

  1. post init 各种启动

SparkCore核心机制详解_Spark_29

8.4Spark 运行架构特点

  1. 每个 Application 获取专属的 executor 进程,该进程在 Application 期间一直驻留,并以 多线程方式运行 tasks。这种 Application 隔离机制有其优势的,无论是从调度角度看(每个 Driver 调度它自己的任务),还是从运行角度看(来自不同 Application 的 Task 运行在不同的 JVM 中)。当然,这也意味着 Spark Application 不能跨应用程序共享数据,除非将数据写入 到外部存储系统。

  2. Spark 与资源管理器无关,只要能够获取 executor 进程,并能保持相互通信就可以了。

  3. 提交 SparkContext 的 Client 应该靠近 Worker 节点(运行 Executor 的节点),最好是在同 一个 Rack 里,因为 Spark Application 运行过程中 SparkContext 和 Executor 之间有大量的信息 交换;如果想在远程集群中运行,最好使用 RPC 将 SparkContext 提交给集群,不要远离 Worker 运行 SparkContext。

  4. Task 采用了数据本地性和推测执行的优化机制

8.5DAScheduler

一个 Application=多个 stage

Stage=多个同种 task

Task 分为 ShuffleMapTask 和 ResultTask

Dependency 分为 ShuffleDependency 宽依赖和 NarrowDependency 窄依赖

面向 stage 的切分,切分依据为宽依赖

维护 waiting jobs 和 active jobs,维护 waiting stages、active stages 和 failed stages,以及与 jobs 的映射关系

主要职能:

  1. 接收提交 Job 的主入口,submitJob(rdd, …)或 runJob(rdd, …)。在 SparkContext 里会调用这 两个方法。

    生成一个 Stage 并提交,接着判断 Stage 是否有父 Stage 未完成,若有,提交并等待父 Stage, 以此类推。结果是:DAGScheduler 里增加了一些 waiting stage 和一个 running stage。 running stage 提交后,分析 stage 里 Task 的类型,生成一个 Task 描述,即 TaskSet。 调 用 TaskScheduler.submitTask(taskSet, …) 方法,把 Task 描述 提交给 TaskScheduler 。 TaskScheduler 依据资源量和触发分配条件,会为这个 TaskSet 分配资源并触发执行。 DAGScheduler 提交 job 后,异步返回 JobWaiter 对象,能够返回 job 运行状态,能够 cancel job, 执行成功后会处理并返回结果

  2. 处理 TaskCompletionEvent

    如果 task 执行成功,对应的 stage 里减去这个 task,做一些计数工作:

    A:如果 task 是 ResultTask,计数器 Accumulator 加一,在 job 里为该 task 置为 true,job finish 总数加一。加完后如果 finish 数目与 partition 数目相等,说明这个 stage 完成了,标记 stage 完成,从 running stages 里减去这个 stage,做一些 stage 移除的清理工作

    B:如果 task 是 ShuffleMapTask,计数器 Accumulator 加一,在 stage 里加上一个 output location,里面是一个 MapStatus 类。MapStatus 是 ShuffleMapTask 执行完成的返回,包含 location 信息和 block size( 可以选择压缩或未压缩 )。同时检查该 stage 完成,向 MapOutputTracker 注册本 stage 里的 shuffleId 和 location 信息。然后检查 stage 的 output location 里是否存在空,若存在空,说明一些 task 失败了,整个 stage 重新提交;否则,继 续从 waiting stages 里提交下一个需要做的 stage

    C:如果 task 是重提交,对应的 stage 里增加这个 task: 如果 task 是 fetch 失败,马上标记对应的 stage 完成,从 running stages 里减去。如果不允许 retry,abort 整个 stage;否则,重新提交整个 stage。另外,把这个 fetch 相关的 location 和 map 任务信息,从 stage 里剔除,从 MapOutputTracker 注销掉。最后,如果这次 fetch 的 blockManagerId 对象不为空,做一次 ExecutorLost 处理,下次 shuffle 会换在另一个 executor 上去执行。

    D:其他 task 状态会由 TaskScheduler 处理,如 Exception, TaskResultLost, commitDenied 等。

  3. 其他与 job 相关的操作还包括:cancel job,cancel stage, resubmit failed stage 等

  4. 其他职能:cacheLocations 和 preferLocation

8.6TaskScheduler

维护 task 和 executor 对应关系,executor 和物理资源对应关系,在排队的 task 和正在跑的 task。维护内部一个任务队列,根据 FIFO 或 Fair 策略,调度任务。

TaskScheduler 本身是个接口,spark 里只实现了一个 TaskSchedulerImpl,理论上任务调度可 以定制。

主要职能:

  1. submitTasks(taskSet),接收 DAGScheduler 提交来的 tasks 为 tasks 创建一个 TaskSetManager,添加到任务队列里。TaskSetManager 跟踪每个 task 的执 行状况,维护了 task 的许多具体信息。 触发一次资源的需要。

首先,TaskScheduler 对照手头的可用资源和 Task 队列,进行 executor 分配(考虑优先级、 本地化等策略),符合条件的 executor 会被分配给 TaskSetManager。

然后,得到的 Task 描述交给 SchedulerBackend,调用 launchTask(tasks),触发 executor 上 task 的执行。task 描述被序列化后发给 executor,executor 提取 task 信息,调用 task 的 run() 方法执行计算。

  1. cancelTasks(stageId),取消一个 stage 的 tasks 调用 SchedulerBackend 的 killTask(taskId, executorId, …)方法。taskId 和 executorId 在 TaskScheduler 里一直维护着。

  2. resourceOffer(offers: Seq[Workers]) , 这 是 非 常 重 要 的 一 个 方 法 , 调 用 者 是 SchedulerBacnend , 用 途 是 底 层 资 源 SchedulerBackend 把 空 余 的 workers 资源交给 TaskScheduler,让其根据调度策略为排队的任务分配合理的 cpu 和内存资源,然后把任务描 述列表传回给 SchedulerBackend

从 worker offers 里,搜集 executor 和 host 的对应关系、active executors、机架信息等等。worker offers 资源列表进行随机洗牌,任务队列里的任务列表依据调度策略进行一次排序

遍历每个 taskSet,按照进程本地化、worker 本地化、机器本地化、机架本地化的优先级顺 序,为每个 taskSet 提供可用的 cpu 核数,看是否满足 默认一个 task 需要一个 cpu,设置参数为"spark.task.cpus=1"

为 taskSet 分配资源,校验是否满足的逻辑,最终在 TaskSetManager 的 resourceOffer(execId, host, maxLocality)方法里。满足的话,会生成最终的任务描述,并且调用 DAGScheduler 的 taskStarted(task, info)方法,通知 DAGScheduler,这时候每次会触发 DAGScheduler 做一次submitMissingStage 的尝试,即 stage 的 tasks 都分配到了资源的话,马上会被提交执行

  1. statusUpdate(taskId, taskState, data),另一个非常重要的方法,调用者是SchedulerBacnend, 用途是 SchedulerBacnend 会将 task 执行的状态汇报给 TaskScheduler 做一些决定

    若 TaskLost,找到该 task 对应的 executor,从 active executor 里移除,避免这个 executor 被分配到其他 task 继续失败下去。

    task finish 包括四种状态:finished, killed, failed, lost。只有 finished 是成功执行完成了。 其他三种是失败。

    task 成功执行完,调用 TaskResultGetter.enqueueSuccessfulTask(taskSet, tid, data),否则调 用 TaskResultGetter.enqueueFailedTask(taskSet, tid, state, data)。TaskResultGetter 内部维护了一 个线程池,负责异步 fetch task 执行结果并反序列化。默认开四个线程做这件事,可配参数 “spark.resultGetter.threads”=4。

补充:TaskResultGetter 取 task result 的逻辑

  1. 对于 success task,如果 taskResult 里的数据是直接结果数据,直接把 data 反序列出来得 到结果;如果不是,会调用 blockManager.getRemoteBytes(blockId)从远程获取。如果远程取 回的数据是空的,那么会调用 TaskScheduler.handleFailedTask,告诉它这个任务是完成了的 但是数据是丢失的。否则,取到数据之后会通知 BlockManagerMaster 移除这个 block 信息, 调用 TaskScheduler.handleSuccessfulTask,告诉它这个任务是执行成功的,并且把 result data 传回去。

  2. 对于 failed task,从 data 里解析出 fail 的理由,调用 TaskScheduler.handleFailedTask,告诉 它这个任务失败了,理由是什么。

8.7SchedulerBackend 在 TaskScheduler 下层,用于对接不同的资源管理系统,SchedulerBackend 是个接口,需要实 现的主要方法如下:

def start():Unit 
def stop():Unit 
def reviveOffers():Unit // 重要方法:SchedulerBackend 把自己手头上的可用资源交给 TaskScheduler,TaskScheduler 根据调度策略分配给排队的任务吗,返回一批可执行的任务描 述,SchedulerBackend 负责 launchTask,即最终把 task 塞到了 executor 模型上,executor 里 的线程池会执行 task 的 run() 
def killTask(taskId: Long, executorId: String, interruptThread: Boolean): Unit = throw new UnsupportedOperationException 

粗粒度:进程常驻的模式,典型代表是 Standalone 模式,Mesos 粗粒度模式,YARN 细粒度:Mesos 细粒度模式

这里讨论粗粒度模式,更好理解:CoarseGrainedSchedulerBackend。维护 executor 相关信息(包 括 executor 的地址、通信端口、host、总核数,剩余核数),手头上 executor 有多少被注册 使用了,有多少剩余,总共还有多少核是空的等等。

主要职能:

  1. Driver 端主要通过 actor 监听和处理下面这些事件:

    RegisterExecutor(executorId, hostPort, cores, logUrls)

    这 是 executor 添 加 的 来 源 , 通 常 worker 拉 起 、 重 启 会 触 发 executor 的 注 册 。 CoarseGrainedSchedulerBackend 把这些 executor 维护起来,更新内部的资源信息,比如总核 数增加。最后调用一次 makeOffer(),即把手头资源丢给 TaskScheduler 去分配一次,返回任 务描述回来,把任务 launch 起来。这个 makeOffer()的调用会出现在任何与资源变化相关的 事件中,下面会看到。

    StatusUpdate(executorId, taskId, state, data)

    task 的状态回调。首先,调用 TaskScheduler.statusUpdate 上报上去。然后,判断这个 task 是 否执行结束了,结束了的话把 executor 上的 freeCore 加回去,调用一次 makeOffer()。

    ReviveOffers

    这个事件就是别人直接向 SchedulerBackend 请求资源,直接调用 makeOffer()。

    KillTask(taskId, executorId, interruptThread)

    这个 killTask 的事件,会被发送给 executor 的 actor,executor 会处理 KillTask 这个事件。

    StopExecutors

    通知每一个 executor,处理 StopExecutor 事件。

    RemoveExecutor(executorId, reason)

    从维护信息中,那这堆 executor 涉及的资源数减掉,然后调用 TaskScheduler.executorLost() 方法,通知上层我这边有一批资源不能用了,你处理下吧。TaskScheduler 会继续把 executorLost 的事件上报给 DAGScheduler,原因是 DAGScheduler 关心 shuffle 任务的 output location。DAGScheduler 会告诉 BlockManager 这个 executor 不可用了,移走它,然后把所有 的 stage 的 shuffleOutput 信息都遍历一遍,移走这个 executor,并且把更新后的 shuffleOutput 信息注册到 MapOutputTracker 上,最后清理下本地的 CachedLocationsMap。

  2. reviveOffers()方法的实现

    直接调用了 makeOffers()方法,得到一批可执行的任务描述, 调用 launchTasks。

  3. launchTasks(tasks: Seq[Seq[TaskDescription]])方法

    遍历每个 task 描述,序列化成二进制,然后发送给每个对应的 executor 这个任务信息 如果 这个二进制信息太大,超过了9.2M(默认的akkaFrameSize 10M减去默认为akka留空的200K), 会出错,abort 整个 taskSet,并打印提醒增大 akka frame size。如果二进制数据大小可接受, 发送给 executor 的 actor,处理 LaunchTask(serializedTask)事件。

8.8Executor

Executor 是 Spark 里的进程模型,可以套用到不同的资源管理系统上,与 SchedulerBackend配合使用。

内部有个线程池,有个 running tasks map,有个 actor,接收上面提到的由 SchedulerBackend 发来的事件。

SparkCore核心机制详解_spark_30

事件处理

launchTask。根据 task 描述,生成一个 TaskRunner 线程,丢尽 running tasks map 里,用线程 池执行这个 TaskRunner killTask。从 running tasks map 里拿出线程对象,调它的 kill 方法。

9.Spark程序执行流程

SparkCore核心机制详解_Spark_31

Spark 任务的任务执行流程文字详细描述

  1. 将我们编写的程序打成 jar 包

  2. 调用 spark-submit 脚本提交任务到集群上运行

  3. 运行 sparkSubmit 的 main 方法,在这个方法中通过反射的方式创建我们编写的主类的 实例对象,然后调用 main 方法,开始执行我们的代码(注意,我们的 spark 程序中的 driver 就运行在 sparkSubmit 进程中)

  4. 当代码运行到创建 SparkContext 对象时,那就开始初始化 SparkContext 对象了

  5. 在初始化 SparkContext 对象的时候,会创建两个特别重要的对象,分别是:DAGScheduler 和 TaskScheduler 【DAGScheduler 的作用】将 RDD 的依赖切分成一个一个的 stage,然后将 stage 作为 taskSet 提交给 DriverActor

  6. 在构建 TaskScheduler 的同时,会创建两个非常重要的对象,分别是 DriverActor 和 ClientActor 【clientActor 的作用】向 master 注册用户提交的任务 【DriverActor 的作用】接受 executor 的反向注册,将任务提交给 executor

  7. 当 ClientActor 启动后,会将用户提交的任务和相关的参数封装到 ApplicationDescription 对象中,然后提交给 master 进行任务的注册

  8. 当 master 接受到 clientActor 提交的任务请求时,会将请求参数进行解析,并封装成Application,然后将其持久化,然后将其加入到任务队列 waitingApps 中

  9. 当轮到我们提交的任务运行时,就开始调用 schedule(),进行任务资源的调度

  10. master 将调度好的资源封装到 launchExecutor 中发送给指定的 worker

  11. worker接受到Maseter发送来的launchExecutor时,会将其解压并封装到ExecutorRunner 中,然后调用这个对象的 start(), 启动 Executor

  12. Executor 启动后会向 DriverActor 进行反向注册

  13. driverActor 会发送注册成功的消息给 Executor

  14. Executor 接受到 DriverActor 注册成功的消息后会创建一个线程池,用于执行 DriverActor 发送过来的 task 任务

  15. 当属于这个任务的所有的 Executor 启动并反向注册成功后,就意味着运行这个任务的 环境已经准备好了,driver 会结束 SparkContext 对象的初始化,也就意味着 new SparkContext 这句代码运行完成

  16. 当初始化 sc 成功后,driver 端就会继续运行我们编写的代码,然后开始创建初始的 RDD, 然后进行一系列转换操作,当遇到一个 action 算子时,也就意味着触发了一个 job

  17. driver 会将这个 job 提交给 DAGScheduler

  18. DAGScheduler 将接受到的 job,从最后一个算子向前推导,将 DAG 依据宽依赖划分成 一个一个的 stage,然后将 stage 封装成 taskSet,并将 taskSet 中的 task 提交给 DriverActor

  19. DriverActor 接受到 DAGScheduler 发送过来的 task,会拿到一个序列化器,对 task 进行 序列化,然后将序列化好的 task 封装到 launchTask 中,然后将 launchTask 发送给指定的 Executor

  20. Executor 接受到了 DriverActor 发送过来的 launchTask 时,会拿到一个反序列化器,对 launchTask 进行反序列化,封装到 TaskRunner 中,然后从 Executor 这个线程池中获取一个线 程,将反序列化好的任务中的算子作用在 RDD 对应的分区上

10.Spark 在不同集群中的架构

Spark 注重建立良好的生态系统,它不仅支持多种外部文件存储系统,提供了多种多样的集 群运行模式。部署在单台机器上时,既可以用本地(Local)模式运行,也可以使用伪分布 式模式来运行;当以分布式集群部署的时候,可以根据自己集群的实际情况选择 Standalone 模式(Spark 自带的模式)、YARN-Client 模式或者 YARN-Cluster 模式。Spark 的各种运行模式虽然在启动方式、运行位置、调度策略上各有不同,但它们的目的基本都是一致的,就是在 合适的位置安全可靠的根据用户的配置和 Job 的需要运行和管理 Task。

10.1Spark On StandAlone 运行过程

Standalone 模式是 Spark 实现的资源调度框架,其主要的节点有 Client 节点、Master 节点和 Worker 节点。其中 Driver 既可以运行在 Master 节点上中,也可以运行在本地 Client 端。当 用 spark-shell 交互式工具提交 Spark 的 Job 时,Driver 在 Master 节点上运行;当使用 spark-submit.sh 工具提交 Application 或者在 Eclipes、IDEA 等开发平台上使用 new SparkConf().setMaster(“spark://master:7077”)方式运行 Spark 任务时,Driver 是运行在本地 Client 端上的。

运行过程文字说明

  1. 我们提交一个任务,任务就叫 Application

  2. 初始化程序的入口 SparkContext:

    • 初始化 DAG Scheduler

    • 初始化 Task Scheduler

  3. Task Scheduler 向 master 去进行注册并申请资源(CPU Core 和 Memory)

  4. Master 根据 SparkContext 的资源申请要求和 Worker 心跳周期内报告的信息决定在哪个 Worker 上分配资源,然后在该 Worker 上获取资源,然后启动 StandaloneExecutorBackend; 顺便初始化好了一个线程池

  5. StandaloneExecutorBackend 向 Driver(SparkContext)注册,这样 Driver 就知道哪些 Executor 为他进行服务了。到这个时候其实我们的初始化过程基本完成了,我们开始执行 transformation 的代码,但是代码并不会真正的运行,直到我们遇到一个 action 操作。生成 一个 job 任务,进行 stage 的划分

  6. SparkContext 将 Applicaiton 代码发送给 StandaloneExecutorBackend;并且 SparkContext 解析 Applicaiton 代码,构建 DAG 图,并提交给 DAG Scheduler 分解成 Stage(当碰到 Action 操作时,就会催生 Job;每个 Job 中含有 1 个或多个 Stage,Stage 一般在获取外部数据和 shuffle 之前产生)

  7. 将 Stage(或者称为 TaskSet)提交给 Task Scheduler。Task Scheduler 负责将 Task 分配到相 应的 Worker,最后提交给 StandaloneExecutorBackend 执行

  8. 对 task 进行序列化,并根据 task 的分配算法,分配 task

  9. 对接收过来的 task 进行反序列化,把 task 封装成一个线程

  10. 开始执行 Task,并向 SparkContext 报告,直至 Task 完成

  11. 资源注销

图解:

SparkCore核心机制详解_Spark_32

10.2Spark On YARN 运行过程

YARN 是一种统一资源管理机制,在其上面可以运行多套计算框架。目前的大数据技术世界, 大多数公司除了使用 Spark 来进行数据计算,由于历史原因或者单方面业务处理的性能考虑 而使用着其他的计算框架,比如 MapReduce、Storm 等计算框架。Spark 基于此种情况开发 了 Spark on YARN 的运行模式,由于借助了 YARN 良好的弹性资源管理机制,不仅部署 Application更加方便,而且用户在YARN集群中运行的服务和Application的资源也完全隔离, 更具实践应用价值的是 YARN 可以通过队列的方式,管理同时运行在集群中的多个服务。

Spark on YARN 模式根据 Driver 在集群中的位置分为两种模式: 一种是 YARN-Client 模式

SparkCore核心机制详解_spark_33

一种是 YARN-Cluster(或称为 YARN-Standalone 模式),不过不能启动到 spark-shell,只能在 spark-submit 提交任务的时候才能使用

SparkCore核心机制详解_Spark_34

10.2.1YARN-Client

Yarn-Client 模式中,Driver 在客户端本地运行,这种模式可以使得 Spark Application 和客户端 进行交互,因为 Driver 在客户端,所以可以通过 webUI 访问 Driver 的状态,默认是 http://hadoop1:4040 访问,而 YARN 通过 http:// hadoop1:8088 访问。

YARN-client 的工作流程分为以下几个步骤

文字说明:

  1. Spark Yarn Client 向 YARN 的 ResourceManager 申请启动 Application Master。同时在 SparkContent 初始化中将创建 DAGScheduler 和 TASKScheduler 等,由于我们选择的是 Yarn-Client 模式,程序会选择 YarnClientClusterScheduler 和 YarnClientSchedulerBackend;

  2. ResourceManager 收到请求后,在集群中选择一个 NodeManager,为该应用程序分配第 一个Container,要求它在这个Container中启动应用程序的ApplicationMaster,与YARN-Cluster 区别的是在该 ApplicationMaster 不运行 SparkContext,只与 SparkContext 进行联系进行资源 的分派;

  3. Client中的SparkContext初始化完毕后,与ApplicationMaster建立通讯,向ResourceManager 注册,根据任务信息向 ResourceManager 申请资源(Container);

  4. 一旦 ApplicationMaster 申请到资源(也就是 Container)后,便与对应的 NodeManager 通信,要求它在获得的 Container 中 启 动 启 动 CoarseGrainedExecutorBackend , CoarseGrainedExecutorBackend 启动后会向 Client 中的 SparkContext 注册并申请 Task;

  5. Client 中 的 SparkContext 分 配 Task 给 CoarseGrainedExecutorBackend 执 行 , CoarseGrainedExecutorBackend 运行 Task 并向 Driver 汇报运行的状态和进度,以让 Client 随 时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;

  6. 应用程序运行完成后,Client 的 SparkContext 向 ResourceManager 申请注销并关闭自己。

图解:

SparkCore核心机制详解_spark_35

10.2.3YARN-Cluster

在 YARN-Cluster 模式中,当用户向 YARN 中提交一个应用程序后,YARN 将分两个阶段运行该 应用程序:第一个阶段是把 Spark 的 Driver 作为一个 ApplicationMaster 在 YARN 集群中先启 动;第二个阶段是由 ApplicationMaster 创建应用程序,然后为它向 ResourceManager 申请资 源,并启动 Executor 来运行 Task,同时监控它的整个运行过程,直到运行完成。

YARN-cluster 的工作流程分为以下几个步骤:

文字说明

  1. Spark Yarn Client 向 YARN 中提交应用程序,包括 ApplicationMaster 程序、启动 ApplicationMaster 的命令、需要在 Executor 中运行的程序等;

  2. ResourceManager 收到请求后,在集群中选择一个 NodeManager,为该应用程序分配第 一个 Container,要求它在这个 Container 中启动应用程序的 ApplicationMaster,其中 ApplicationMaster 进行 SparkContext 等的初始化;

  3. ApplicationMaster 向 ResourceManager 注册,这样用户可以直接通过 ResourceManage 查 看应用程序的运行状态,然后它将采用轮询的方式通过 RPC 协议为各个任务申请资源,并 监控它们的运行状态直到运行结束;

  4. 一旦 ApplicationMaster 申请到资源(也就是 Container)后,便与对应的 NodeManager 通信,要求它在获得的 Container 中 启 动 启 动 CoarseGrainedExecutorBackend ,CoarseGrainedExecutorBackend 启动后会向 ApplicationMaster 中的 SparkContext 注册并申请 Task。这一点和 Standalone 模式一样,只不过 SparkContext 在 Spark Application 中初始化时, 使用 CoarseGrainedSchedulerBackend 配合 YarnClusterScheduler 进行任务的调度,其中 YarnClusterScheduler 只是对 TaskSchedulerImpl 的一个简单包装,增加了对 Executor 的等待逻 辑等;

  5. ApplicationMaster 中的 SparkContext 分配 Task 给 CoarseGrainedExecutorBackend 执行, CoarseGrainedExecutorBackend 运行 Task 并向 ApplicationMaster 汇报运行的状态和进度,以 让 ApplicationMaster 随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务;

  6. 应用程序运行完成后,ApplicationMaster 向 ResourceManager 申请注销并关闭自己。

图解:

SparkCore核心机制详解_Spark_36

10.2.4YARN-Client 和 YARN-Cluster 区别

理解 YARN-Client 和 YARN-Cluster 深层次的区别之前先清楚一个概念:ApplicationMaster。在 YARN 中,每个 Application 实例都有一个 ApplicationMaster 进程,它是 Application 启动的第 一个容器。它负责和 ResourceManager 打交道并请求资源,获取资源之后告诉 NodeManager 为其启动 Container。从深层次的含义讲 YARN-Cluster 和 YARN-Client 模式的区别其实就是 ApplicationMaster 进程的区别。

  1. YARN-Cluster 模式下,Driver 运行在 AM(Application Master)中,它负责向 YARN 申请资源, 并监督作业的运行状况。当用户提交了作业之后,就可以关掉 Client,作业会继续在 YARN 上运行,因而 YARN-Cluster 模式不适合运行交互类型的作业;

  2. YARN-Client 模式下,ApplicationMaster 仅仅向 YARN 请求 Executor,Client 会和请求的 Container 通信来调度他们工作,也就是说 Client 不能离开。

SparkCore核心机制详解_spark_37

SparkCore核心机制详解_spark_38

SparkCore核心机制详解_Spark_39

SparkCore核心机制详解_Spark_40