Spark 内核

Spark 运行流程宏观描述

申请资源—>Driver线程被阻塞—>反射执行main()方法,初始化SparkContext上下文环境—>资源继续申请,申请成功—>执行用户程序后续代码—>任务切分、任务提交、任务执行

Spark基于Yarn的Cluster提交作业流程

在这里插入图片描述

  1. 代码打成jar包上传到集群后,执行脚本命令spark-submit
  2. 执行类 SparkSubmit,这时会创建一个进程(这个进程在控制台黑窗口中可以看到:SparkSubmit)
  3. 通过类中的解析参数的方法 parseArguments(args)去解析参数,参数中包括 --master --class 等信息
  4. 参数解析完毕后准备提交,然后判断是mesosCluster模式、Client模式还是yarnCluster模式,若模式为yarnCluster,则会构建yarnClient和RM进行通信,然后由yarnClient向RM提交 Application, Application中其实就是一些指令:例如请求RM 启动 AM
  5. RM会给找一个空闲的NM启动AM,AM启动后创建一个客户端(AMRMClient) ,用来和RM进行通信
  6. 当运行到runDriver()方法时,会根据参数启动一个Driver线程,Driver会去读取用户程序代码,初始化 SparkContext 上下文环境, 判断sc(SparkContext)是否为空,并同时开始初始化 RPCEnv 通信环境
  7. 因为拿到了上下文环境,所以AM客户端(AMRMClient)就会知道该程序需要用多少资源,然后和RM进行通信申请资源
  8. RM返回资源列表给AM客户端,AM这边会在线程池中创建ExecutorRunable对象,由该对象创建NMClient,创建NMClient的目的是和其它NM进行通信来,创建Executor
  9. 和其它NM建立连接后,NM首先会启动一个ExecutorBackend后台进程(这个进程在控制台黑窗口中可以看到:CoarseGrainedExecutorBackend),随之设置RPCEnv 通信环境
  10. 通信环境设置成功以后,会向Driver请求注册Executor,Driver返回应答后开始真正创建 Executor 计算对象,随之继续执行用户编写的程序代码

总结:

以上10步则为申请资源,即把driver和executor所需要的资源都准备好,然后开始真正执行用户编写的程序代码了,这也就有后续的任务的切分,任务的提交等操作了

注意:
以上有3处标红的地方,前2处是涉及到Driver和Executor之间是怎么进行通信的,该过程也非常复杂,如果展开叙述篇幅过大,下边会开一个标题专门叙述,而最后一处红色标记,也会开标题专门叙述

Spark 任务的划分

Spark Job 划分

Spark 任务的提交流程

  1. 在Driver初始化SparkContext环境时,同时也初始化了RPCEnv通信环境、TaskScheduler、DAGScheduler等对象
  2. 当Executor 计算对象创建完毕后,Driver就开始执行用户编写的程序代码了,当在代码中遇到行动算子时会触发任务的执行,然后按照rdd之间的血缘关系形成一个DAG有向无环图,并发送给 DAGScheduler
  3. DAGScheduler根据其中宽依赖的个数划分stage,再根据最后一个RDD的分区数把每个stage划分成一个个的task,然后把这些task包装成taskset,发送给TaskScheduler(TaskScheduler就是调度器,有2种调度器,FIFO:默认、公平:企业用),TaskScheduler也会对taskset进行包装,包装成taskmanager(taskmanager就是对task进行管理的,例如task失败重试),将taskmanager放入任务池的调度器中,并在任务池当中进行本地化级别的判断,本地化级别简单来说就是—移动数据,还是移动计算逻辑到某结点上,选择好本地化级别后,会将任务池当中的任务数据取出并序列化,通过Driver端RPCEnv通信模块发送到Exector计算结点上,然后Exector会根据接收到的信息,开启相应的task进行数据计算

Spark 通信流程

简介:

该通信是用于Driver和Executor之间是怎么进行通信的

Spark 通信架构前后共有2种,一种是Akka,另一种是的Netty

spark 使用的通信框架大致演变过程

在 Spark0.x系列中, 通信框架是Akka

在 Spark1.3 中引入了 Netty 通信框架,引入这个框架的目的是为了解决Shuffle的大数据传输问题

Spark1.6 中 Netty 完全实现了 Akka 在Spark 中的功能,所以从Spark2.0.0, Akka 被移除了

为什么使用Netty框架?

因为它不仅采用了异步非阻塞式IO,即AIO,也解决了Shuffle的大数据传输问题

通信IO大致分为3类

  1. BIO阻塞式IO
  2. NIO :非阻塞式IO
  3. AIO:异步非阻塞式IO

缺点:linux对AIO支持不够友好,但是服务器一般都是linux系统,所以又借用了Epoll的方式来模仿AIO操作

Spark RPC通信流程

这里以executor向driver注册为例,大致描述一下通信流程:

在这里插入图片描述

  1. Driver端和ExeCutor端各有N个OutBox发件箱,和一个Inbox收件箱
  2. 由于双方各自既是消息的发送方又是消息的接收方,所以它们既有客户端又有服务器端
  3. 当NM的通信环境初始化好以后,Executor会通过ask方法向Driver请求注册,ask请求信息需要先经过自己的消息分发器Dispatcher分发到Outbox发件箱中,然后通过发件箱发送到Driver的Inbox收件箱中
  4. Driver接收到消息以后,会对消息的类型进行判断,判断出对方的消息类型为ask,那么则需要自己进行应答,应答消息经由自己的消息分发器先分发到Outbox发件箱中,然后发送到Executor服务器端的inbox收件箱中
  5. Executor接收到消息以后会先对消息类型进行判断,判读以后就启动task开始干活了

通信过程涉及很多未提及的专业名词,但是不重要,不是平台研发岗,无需掌握

组件概念解释:

  1. RpcEndpoint:RPC端点,Spark针对每个节点(Client/Master/Worker)都称之为一个Rpc端点,且都实现RpcEndpoint接口,内部根据不同端点的需求,设计了不同的消息和不同的业务处理逻辑,如果需要send message or receive message 则调用Dispatcher
  2. RpcEnv:RPC上下文环境,每个RPC端点运行时依赖的上下文环境称为RpcEnv
  3. Dispatcher:消息分发器,针对于RPC端点需要发送消息或者从远程RPC接收到的消息,分发至对应的指令收件箱/发件箱。如果指令接收方是自己则存入收件箱,如果指令接收方不是自己,则放入发件箱
  4. Inbox:指令消息收件箱,一个本地RpcEndpoint对应一个收件箱,Dispatcher在每次向Inbox存入消息时,都将对应EndpointData加入内部ReceiverQueue中,另外Dispatcher创建时会启动一个单独线程进行轮询ReceiverQueue,进行收件箱消息消费
  5. RpcEndpointRef:RpcEndpointRef是对远程RpcEndpoint的一个引用。当我们需要向一个具体的RpcEndpoint发送消息时,一般我们需要获取到该RpcEndpoint的引用,然后通过该应用发送消息
  6. OutBox:指令消息发件箱,对于当前RpcEndpoint来说,一个目标RpcEndpoint对应一个发件箱,如果向多个目标RpcEndpoint发送信息,则有多个OutBox。当消息放入Outbox后,紧接着通过TransportClient将消息发送出去。消息放入发件箱以及发送过程是在同一个线程中进行
  7. RpcAddress:远程RpcEndpointRef的Host + Port
  8. TransportClient:Netty通信客户端,一个OutBox对应一个TransportClient,TransportClient不断轮询OutBox,根据OutBox消息的receiver信息,请求对应的远程TransportServer
  9. TransportServer:Netty通信服务端,一个RpcEndpoint对应一个TransportServer,接受远程消息后调用Dispatcher分发消息至对应收发件箱

Spark Shuffle

Shuffle阶段的划分

当代码中有shuffle算子时,会做一次阶段划分,然后分为2个阶段:ShuffleMapStage、ResultStage。shuffle算子之后的称为ResultStage,之前的统称为ShuffleMapStage

Shuffle类型

在这里插入图片描述

HashShuffle

Spark 2.0 版本中, Hash Shuffle 方式己经不再使用

(1)未优化的HashShuffleManager

特点:下一个stage的task有多少个,当前stage的每个task就要创建多少份磁盘文件

比如说:下一个 stage 总共有 100 个 task,当前 stage 有 50 个 task,那么需要创建50*100=5000个小文件

优点:没有排序
缺点:小文件过多,会引起大量的磁盘IO

(2)优化后的HashShuffleManager

特点:第一批并行执行的每个task都会创建一个shuffleFileGroup(组内文件的数量与下游 stage 的 task 数量是相同),以后的第二批、第三批等的task都会复用第一批task创建的shuffleFileGroupshuffleFileGroup

优点:没有排序
缺点:依旧不能有效缓解磁盘小文件创建数量

SortShuffle

(1)普通运行机制(有排序) ,且是默认的

优点:减少了小文件

流程图:

在这里插入图片描述

  1. map task 首先会将数据写入到内存数据结构里面,如果是 reduceByKey 这种聚合类的 shuffle 算子,那么就会写入到 Map 数据结构,一边通过 Map 进行聚合,一边写入内存,如果是 join 这种普通的 shuffle 算子,那么就会写入 Array 数据结构,然后直接写入内存,内存数据结构默认是5M

  2. 在数据写入内存后,会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过5M时,它首先会尝试申请内存,如果申请成功,继续写入内存,如果申请失败,那么就会溢写数据到磁盘文件(比如现在内存结构中的数据为5.01M,那么它会申请 5.01*2-5=5.02M 内存)

  3. 在溢写之前会对内存中的数据进行快排

  4. 排序好的数据会以batch(一个batch是1万条数据)的形式先先写入java的内存缓冲区(32k),待缓冲区满后,写入磁盘

  5. map task执行完成后,磁盘中可能会产生大量小文件,然后对这些小文件进行一次归并排序,合并成一个大文件,同时生成一个索引文件

  6. reduce task去map端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取自己指定分区的数据,存入内存,内存不够,溢写磁盘(内存大小48m)

(2)bypass运行机制(无排序)

不排序的条件:

  1. map端没有预聚合(即不是聚合类的shuffle算子)

reduceByKey:是shuffle类算子并且有map端有预聚合功能
groupByKey:是shuffle类算子但没有map端预聚合功能

  1. shuffle上一阶段最后一个RDD的分区数小于200 (spark.shuffle.sort.bypassMergeThreshold=200)

特点:该过程的磁盘写机制其实跟未经优化的 HashShuffle是一模一样的,都要创建数量惊人的磁盘文件,只不过是会在最后做一个磁盘文件的合并而已

流程图:

在这里插入图片描述

Spark的Shuffle和Hadoop的shuffle异同?

  • 相同点

从宏观上看,2者没啥区别

(1)都是先在Map端对数据进行处理,然后在Reduce端对数据进行处理
(2)都有对数据进行分区,和提前进行预聚合的功能

  • 不同点

从细节上看

  1. shuffle的分类不同: spark的shuffle分为2大类,第一类是HashShuffle,第二类是SortShuffle。HashShuffle又分为未优化的HashShuffle和未优化的HashShuffle(Spark 2.0 版本中, Hash Shuffle 方式己经不再使用);SortShuffle也分为可排序的和不可排序的Shuffle,而mr的shuflle只有一种必须排序的shuffle。在一些求和和求平均值场景下,排序只会带来不必要的资源消耗

  2. 若都为可排序的shuffle:mr 的map阶段过后,只有一个数据文件,而spark不仅有数据文件,而且还有一个索引文件,由于有了索引文件,那么数据文件就可以相对很大,数据文件越大,那么后续的reduce task数量就越少

  3. mr的shuffle前中后有着更为细致的划分,例如在map端可细划分为Read、Map、Collect、Spill(溢写)Merge阶段,reduce端可细划分为pull、merge、reduce阶段,而spark的shuffle没有明显的阶段划分,只有在遇到行动算子时,才会去真正的拉取数据

怎么提高shuffle的效率

提高shuffle性能最有效的办法,提前进行预聚合,有预聚合功能的算子有

  • foldbykey
  • reducebykey
  • aggregatebykey
  • combinebykey

Spark 内存管理

Spark 内存种类划分

spark 内存分为堆内和堆外2种

  • 堆内内存(On-heap):建立在 JVM 的内存管理之上,受JVM统一管理
  • 堆外内存(Off-heap):向操作系统借的,直接受操作系统管理,不由JVM统一管理

Spark 内存空间划分

Spark 1.6 之前,堆内内存使用的是静态内存管理机制,Spark 1.6 之后,使用的是统一内存管理机制,即动态内存管理机制,动态内存管理机制和静态内存管理的最大区别在于,动态内存管理机制下,存储内存和执行内存共享同一块空间,当某一方的内存不足时,可以动态占用对方的空闲内存。

统一内存的堆内内存结构如下图所示:

在这里插入图片描述

统一内存的堆外内存结构如图:

在这里插入图片描述
堆外内存的作用:

  1. 为了提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,来直接存储排序好的数据

  2. 堆外内存不受Java虚拟机管理,那么就可以很有效的减少垃圾回收对应用的影响

统一内存最重要的优化在于动态占用机制,其规则如下:

在这里插入图片描述

  1. 存储内存和执行内存双方的空间都不足时,则存储到硬盘;若有一方内存空间不足而对方空余时,可借用对方的空间(存储空间不足是指不足以放下一个完整的 Block)

  2. 存储内存占用执行内存的空间后,执行内存不够时,可让存储内存将占用的部分转存到硬盘,然后”归还”借用的空间

  3. 执行内存占用存储内存后,存储内存不够时,有肯无法让执行内存”归还”,因为要保证数据结果的准确性

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值