Spark 源码解析

Spark 源码解析

基于YarnCluster模式的任务提交流程:

  1. 通过spark-Submit命令脚本提交参数,声明部署模式、运行模式、全类名、Jar包、输入输出路径等,之后脚本启动执行。
  2. 脚本运行后会启动SparkSubmit进程,SparkSubmit启动之后会先解析命令行参数,之后会创建一个客户端YarnClient,
    YarnClient底层会通过java命令封装提交参数和命令,之后通过SubmitApplication提交任务信息给ResourceManager。
  3. ResourceManager接收到提交命令后,会在NodeManager上启动一个Container,并在Container中启动ApplicationMaster,之后ApplicationMaster中会启动一个YarnRMClient用来与ResourceManager进行通信。之后ApplicationMaster根据ResourceManager传递的参数启动Driver线程,并初始化SparkContext,这个过程中会等待,等待SparkContex初始化完成后才会继续执行其他操作。当SparkContext初始化完成之后ApplicationMaster会通过YarnRMClient向ResourceManager注册自己申请资源。
  4. ResouceManager收到申请之后评估,并返回资源可用列表。
  5. ApplicationMaster获取到资源可用列表之后会有一个启动池lanchPool,这个池子中会有一个ExecutorRunnable是一个NodeManager的客户端,用来和其他的NodeManager进行通信,然后回通过java命令到其他的NodeManager节点的Container容器中启动一个粗糙的Executor后端CoarseGrainedExecutorBackend,此时会启动Driver和CoarseGrainedExecutorBackend中的通信模块RPC"Spark通信架构"。然后CoarseGrainedExecutorBackend会通过自己的RPC向Driver申请注册Executor,Driver通过自己的RPC接收到申请后并返回注册成功消息,当Executor注册成功后才会真正创建Executor对象启动,之后向Driver汇报Executor启动成功。这里涉及RPC通信原理。
  6. Driver接收到Executor启动成功的消息后,会解析用户代码,然后会对任务进行切分"Stage任务划分",切分完任务之后会进行任务的分配"Task任务调度执行",将任务分配到不同的Executor执行。
  7. 接收到任务的Executor会有一个线程池ThreadPool,线程池中的每一个线程Thread都会跑一个Task。

Spark通信架构:

  • 有RpcEndPoint终端、Dispatcher转发器,用来调度协调,里面由一个Inbox收件箱用来接收信息、还有多个发件箱、RpcEndpointRef通信对方的引用。RpcEndPoint发送消息时首先要判断发送地址是否是自身,如果是直接发送到Inbox然后通过receive方法处理,如果不是自己需要将信息发送OutBox,每一个OutBox对应一个TransportClient消息发送客户端,然后该客户端会将消息发送给对方的TransportServer消息发送服务端,收到消息后该服务端会将接收到的消息发送到Inbox,然后会通过receive方法处理。如果RpcEndpointRef为ask模式处理完后需要回复,以同样的方式将消息发送到Driver端的收件箱Inbox中之后等待receive处理。

Stage任务划分:

  • 当任务遇到执行算子之后,开始对任务继续处理,首先DAGScheduler对Job进行Stage的划分,Stage产生Task。从后向前找宽依赖,碰到一个宽依赖切一刀,最后一个Stage叫RestuleStage,其余的Stage叫ShuffleMapStage,Stage的数量为宽依赖的个数加一,每个Job的Task个数为每个Stage阶段最后一个RDD的分区个数之和。之后TaskScheduler通过TaskSet获取每个Job中所有的Task然后序列化后发往Executor。

Task任务调度执行:

  • 首先会将用户代码转换成RDD,之后通过JobSubmitted提交作业到EventProcessLoop事件循环体中,里面放的是等待处理的Event。之后DAGScheduler会根据依赖对Stage进行划分,根据分区对Task划分,然后形成TaskSet。然后TaskScheduler通过ScheduerBuilder调度器的FIFO调度策略或者Fair调度策略以TaskSetManager为单位进行对任务进行调度,TaskSetManager内部封装了一个Statge中所有的Task,并负责调度这些Task。然后通过SchedulerBackend按照本地化级别分配原则进行任务的分配, 然后通过RPC通信模块,将任务发送到Outbox再通过TransportClient发送到Executor端的TransportServer,再由TransportServer将接收到的任务发送的Inbox,之后任务会进到对应Executor中的ThreadPool中启动线程执行Task。

Spark Shuffle :

  1. HashShuffle :
  2. SortShuffle :
    • 每个Task任务先会进入到Buffer缓冲区,之后将每个Buffer的数据都溢写到一份File文件中,之后将内存中的数据和磁盘中的数据Merge到一起写入到HDFS中,其中一部分是索引文件,另一部分是对应分区的数据文件,之后Reduce拉取对应分区的数据,默认拉取48M。相比于之前的HashShuffle减少了小文件的个数。

Spark 内存管理:

  1. 堆内内存:程序在运行时动态地申请某个大小的内存空间,JVM管理。Executor内。

  2. 堆外内存:直接向操作系统进行申请的内存,不受JVM控制。 Executor外。

    说明:

    堆外内存是序列化的,其占用的内存大小可直接计算。堆内内存是非序列化的对象,其占用的内存是通过周期性地采样近似估算而得,并不是每次新增的数据项都会计算一次占用的内存大小,这种方法降低了时间开销但是有可能误差较大,导致某一时刻的实际内存有可能远远超出预期。此外,在被Spark标记为释放的对象实例,很有可能在实际上并没有被JVM回收,导致实际可用的内存小于Spark记录的可用内存。所以Spark并不能准确记录实际可用的堆内内存,从而也就无法完全避免内存溢出OOM的异常。

    • 堆外内存相比于堆内内存有几个优势:
      1. 减少了垃圾回收工作,垃圾回收会暂停其他的工作。
      2. 加快了复制的速度,因为堆内在Flush到远程是,会先序列化然后再发送,而堆外内存本身就是序列化的,相当于胜率了这个过程。
    • 堆外内存相比于堆内内存有几个缺点:
      1. 堆外内存难以控制,如果内存泄漏难以排查。
      2. 堆外内存不适合存储复杂对象,比较适合存储对象或扁平化。
  3. 堆内内存空间分配:

    • 堆内内存包括:存储内存(Storage)、执行内存(Execution)、其他内存。

    • 静态内存管理:

      1.Spark最初采用的静态内存管理,存储内存、执行内存和其他内存大小在程序运行期间均为固定值,但是用户可以在应用程序启动前进行配置。
      2.Storage内存和Execution内存都有预留空间,目的是防止OOM,因为Spark堆内内存大小的记录是不准确的,需要留出保险区域。堆外的空间分配只有存储内存和执行内存,由于堆外内存占用的空间可以被精确计算,所以无需再设定保险区域。

    • 统一内存管理:

      1.Spark1.6 之后引入的统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域。双方空间都不足时,则存储到磁盘,若己方空间不醉而对方空间有空余时,可以借对方的空间,存储空间不足指不能放下一个完整的Block。
      2.执行内存的空间被占用时,可让对方将占用的数据转存至磁盘,然后归还空间。
      3.当存储内存被对方占用时,无法让对方归还,只能等待对方释放内存。因为需要考虑到Shuffle过程的很多因素,实现起来比较困难。

      说明:统一内存管理在一定晨读是提高了堆内和堆外内存资源的利用率,降低了开发者维护的难度。

posted @ 2021-05-31 16:43  yuexiuping  阅读(167)  评论(0编辑  收藏  举报