目录
- 1、Spark内核概述
- 1.1 Spark核心组件回顾
- 1.1.1 Driver
- 1.1.2 Executor
- 1.2 Spark通用运行流程概述
- 2、Spark通讯架构
- 2.1 Spark通信架构概述
- 2.2 Saprk通讯架构解析
- 2.3 Spark集群启动
- 3、Spark部署模式
- 3.1 Standalone模式运行机制
- 3.1.1 Standalone Client模式
- 3.1.2 Standalone Cluster模式
- 3.2 Yarn模式运行机制
- 3.2.1 Yarn Client模式
- 3.2.2 Yarn Cluster模式
- 4、Spark任务调度机制
- 4.1 Spark 任务提交流程
- 4.2 Spark 任务调度概述
- 4.3 Spark Stage 级调度
- 4.4 Spark Task 级调度
1、Spark内核概述
Spark内核泛指Spark的核心运行机制,包括Spark核心组件的运行机制、Spark任务调度机制、Spark内存管理机制、Spark核心功能的运行原理等,熟练掌握Spark内核原理,能够帮助我们更好的完成Spark代码设计,并能够帮助我们准确锁定项目运行过程中出现的问题的症结所在。
1.1 Spark核心组件回顾
1.1.1 Driver
Spark驱动器节点,用于执行Saprk任务中的main方法,负责实际代码的执行工作。
Drievr
在Spark作业执行时主要负责:
- 将用户程序转换成任务(job)
- 在Executor之间调度任务(task)
- 跟踪Executor的执行情况
- 通过UI展示查询运行情况
1.1.2 Executor
Executor
节点是一个JVM进程,负责在Spark作业中运行具体任务,任务彼此之间相互独立。Spark任务启动时,Executor节点被同时启动,并且始终伴随着整个Spark应用的生命周期而存在。如果有Executor
节点发生了故障或崩溃,Spark应用也可以继续执行,会将出错节点上的任务调度到其他Executor
节点上继续执行。
Executor有两个核心功能:
- 负责运行组成Spark应用的任务,并将结果返回给驱动器进程
- 它们通过自身的块管理器(Block Manager) 为用户程序中要求缓存的RDD提供内存式存储,RDD是直接缓存在
Executor
进程内的,因此任务可以在运行时充分利用缓存数据加速运算。
1.2 Spark通用运行流程概述
上图为Spark通用运行流程,不论Spark以何种模式进行部署,都是以如下核心步骤进行工作的:
- 任务提交后,都会先启动
Driver
程序 - 随后
Driver
向集群管理器注册应用程序 - 之后集群管理器根据此任务的配置文件分配
Executor
并启动 - 当
Driver
所需的资源全部满足后,Driver
开始执行main函数,Spark查询为懒执行,当执行到Action算子时开始反向推算,根据宽依赖进行Stage
的划分,随后每一个Stage
对应一个TaskSet
,TaskSet
中有多个Task - 根据本地化原则,Task会被分发到执行的
Executor
去执行,在任务执行的过程中,Executor
也会不断地与Drievr
进行通信,报告任务运行情况
2、Spark通讯架构
2.1 Spark通信架构概述
Spark2.x版本使用Netty通讯框架作为内部通讯组件。Spark基于Netty新的rpc框架借鉴了Akka中的设计,它是基于Actor模型,如下图所示:
Spark通讯框架中各个组件(Client/Master/Worker)可以认为是一个个独立的实体,各个实体之间通过消息来进行通信,具体各个组件之间的关系图如下:
Endpoint
(Client/Master/Worker)有一个InBox和N个OutBox (N>=1, N取决于当前Endpoint
与多少其他的Endpoint
进行通信,一个与其通讯的其他Endpoint对应一个OutBox), Endpoint
接收到的消息被写入InBox, 发送出去的消息写入OutBox 并被发送到其他 Endpoint
的 InBox 中。
2.2 Saprk通讯架构解析
Spark通讯架构如下图所示:
-
RpcEndpoint
: RPC端点,Spark针对每个节点 (Client/Master/Worker) 都称之为一个RPC端点,且都实现RpcEndpoint
接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(访问) 则调用Dispatcher
-
RpcEnv
: RPC上下文环境,每个RPC端点运行时依赖的上下文环境称之为RpcEnc -
Dispatcher
: 消息分发器,针对于RPC端点需要发送消息或者从远程RPC接收消息,分发至对应的指令收件箱/发件箱,如果指令接收方是自己则存入收件箱,如果指令接收方不是自己,则放入发件箱 -
InBox
: 指令消息收件箱,一个本地RpcEndpoint对应一个收件箱,Dispatcher在每次向InBox存入消息时,都将对应EndpointData加入内部ReceiverQueue中,另外Dispatcher创建时会启动一个单独线程进行轮询ReceiverQueue, 进行收件箱消息消费 -
RpcEndpointRef
: RpcEndpointRef是对远程RpcEndpoint的一个引用。当我们需要向一个具体的RpcEndpoint发送消息时,一般我们需要获取到该RpcEndpoint的引用,然后通过该引用发送消息。 -
OutBox
: 指令消息发件箱,对于当前RpcEndpoint来说,一个目标RpcEndpoint对应一个发件箱,如果向多个目标RpcEndpoint发送消息,则有多个OutBox。当消息放入OutBox后,紧接着通过TransportClient将消息发送出去,消息放入发件箱以及发送过程是在同一个线程中进行 -
RpcAddress
: 表示远程的RpcEndpointRef的地址, Host + Port -
TransportClient
: Netty通信客户端,一个OutBox对应一个TransportClient, TransportClient不断轮询OutBox, 根据OutBox消息的receiver信息,请求对应的远程TransportServer -
TransportServer
: Netty通信服务端,一个RpcEndpoint对应一个TransportServer, 接收远程消息后调用Dispatcher分发消息至对应收/发件箱
根据上面的分析,Spark通信架构的高层视图如下图所示:
2.3 Spark集群启动
Master & Worker启动流程分析
可以看到,Spark集群采用的消息模式进行通信,也就是EDA架构模式,借助于RPC层的优雅设计,任何两个Endpoint
进行通信,发送消息并携带数据即可。上图的流程描述如下所示:
- Master启动时首先创建一个RpcEnv对象,负责管理所有通讯逻辑
- Master通过RpcEnv对象创建一个Endpoint, Master就是一个Endpoint, Worker可以与其进行通讯
- Worker启动时也是创建一个RpcEnv对象
- Worker通过RpcEnv对象创建一个Endpoint
- Worker通过RpcEnv对象,建立到Master的连接,获取到一个RpcEndpointRef对象,通过该对象可以与Master通信
- Worker向Master注册,注册内容包括主机名、端口、CPU Core数量、内存数量
- Master接收到Worker的注册,将注册信息维护在内存中的Table中,其中还包含了一个到Worker的RpcEndpointRef对象引用
- Master回复Worker已经接收到注册,告知Worker已经注册成功
- 此时如果有用户提交Spark程序,Master需要协调启动Driver;而Worker端收到成功注册响应后,开始周期性向Master发送心跳
3、Spark部署模式
Saprk支持三种集群管理器(Cluster manager), 分别为:
-
Standalone
: 独立模式,Spark原生的集群资源管理器,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统,使用Standalone可以很方便地搭建一个集群 -
Apache Mesos
: 一个强大的分布式资源管理框架,它允许多种不同的框架部署在其上,包括yarn -
Hadoop Yarn
: 统一的资源管理机制,在上面可以运行多套计算框架,如mapreduce, storm等,根据driver在集群中的位置不同,分为yarn-client
和yarn-cluster
Spark的运行模式取决于传递给SparkContext的master环境变量的值,个别模式还需要辅助的程序接口来配合使用,目前支持的master字符串以及URL包括:
Master URL | Meaning |
local | 在本地运行,只有一个工作进程,无并行计算能力 |
local[K] | 在本地运行,有K个工作进程,通常设置K为机器的CPU核心数量 |
spark://HOST:PORT | 以Standalone模式运行,这是Spark自身提供的集群运行模式,默认端口号7077 |
mesos://HOST:PORT | 在 Mesos集群上运行,Driver进程和Worker进程运行在Mesos集群上,部署模式必须使用固定值:–deploy-mode cluster |
yarn-client | 在Yarn集群上运行,Driever进程在本地,Worker进程在Yarn集群上, 部署模式必须使用固定值:–deploy-mode client。yarn集群地址必须在HADOOP_CONF_DIR or YARN_CONF_DIR变量里定义 |
yarn-cluster | 在Yarn集群上运行,Driever进程在Yanr集群上,Worker进程也在Yarn集群上,部署模式必须使用固定值:–deploy-mode cluster。yarn集群地址必须在HADOOP_CONF_DIR or YARN_CONF_DIR变量里定义 |
用户在提交任务给Saprk处理时,以下两个参数共同决定了Saprk的运行方式:
- –master MASTER_URL : 决定了Saprk任务提交给哪种集群处理
- –deploy-mode DEPLOY_MODE : 决定了Driver的运行方式,可选值为Client或者Cluster
3.1 Standalone模式运行机制
Standalone集群有4个重要组成部分,分别是:
-
Driver
: 是一个进程,我们编写的Spark应用程序就运行在Driver上,由Driver进程执行 -
Master
: 是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责 -
Worker
: 是一个进程,一个Worker运行在集群的一台服务器上,主要负责两个职责,一个是用自己的内存存储RDD的某个或某些partition; 另一个是启动其他进程和线程(Executor),对RDD上的partition进行并行的处理和计算 -
Executor
: 是一个进程,一个Worker上可以运行多个Executor, executor通过启动多个线程(task)来执行对RDD的partition进行并行计算,也就是执行我们对RDD定义的例如map、flatMap、reduce等算子操作
3.1.1 Standalone Client模式
在Standalone Client
模式下,Driver
在任务提交的本地机器上运行,Driver
启动后向Master
注册应用程序,Master
根据submit脚本的资源需求找到内部资源至少可以启动一个Executor
的所有Worker
, 然后在这些Worker
之间分配Executor
, Worker
上的Executor
启动后会向Driver
反向注册,所有的Executor注册完成之后,Driver
开始执行main函数,之后执行到Action算子时,开始划分Stage
, 每个Stage
生成对应的TaskSet
, 之后将task分发到各个Executor
上执行。
3.1.2 Standalone Cluster模式
在Standalone cluster
模式下,任务提交后,Master
会找到一个Worker
启动Driver
进程,Driver
启动后向Master
注册应用程序,Master
根据submit脚本的资源需求找到内部资源至少可以启动一个Executor
的所有Worker
,然后在这些Worker
之间分配Executor
, Worker
上的Executor
启动后会向Driver
反向注册,所有的Executor
注册完成后,Driver
开始执行main函数,之后执行到Action算子时,开始划分Stage
, 每个Stage
生成对应的TaskSet
, 之后将task分发到各个Executor
上执行。
注意: Standalone
的两种模式下( client/Cluster) , Master
在接到 Driver
注册 Spark 应用程序的请求后,会获取其所管理的剩余资源能够启动一个 Executor
的所有 Worker
, 然后在这些 Worker
之间分发 Executor
, 此时的分发只考虑 Worker
上的资源是否足够使用,直到当前应用程序所需的所有 Executor
都分配完毕, Executor
反向注册完毕后,Driver
开始执行 main 程序。
3.2 Yarn模式运行机制
3.2.1 Yarn Client模式
在 YARN Client
模式下,Driver
在任务提交的本地机器上运行,Driver
启动后会和 ResourceManager
通讯申请启动 ApplicationMaster
, 随后 ResourceManager
分配 container , 在 合 适 的 NodeManager
上启动 ApplicationMaster
,此时的 ApplicationMaster
的功能相当于一个 ExecutorLaucher
, 只负责向 ResourceManager
申请 Executor
内存。
ResourceManager
接到 ApplicationMaster
的资源申请后会分配 container,然后 ApplicationMaster
在资源分配指定的 NodeManager
上启动 Executor
进程,Executor
进程启动后会向 Driver
反向注册, Executor
全部注册完成后 Driver
开始执行 main 函数,之后执行到 Action 算子时,触发一个 job,并根据宽依赖开始划分 stage
,每个 stage
生成对应的 taskSet
,之后将 task 分发到各个 Executor
上执行。
3.2.2 Yarn Cluster模式
在 YARN Cluster
模式下, 任务提交后会和 ResourceManager
通讯申请启动 ApplicationMaster
, 随后 ResourceManager
分配 container,在合适的 NodeManager
上启动 ApplicationMaster
,此时的 ApplicationMaster
就是 Driver
。
Driver
启动后向 ResourceManager
申请 Executor
内存, ResourceManager
接到 ApplicationMaster
的资源申请后会分配 container,然后在合适的 NodeManager
上启动 Executor
进程,Executor
进程启动后会向 Driver
反向注册, Executor
全部注册完成后 Driver
开始执行 main 函数,之后执行到 Action 算子时,触发一个 job,并根据宽依赖开始划分 stage
,每个 stage
生成对应的 taskSet
,之后将 task 分发到各个 Executor
上执行。
4、Spark任务调度机制
在生产环境下, Spark 集群的部署方式一般为 YARN-Cluster
模式, 之后的内核分析内容中我们默认集群的部署方式为 YARN-Cluster
模式。
4.1 Spark 任务提交流程
提交一个 Spark 应用程序, 首先通过 Client 向 ResourceManager
请求启动一个Application,同时检查是否有足够的资源满足Application 的需求,如果资源条件满足,则准备 ApplicationMaster
的启动上下文,交给 ResourceManager
,并循环监控Application 状态。
当提交的资源队列中有资源时, ResourceManager
会在某个 NodeManager
上启动 ApplicationMaster
进程,ApplicationMaster
会单独启动 Driver
后台线程,当 Driver
启动后,ApplicationMaster
会通过本地的 RPC 连接 Driver
,并开始向 ResourceManager
申请 Container 资源运行 Executor
进程(一个 Executor 对应与一个 Container),当ResourceManager
返回 Container 资源,ApplicationMaster
则在对应的 Container 上启动 Executor
。
Driver
线程主要是初始化 SparkContext
对象,准备运行所需的上下文, 然后一方面保持与 ApplicationMaster
的 RPC 连接,通过 ApplicationMaster
申请资源,另一方面根据用户业务逻辑开始调度任务,将任务下发到已有的空闲 Executor
上。
当ResourceManager
向 ApplicationMaster
返 回 Container 资 源 时 ,ApplicationMaster
就尝试在对应的 Container 上启动 Executor
进程,Executor
进程起来后,会向 Driver
反向注册,注册成功后保持与 Driver
的心跳,同时等待 Driver
分发任务,当分发的任务执行完毕后,将任务状态上报给 Driver
。
从上述时序图可知,Client 只负责提交 Application 并监控 Application 的状态。对于 Spark 的任务调度主要是集中在两个方面: 资源申请和任务分发,其主要是通过 ApplicationMaster
、Driver
以及 Executor
之间来完成。
4.2 Spark 任务调度概述
当 Driver
起来后,Driver
则会根据用户程序逻辑准备任务,并根据 Executor
资源情况逐步分发任务。在详细阐述任务调度前,首先说明下 Spark 里的几个概念。一个 Spark 应用程序包括 Job
、Stage
以及 Task
三个概念:
- Job 是以 Action 方法为界, 遇到一个 Action 方法则触发一个 Job;
-
Stage
是 Job 的子集,以 RDD 宽依赖(即 Shuffle)为界,遇到 Shuffle 做一次划分; -
Task
是Stage
的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个 task。 一个task 对应一个RDD的分区
Spark 的任务调度总体来说分两路进行,一路是 Stage 级的调度,一路是 Task 级的调度,总体调度流程如下图所示:
Spark RDD 通过其 Transactions
操作,形成了 RDD 血缘关系图,即 DAG,最后通过 Action 的调用, 触发 Job 并调度执行。DAGScheduler
负责 Stage
级的调度主要是将 DAG 切分成若干 Stages
,并将每个 Stage 打包成 TaskSet
交给 TaskScheduler
调度。TaskScheduler
负责 Task 级的调度,将 DAGScheduler
给过来的 TaskSet
按照 指定的调度策略分发到 Executor
上执行,调度过程中 SchedulerBackend
负责提供可用资源,其中 SchedulerBackend
有多种实现,分别对接不同的资源管理系统。有了上述感性的认识后,下面这张图描述了 Spark-On-Yarn 模式下在任务调度期间,ApplicationMaster
、Driver
以及 Executor
内部模块的交互过程:
Driver
初始化 SparkContext
过 程 中 , 会 分 别 初 始 化 DAGScheduler
、 TaskScheduler
、SchedulerBackend
以及 HeartbeatReceiver
,并启动 SchedulerBackend
以及 HeartbeatReceiver
。SchedulerBackend
通过 ApplicationMaster
申请资源,并不断从 TaskScheduler
中拿到合适的 Task 分发到 Executor
执行。HeartbeatReceiver
负责接收 Executor
的心跳信息, 监控 Executor
的存活状况, 并通知到 TaskScheduler
。
4.3 Spark Stage 级调度
Spark 的任务调度是从 DAG 切割开始, 主要是由 DAGScheduler
来完成。当遇到一个 Action 操作后就会触发一个 Job 的计算, 并交给 DAGScheduler
来提交,下图是涉及到 Job 提交的相关方法调用流程图。
Job 由 最 终 的 RDD 和 Action 方 法 封 装 而 成 , SparkContext
将 Job 交给 DAGScheduler
提交,它会根据 RDD 的血缘关系构成的 DAG 进行切分,将一个 Job 划分为若干 Stages
,具体划分策略是,由最终的 RDD 不断通过依赖回溯判断父依赖是否是宽依赖,即以 Shuffle 为界,划分 Stage
,窄依赖的 RDD 之间被划分到同一个Stage
中,可以进行 pipeline 式的计算,如上图紫色流程部分。划分的 Stages
分两类, 一类叫做 ResultStage
,为 DAG 最下游的 Stage
,由 Action 方法决定,另一类叫做 ShuffleMapStage
,为下游 Stage
准备数据, 下面看一个简单的例子 WordCount。
Job 由 saveAsTextFile 触发,该 Job 由 RDD-3 和 saveAsTextFile 方法组成,根据 RDD 之间的依赖关系从 RDD-3 开始回溯搜索, 直到没有依赖的 RDD-0,在回溯搜索过程中,RDD-3 依赖 RDD-2, 并且是宽依赖, 所以在 RDD-2 和 RDD-3 之间划分Stage,RDD-3 被划到最后一个 Stage,即 ResultStage
中,RDD-2 依赖 RDD-1,RDD-1 依赖 RDD-0, 这些依赖都是窄依赖, 所以将 RDD-0、RDD-1 和 RDD-2 划分到同一个 Stage,即 ShuffleMapStage
中, 实际执行的时候, 数据记录会一气呵成地执行RDD-0 到 RDD-2 的转化。不难看出, 其本质上是一个深度优先搜索算法。
一个 Stage 是否被提交,需要判断它的父 Stage 是否执行,只有在父 Stage 执行完毕才能提交当前 Stage,如果一个 Stage 没有父 Stage,那么从该 Stage 开始提交。
相对来说 DAGScheduler
做的事情较为简单,仅仅是在 Stage
层面上划分 DAG, 提交 Stage
并监控相关状态信息。TaskScheduler
则相对较为复杂,下面详细阐述其细节。
4.4 Spark Task 级调度
Spark Task 的调度是由 TaskScheduler
来完成,由前文可知,DAGScheduler
将 Stage
打包到 TaskSet
交给 TaskScheduler
, TaskScheduler
会将 TaskSet
封装为 TaskSetManager
加入到调度队列中, TaskSetManager
结构如下图所示。
TaskSetManager
负责监控管理同一个 Stage
中的 Tasks
, TaskScheduler
就是以TaskSetManager
为单元来调度任务。前面也提到, TaskScheduler 初始化后会启动 SchedulerBackend, 它负责跟外界打交道,接收 Executor 的注册信息,并维护 Executor 的状态,所以说SchedulerBackend是管“粮食”的,同时它在启动后会定期地去“询问”TaskScheduler 有没有任务要运行,也就是说,它会定期地 “ 问 ”TaskScheduler“ 我有这么余量,你要不要啊 ” ,TaskScheduler 在 SchedulerBackend“问”它的时候, 会从调度队列中按照指定的调度策略选择 TaskSetManager 去调度运行, 大致方法调用流程如下图所示:
图中,将 TaskSetManager
加入 rootPool 调度池中之后,调用 SchedulerBackend
的 riciveOffers 方法给 driverEndpoint 发送 ReciveOffer 消息; driverEndpoint 收到 ReviveOffer 消息后调用 makeOffers 方法,过滤出活跃状态的 Executor
(这些 Executor
都是任务启动时反向注册到 Driver 的 Executor
),然后将 Executor
封装成 WorkerOffer 对象 ; 准 备 好 计 算 资 源 ( WorkerOffer ) 后, taskScheduler
基 于 这 些 资 源 调用 resourceOffer 在 Executor
上分配 task。