文章目录
Spark 运行流程宏观描述
申请资源—>Driver线程被阻塞—>反射执行main()方法,初始化SparkContext上下文环境—>资源继续申请,申请成功—>执行用户程序后续代码—>任务切分、任务提交、任务执行
Spark基于Yarn的Cluster提交作业流程
- 代码打成jar包上传到集群后,执行脚本命令spark-submit
- 执行类 SparkSubmit,这时会创建一个进程(这个进程在控制台黑窗口中可以看到:SparkSubmit)
- 通过类中的解析参数的方法 parseArguments(args)去解析参数,参数中包括 --master --class 等信息
- 参数解析完毕后准备提交,然后判断是mesosCluster模式、Client模式还是yarnCluster模式,若模式为yarnCluster,则会构建yarnClient和RM进行通信,然后由yarnClient向RM提交 Application, Application中其实就是一些指令:例如请求RM 启动 AM
- RM会给找一个空闲的NM启动AM,AM启动后创建一个客户端(AMRMClient) ,用来和RM进行通信
- 当运行到runDriver()方法时,会根据参数启动一个Driver线程,Driver会去读取用户程序代码,初始化 SparkContext 上下文环境, 判断sc(SparkContext)是否为空,
并同时开始初始化 RPCEnv 通信环境
- 因为拿到了上下文环境,所以AM客户端(AMRMClient)就会知道该程序需要用多少资源,然后和RM进行通信申请资源
- RM返回资源列表给AM客户端,AM这边会在线程池中创建ExecutorRunable对象,由该对象创建NMClient,创建NMClient的目的是和其它NM进行通信来,创建Executor
- 和其它NM建立连接后,NM首先会启动一个ExecutorBackend后台进程(这个进程在控制台黑窗口中可以看到:CoarseGrainedExecutorBackend),
随之设置RPCEnv 通信环境
- 通信环境设置成功以后,会向Driver请求注册Executor,Driver返回应答后开始真正创建 Executor 计算对象,
随之继续执行用户编写的程序代码
总结:
以上10步则为申请资源,即把driver和executor所需要的资源都准备好,然后开始真正执行用户编写的程序代码了,这也就有后续的任务的切分,任务的提交等操作了
注意:
以上有3处标红的地方,前2处是涉及到Driver和Executor之间是怎么进行通信的,该过程也非常复杂,如果展开叙述篇幅过大,下边会开一个标题专门叙述,而最后一处红色标记,也会开标题专门叙述
Spark 任务的划分
Spark 任务的提交流程
- 在Driver初始化SparkContext环境时,同时也初始化了RPCEnv通信环境、TaskScheduler、DAGScheduler等对象
- 当Executor 计算对象创建完毕后,Driver就开始执行用户编写的程序代码了,当在代码中遇到行动算子时会触发任务的执行,然后按照rdd之间的血缘关系形成一个DAG有向无环图,并发送给 DAGScheduler
- 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类
- BIO阻塞式IO
- NIO :非阻塞式IO
- AIO:异步非阻塞式IO
缺点:linux对AIO支持不够友好,但是服务器一般都是linux系统,所以又借用了Epoll的方式来模仿AIO操作
Spark RPC通信流程
这里以executor向driver注册为例,大致描述一下通信流程:
- Driver端和ExeCutor端各有N个OutBox发件箱,和一个Inbox收件箱
- 由于双方各自既是消息的发送方又是消息的接收方,所以它们既有客户端又有服务器端
- 当NM的通信环境初始化好以后,Executor会通过ask方法向Driver请求注册,ask请求信息需要先经过自己的消息分发器Dispatcher分发到Outbox发件箱中,然后通过发件箱发送到Driver的Inbox收件箱中
- Driver接收到消息以后,会对消息的类型进行判断,判断出对方的消息类型为ask,那么则需要自己进行应答,应答消息经由自己的消息分发器先分发到Outbox发件箱中,然后发送到Executor服务器端的inbox收件箱中
- Executor接收到消息以后会先对消息类型进行判断,判读以后就启动task开始干活了
通信过程涉及很多未提及的专业名词,但是不重要,不是平台研发岗,无需掌握
组件概念解释:
- RpcEndpoint:RPC端点,Spark针对每个节点(Client/Master/Worker)都称之为一个Rpc端点,且都实现RpcEndpoint接口,内部根据不同端点的需求,设计了不同的消息和不同的业务处理逻辑,如果需要send message or receive message 则调用Dispatcher
- RpcEnv:RPC上下文环境,每个RPC端点运行时依赖的上下文环境称为RpcEnv
- 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 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)普通运行机制(有排序) ,且是默认的
优点:
减少了小文件
流程图:
-
map task 首先会将数据写入到内存数据结构里面,如果是 reduceByKey 这种聚合类的 shuffle 算子,那么就会写入到 Map 数据结构,一边通过 Map 进行聚合,一边写入内存,如果是 join 这种普通的 shuffle 算子,那么就会写入 Array 数据结构,然后直接写入内存,内存数据结构默认是5M
-
在数据写入内存后,会有一个定时器,不定期的去估算这个内存结构的大小,当内存结构中的数据超过5M时,它首先会尝试申请内存,如果申请成功,继续写入内存,如果申请失败,那么就会溢写数据到磁盘文件(比如现在内存结构中的数据为5.01M,那么它会申请 5.01*2-5=5.02M 内存)
-
在溢写之前会对内存中的数据进行快排
-
排序好的数据会以batch(一个batch是1万条数据)的形式先先写入java的内存缓冲区(32k),待缓冲区满后,写入磁盘
-
map task执行完成后,磁盘中可能会产生大量小文件,然后对这些小文件进行一次归并排序,合并成一个大文件,同时生成一个索引文件
-
reduce task去map端拉取数据的时候,首先解析索引文件,根据索引文件再去拉取自己指定分区的数据,存入内存,内存不够,溢写磁盘(内存大小48m)
(2)bypass运行机制(无排序)
不排序的条件:
- map端没有预聚合(即不是聚合类的shuffle算子)
reduceByKey:是shuffle类算子并且有map端有预聚合功能
groupByKey:是shuffle类算子但没有map端预聚合功能
- shuffle上一阶段最后一个RDD的分区数小于200 (spark.shuffle.sort.bypassMergeThreshold=200)
特点:
该过程的磁盘写机制其实跟未经优化的 HashShuffle是一模一样的,都要创建数量惊人的磁盘文件,只不过是会在最后做一个磁盘文件的合并而已
流程图:
Spark的Shuffle和Hadoop的shuffle异同?
- 相同点
从宏观上看,2者没啥区别
(1)都是先在Map端对数据进行处理,然后在Reduce端对数据进行处理
(2)都有对数据进行分区,和提前进行预聚合的功能
- 不同点
从细节上看
-
shuffle的分类不同: spark的shuffle分为2大类,第一类是HashShuffle,第二类是SortShuffle。HashShuffle又分为未优化的HashShuffle和未优化的HashShuffle(Spark 2.0 版本中, Hash Shuffle 方式己经不再使用);SortShuffle也分为可排序的和不可排序的Shuffle,而mr的shuflle只有一种必须排序的shuffle。在一些求和和求平均值场景下,排序只会带来不必要的资源消耗
-
若都为可排序的shuffle:mr 的map阶段过后,只有一个数据文件,而spark不仅有数据文件,而且还有一个索引文件,由于有了索引文件,那么数据文件就可以相对很大,数据文件越大,那么后续的reduce task数量就越少
-
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 之后,使用的是统一内存管理机制,即动态内存管理机制,动态内存管理机制和静态内存管理的最大区别在于
,动态内存管理机制下,存储内存和执行内存共享同一块空间,当某一方的内存不足时,可以动态占用对方的空闲内存。
统一内存的堆内内存结构如下图所示:
统一内存的堆外内存结构如图:
堆外内存的作用:
-
为了提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,来直接存储排序好的数据
-
堆外内存不受Java虚拟机管理,那么就可以很有效的减少垃圾回收对应用的影响
统一内存最重要的优化在于动态占用机制
,其规则如下:
-
存储内存和执行内存双方的空间都不足时,则存储到硬盘;若有一方内存空间不足而对方空余时,可借用对方的空间(存储空间不足是指不足以放下一个完整的 Block)
-
存储内存占用执行内存的空间后,执行内存不够时,可让存储内存将占用的部分转存到硬盘,然后”归还”借用的空间
-
执行内存占用存储内存后,存储内存不够时,有肯无法让执行内存”归还”,因为要保证数据结果的准确性