Spark 内核解析

该篇我们将会从如下几个方向进行学习Spark的内核机制:

    • Spark的通信架构

    • Spark的脚本解析

    • Spark的交互流程

    • Spark的Shuffle过程

    • Spark的内存管理

    • Spark的部署模式

 

1.Spark 通信架构

    Spark作为分布式计算框架,多个节点的设计与相互通信模式是其重要的组成部分。Spark一开始使用 Akka 作为内部通信部件。在Spark 1.3年代,为了解决大块数据(如Shuffle)的传输问题,Spark引入了Netty通信框架。到了 Spark 1.6, Spark可以配置使用 Akka 或者 Netty 了,这意味着 Netty 可以完全替代 Akka了。再到 Spark 2, Spark 已经完全抛弃 Akka了,全部使用Netty了。

    为什么呢?官方的解释是:

    1) 很多Spark用户也使用Akka,但是由于Akka不同版本之间无法互相通信,这就要求用户必须使用跟Spark完全一样的Akka版本,导致用户无法升级Akka。

    2) Spark的Akka配置是针对Spark自身来调优的,可能跟用户自己代码中的Akka配置冲突。

    3) Spark用的Akka特性很少,这部分特性很容易自己实现。同时,这部分代码量相比Akka来说少很多,debug比较容易。如果遇到什么bug,也可以自己马上fix,不需要等Akka上游发布新版本。而且,Spark升级Akka本身又因为第一点会强制要求用户升级他们使用的Akka,对于某些用户来说是不现实的。

1.1 通信组件概览

    对源码分析,对于设计思路理解如下:

    1)  RpcEndpoint:RPC端点 ,Spark针对于每个节点(Client/Master/Worker)都称之一个Rpc端点 ,且都实现RpcEndpoint接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(询问)则调用Dispatcher

    2)  RpcEnv:RPC上下文环境,每个Rpc端点运行时依赖的上下文环境称之为RpcEnv

    3)  Dispatcher:消息分发器,针对于RPC端点需要发送消息或者从远程RPC接收到的消息,分发至对应的指令收件箱/发件箱。如果指令接收方是自己存入收件箱,如果指令接收方为非自身端点,则放入发件箱

    4)  Inbox:指令消息收件箱,一个本地端点对应一个收件箱,Dispatcher在每次向Inbox存入消息时,都将对应EndpointData加入内部待Receiver Queue中,另外Dispatcher创建时会启动一个单独线程进行轮询Receiver Queue,进行收件箱消息消费

    5)  OutBox:指令消息发件箱,一个远程端点对应一个发件箱,当消息放入Outbox后,紧接着将消息通过TransportClient发送出去。消息放入发件箱以及发送过程是在同一个线程中进行,这样做的主要原因是远程消息分为RpcOutboxMessage, OneWayOutboxMessage两种消息,而针对于需要应答的消息直接发送且需要得到结果进行处理

    6)  TransportClient:Netty通信客户端,根据OutBox消息的receiver信息,请求对应远程TransportServer

    7)  TransportServer:Netty通信服务端,一个RPC端点一个TransportServer,接受远程消息后调用Dispatcher分发消息至对应收发件箱

注意:

    TransportClient与TransportServer通信虚线表示两个RpcEnv之间的通信,图示没有单独表达式

    一个Outbox一个TransportClient,图示没有单独表达式

    一个RpcEnv中存在两个RpcEndpoint,一个代表本身启动的RPC端点,另外一个为 RpcEndpointVerifier

 

2.Spark 脚本解析

 

3.Spark 交互流程(Standalone模式讲解)

3.1 Spark的交互流程,节点启动流程:

    1)  Master启动时首先创一个RpcEnv对象,负责管理所有通信逻辑

    2)  Master通过RpcEnv对象创建一个Endpoint,Master就是一个Endpoint,Worker可以与其进行通信

    3)  Worker启动时也是创一个RpcEnv对象

    4)  Worker通过RpcEnv对象创建一个Endpoint

    5)  Worker通过RpcEnv对,建立到Master的连接,获取到一个RpcEndpointRef对象,通过该对象可以与Master通信

    6)  Worker向Master注册,注册内容包括主机名、端口、CPU Core数量、内存数量

    7)  Master接收到Worker的注册,将注册信息维护在内存中的Table中,其中还包含了一个到Worker的RpcEndpointRef对象引用

    8)  Master回复Worker已经接收到注册,告知Worker已经注册成功

    9)  此时如果有用户提交Spark程序,Master需要协调启动Driver;而Worker端收到成功注册响应后,开始周期性向Master发送心跳

 

3.2 核心组件交互流程

    在Standalone模式下,Spark中各个组件之间交互还是比较复杂的,但是对于一个通用的分布式计算系统来说,这些都是非常重要而且比较基础的交互。首先,为了理解组件之间的主要交互流程,我们给出一些基本要点:

    一个Application会启动一个Driver

    一个Driver负责跟踪管理该Application运行过程中所有的资源状态和任务状态

    一个Driver会管理一组Executor

    一个Executor只执行属于一个Driver的Task

    核心组件之间的主要交互流程,如下图所示:

    上图中,通过不同颜色或类型的线条,给出了如下6个核心的交互流程,我们会详细说明:

橙色:提交用户Spark程序

    用户提交一个Spark程序,主要的流程如下所示:

     1)  用户spark-submit脚本提交一个Spark程序,会创建一个ClientEndpoint对象,该对象负责与Master通信交互

    2)  ClientEndpoint向Master发送一个RequestSubmitDriver消息,表示提交用户程序

    3)  Master收到RequestSubmitDriver消息,向ClientEndpoint回复SubmitDriverResponse,表示用户程序已经完成注册

    4)  ClientEndpoint向Master发送RequestDriverStatus消息,请求Driver状态   

     5)  如果当前用户程序对应的Driver已经启动,则ClientEndpoint直接退出,完成提交用户程序

紫色:启动Driver进程

    当用户提交用户Spark程序后,需要启动Driver来处理用户程序的计算逻辑,完成计算任务,这时Master协调需要启动一个Driver,具体流程如下所示:

    1)  Maser内存中维护着用户提交计算的任务Application,每次内存结构变更都会触发调度,向Worker发送LaunchDriver请求

    2)  Worker收到LaunchDriver消息,会启动一个DriverRunner线程去执行LaunchDriver的任务

    3)  DriverRunner线程在Worker上启动一个新的JVM实例,该JVM实例内运行一个Driver进程,该Driver会创建SparkContext对象

红色:注册Application

    Dirver启动以后,它会创建SparkContext对象,初始化计算过程中必需的基本组件,并向Master注册Application,流程描述如下:

    1)  创建SparkEnv对象,创建并管理一些基本组件

    2)  创建TaskScheduler,负责Task调度

    3)  创建StandaloneSchedulerBackend,负责与ClusterManager进行资源协商

    4)  创建DriverEndpoint,其它组件可以与Driver进行通信

    5)  在StandaloneSchedulerBackend内部创建一个StandaloneAppClient,负责处理与Master的通信交互

    6)  StandaloneAppClient创建一个ClientEndpoint,实际负责与Master通信

    7)  ClientEndpoint向Master发送RegisterApplication消息,注册Application

    8)  Master收到RegisterApplication请求后,回复ClientEndpoint一个RegisteredApplication消息,表示已经注册成功

蓝色:启动Executor进程

    1)  Master向Worker发送LaunchExecutor消息,请求启动Executor;同时Master会向Driver发送ExecutorAdded消息,表示Master已经新增了一个Executor(此时还未启动)

    2)  Worker收到LaunchExecutor消息,会启动一个ExecutorRunner线程去执行LaunchExecutor的任务

    3)  Worker向Master发送ExecutorStageChanged消息,通知Executor状态已发生变化

    4)  Master向Driver发送ExecutorUpdated消息,此时Executor已经启动

粉色:启动Task执行

    1)  StandaloneSchedulerBackend启动一个DriverEndpoint

    2)  DriverEndpoint启动后,会周期性地检查Driver维护的Executor的状态,如果有空闲的Executor便会调度任务执行

    3)  DriverEndpoint向TaskScheduler发送Resource Offer请求

    4)  如果有可用资源启动Task,则DriverEndpoint向Executor发送LaunchTask请求

    5)  Executor进程内部的CoarseGrainedExecutorBackend调用内部的Executor线程的launchTask方法启动Task

    6)  Executor线程内部维护一个线程池,创建一个TaskRunner线程并提交到线程池执行

绿色:Task运行完成

    1)  Executor进程内部的Executor线程通知CoarseGrainedExecutorBackend,Task运行完成

    2)  CoarseGrainedExecutorBackend向DriverEndpoint发送StatusUpdated消息,通知Driver运行的Task状态发生变更

    3)  StandaloneSchedulerBackend调用TaskScheduler的updateStatus方法更新Task状态

    4)  StandaloneSchedulerBackend继续调用TaskScheduler的resourceOffers方法,调度其他任务运行

 

3.3 SPARK的交互流程 – 任务运行

    1)  Application

   用户编写的Spark程序,完成一个计算任务的处理。它是由一个Driver程序和一组运行于Spark集群上的Executor组成。

    2)  Job

   用户程序中,每次调用Action时,逻辑上会生成一个Job,一个Job包含了多个Stage。

    3)  Stage

      Stage包括两类:ShuffleMapStage和ResultStage,如果用户程序中调用了需要进行Shuffle计算的Operator,如groupByKey等,就会以Shuffle为边界分成ShuffleMapStage和ResultStage。

    4)  TaskSet

   基于Stage可以直接映射为TaskSet,一个TaskSet封装了一次需要运算的、具有相同处理逻辑的Task,这些Task可以并行计算,粗粒度的调度是以TaskSet为单位的。

    5)  Task

      Task是在物理节点上运行的基本单位,Task包含两类:ShuffleMapTask和ResultTask,分别对应于Stage中ShuffleMapStage和ResultStage中的一个执行基本单元。

 

4.Spark的Shuffle过程

4.1 MapReduce 的 Shuffle 过程

    Shuffle横跨Map端和Reduce端,在Map端包括Spill过程,在Reduce端包括copy和sort过程

 

4.2 Spark 的 Shuffle 过程(HashShuffle 过程)

    Map任务会为每个Reduce创建对应的bucket,Map产生的结果会根据设置的Partitioner得到对应的bucketId,然后填充到相应的bucket中去。每个Map的输出结果可能包含所有的Reduce所需要的数据,所以每个Map会创建R个bucket(R是reduce的个数),M个Map总共会创建M*R个bucket。

然后bucket缓冲区中的数据就写入到以文件名hash值为名的blockfile中,Reduce从远端或者本地逐个读取自己需要的文件,放到一个大的Hashmap中,key是Map输出的key,Map输出对应这个key的所有value组成HashMap的value,如果内存不够就溢写到磁盘。

    说明:HashShuffle 的优化

    这种方式的改进是减少了map端的磁盘文件数量,map在某个节点上第一次执行,为每个reduce创建bucket对应的输出文件,把这些文件组织成ShuffleFileGroup,当这次map执行完之后,这个ShuffleFileGroup可以释放为下次循环利用;当又有map在这个节点上执行时,不需要创建新的bucket文件,而是在上次的ShuffleFileGroup中取得已经创建的文件继续追加写一个segment;当前次map还没执行完,ShuffleFileGroup还没有释放,这时如果有新的map在这个节点上执行,无法循环利用这个ShuffleFileGroup,而是只能创建新的bucket文件组成新的ShuffleFileGroup来写输出。reduce过程与第一阶段相同。

 

4.3  Spark 的 Shuffle 过程(SortShuffle 过程)

    实现逻辑类似于Hadoop MapReduce,Hash Shuffle每一个reducers产生一个文件,但是Sort Shuffle只是产生一个按照reducer id排序可索引的文件,也就是一个数据文件和一个索引文件。

    数据文件中的数据按照Reducer排序,但属于同一个Reducer的数据不排序。Mapper产生的数据先放到AppendOnlyMap这个数据结构中,如果内存不够,数据则会spill到磁盘,最后合并成一个文件。reduce端进行远程或者本地读取进行合并。

    需要注意的一点是对于reducer数比较少的情况,Hash Shuffle明显要比Sort Shuffle快,因此Sort Shuffle有个“fallback”计划,对于reducers数少于 “spark.shuffle.sort.bypassMergeThreshold” (200 by default),我们使用fallback计划,hashing相关数据到分开的文件,然后合并这些文件为一个。

    说明:SortShuffle 的优化

 

4.4 MapReduce与Spark过程对比

    MapReduce和Spark的Shuffle过程对比如下:

对比方向

MapReduce

Spark Hash

collect

 

在内存中构造了一块数据结构用于map输出的缓冲

没有在内存中构造一块数据结构用于map输出的缓冲,而是直接把输出写到磁盘文件

sort

map输出的数据有排序

map输出的数据没有排序

merge

对磁盘上的多个spill文件最后进行合并成一个输出文件

map端没有merge过程,在输出时直接是对应一个reduce的数据写到一个文件中,这些文件同时存在并发写,最后不需要合并成一个

copy

框架 jetty

netty或者直接socket

本地文件

仍然是通过网络框架拖取数据

不通过网络框架,对于在本节点上的map输出文件,采用本地读取的方式

copy

过来的数据存放位置 先放在内存,内存放不下时写到磁盘

一种方式全部放在内存;另一种方式先放在内存,放不下时写到磁盘

merge sort

最后会对磁盘文件和内存中的数据进行合并排序

对于采用另一种方式时也会有合并排序的过程

 

5.Spark的内存管理

    Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色。理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优。本文中阐述的原理基于 Spark 2.1 版本。

在执行 Spark 的应用程序时,Spark 集群会启动 Driver 和 Executor 两种 JVM 进程,前者为主控进程,负责创建 Spark 上下文,提交 Spark 作业(Job),并将作业转化为计算任务(Task),在各个 Executor 进程间协调任务的调度,后者负责在工作节点上执行具体的计算任务,并将结果返回给 Driver,同时为需要持久化的 RDD 提供存储功能。由于 Driver 的内存管理相对来说较为简单,本文主要对 Executor 的内存管理进行分析,下文中的 Spark 内存均特指 Executor 的内存。

5.1 堆内和堆外内存规划

    作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对 JVM 的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用.堆内和堆外内存示意图如下:

5.1.1  堆内内存

    Executor 的内存管理建立在 JVM 的内存管理之上。 –executor-memory 指定Executor内存,并不能准确记录实际可用的堆内内存

5.1.2  堆外内存

    Spark 引入了堆外(Off-heap)内存,利用 JDK Unsafe API进行实现。存储序列化后的二进制数据

5.1.3 说明:

    通过配置 spark.memory.offHeap.enabled 参数启用,并由 spark.memory.offHeap.size 参数设定堆外空间的大小

5.2 内存空间分配

5.2.1 静态内存管理

5.2.2 统一内存管理

 

5.3 存储内存管理

5.3.1 RDD 的持久化机制

5.3.2 RDD 缓存的过程

5.3.3 淘汰和落盘

5.4 执行内存管理

5.4.1 多任务间内存分配

5.4.2 Shuffle 的内存占用

 

6.Spark的部署模式

    Spark支持的主要的三种分布式部署方式分别是standalone、spark on mesos和 spark on YARN。

    standalone模式,即独立模式,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统。

    yarn是统一的资源管理机制,在上面可以运行多套计算框架,如map reduce、storm等根据driver在集群中的位置不同,分为yarn client和yarn cluster。

    mesos是一个更强大的分布式资源管理框架,它允许多种不同的框架部署在其上,包括yarn。

用户在提交任务给Spark处理时,以下两个参数共同决定了Spark的运行方式。

    • –master MASTER_URL :决定了Spark任务提交给哪种集群处理。

    • –deploy-mode DEPLOY_MODE:决定了Driver的运行方式,可选值为Client或者Cluster。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员学习圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值