Spark 内核

1,724 阅读13分钟

spark outline

大纲目录

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之间是怎么进行通信的,该过程也非常复杂,如果展开叙述篇幅过大,下边会开一个标题专门叙述,而最后一处红色标记,也会开标题专门叙述

以上的叙述的逻辑宏观描述:

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

spark client与spark cluster的区别:

区别就是Driver运行在集群内部,还是集群外部(客户端)

为什么不选择把Driver运行在客户端

Driver运行在客户端,不便于后期Executor会和Driver频繁的信息交换,因为其过程会大量涉及网络IO等

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会先对taskset进行包装,包装成taskmanager,将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注册为例,大致描述一下流程:

流程图.jpg

  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个阶段

在这里插入图片描述

补充:当代码中有多个shuffle算子时,最后一个阶段称之为ResultStage,这个阶段之前的统称为ShuffleMapStage

task个数确定

在这里插入图片描述

reduce task数据拉取过程

  1. map task 执行完毕后会将计算状态以及磁盘小文件位置等信息封装到MapStatus对象中,然后由本进程中的MapOutPutTrackerWorker对象将mapStatus对象发送给Driver进程的MapOutPutTrackerMaster对象
  2. 在reduce task开始执行之前会先让本进程中的MapOutputTrackerWorker向Driver进程中的MapoutPutTrakcerMaster发动请求,请求磁盘小文件位置信息
  3. 当所有的Map task执行完毕后,Driver进程中的MapOutPutTrackerMaster就掌握了所有的磁盘小文件的位置信息。此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息
  4. 步骤1,2,3完成之后,由BlockTransforService去某个Executor0所在的节点拉数据,默认会启动五个子线程。每次拉取的数据量不能超过48M,将拉来的数据存储到Executor内存的20%内存中

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运行机制(无排序)

不排序的条件:

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

流程图:

在这里插入图片描述

spark的Shuffle和Hadoop的shuffle异同?

  • 相同点 从数据的处理阶段上看,2者没啥区别,都是先在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有3次排序,spark只有2次排序,并且归排过后,mr只有一个数据文件,spark不仅有数据文件,而且也有一个索引文件,这也就导致了mr在map阶段过后会有大量的小文件,而spark的小文件却相对很少
  3. mr的shuffle过程必须要排序,spark的shuffle可以选择不进行排序,例如在一些求和和求平均值场景下,排序只会带来不必要的资源消耗
  4. 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):直接向结点操作系统借的

spark 内存空间划分

Spark早期版本使用的是静态内存管理机制

Spark 1.6 之后使用的是统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,统一内存管理的堆内内存结构如下图所示:

在这里插入图片描述

在这里插入图片描述

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

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

为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据

堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响

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

在这里插入图片描述

1.存储内存和执行内存双方的空间都不足时,则存储到硬盘;若有一方内存空间不足而对方空余时,可借用对方的空间(存储空间不足是指不足以放下一个完整的 Block) 2. 存储内存占用执行内存的空间后,可让存储内存将占用的部分转存到硬盘,然后”归还”借用的空间 3. 执行内存占用存储内存后,无法让执行内存”归还”,因为要保证数据结果的准确性