spark 调度分析(四)

1. 概述

在这里插入图片描述
job - stage -TaskSet - Task.
spark调度中,最重要的就是DAGScheduler和TaskScheduler调度器:

  1. DAGScheduler负责任务的逻辑调度。
  2. TaskScheduler负责具体任务的调度执行。
    根据图1从整体上对Spark的作业和任务调度做了分析
  3. spark 的APP 进行各种转换操作,通过行动操作触发作业运行。根据RDD之间的DAG图,提交给DAGScheduler进行解析,形成调度阶段。
  4. DAGScheduler 将DAG拆分成相互依赖的调度阶段。拆分依据是以RDD的依赖是否为宽依赖,遇到宽依赖则划分为新的调度阶段(stage)。划分出来的每个stage包含一个或者多个任务,这些任务形成任务集TaskSet,这些TaskSet将提交给TaskScheduler进行调度运行。DAGScheduler 将记录哪些RDD会存入磁盘。如果在某个调度阶段失败,则需要重新提交搞调度任务。
  5. 每个TaskScheduler 为一个SparkContext实例服务,将接受到的任务集(TaskSet)以任务的形式分发到Worker中的Executor中运行。如果失败则需要重试,超时也重试。
  6. Worker中的Executor收到TaskScheduler发送过来的任务后,以多线程的方式运行,一个线程负责一个Task。任务结束需要返回结果给TaskScheduler。根据不同任务,返回不同形式的对象。

2. 各个阶段

2.1 提交作业(Job)

  1. 作业的提交是从行动操作开始的,比如count。由SparkCount的runJob来提交作业,
  2. 将图提交给DAGScheduler,从行动操作开始根据血缘关系反推,形成一个DAG。
    在spark 应用程序中,会拆分多个作业,对于多个作业之间的调度,Spark目前提供了两种调度策略,一种是FIFO的模式(默认),另一种是FAIR模式。

2.2 划分调度(DAGScheduler)

  1. DAGScheduler会从最后一个RDD触发,使用广度优先遍历整个依赖树,根据RDD是否为宽依赖(shuffle),从而划分调度阶段。
  2. 遍历过程是从最后的ResultStage。使用getParentStages找出依赖的RDD,并判断是否存在shuffle操作。
    在这里插入图片描述

举例:

  1. 在SparkContext中提交运行时,会调用DAGScheduler的handleJobSubmitted进行处理,先找到最后一个RDD(RDDG),调用getParentStages方法。
  2. 在getParentStages方法中会发现join操作是shuffle操作,则获取RDDB和RDDF。
  3. 使用getAncestorShuffleDependencies方法从RDDB向前遍历,发现无祖先RDD,生成stage0.
  4. 使用getAncestorShuffleDependencies方法从RDDF向前遍历,发现groupby,以此划分shuffleMapStage1和shuffleMapStage2.
  5. 最后生成ResultStage3.在该划分中一共有四个阶段,3个shuffleMapStage,一个ReusltStage。

2.3 提交划分调度阶段

  1. 在DAGScheduler的handleJobSubmitted,生成一个finalStage,这个finalStage一般为ResultStage。通过获取finalStage的父类调度阶段。
  2. 如果没有父类stage,则执行submitMissingTasks方法提交执行,否则将该stage方式waitingStages,并调用查找父类stage。
  3. 当入口的stage运行完成后,相继提交后续stage,需要先判断父类stage是否运行成功。如果成功则提交该stage,否则重复提交父类stage。

在这里插入图片描述
举例:

  1. 获取最后一个Resultstage3,通过submitStage进行提交。
  2. 在submitStage先创建作业实例(Job),然后判断该调度阶段是否存在父类stage,因为ResultStage3有两个父类stage,ShuffleMapStage0和ShuffleMapStage2,因此并不立即提交当前stage,把Resultstage3放入waitingStage队列中。
  3. 递归判断ShuffleMapStage0和ShuffleMapStage2是否有父类stage,同理将ShuffleMapStage2放入waitingStage队列中,而ShuffleMapStage0和ShuffleMapStage1两个作为第一次调度使用submitMissingTasks方法提交运行。
  4. 当Executor任务执行完成时,发送消息,DAGScheduler更新状态,如果失败,则重新提交任务,否则从watingStage中获取ShuffleMapStage2提交任务。
  5. 当ShuffleMapStage2完成后,提交Resultstage3,至此全部完成。

2.4 提交任务(Task)

  1. 当DAGScheduler提交运行后,在DAGScheduler中会根据stage中Partition的个数拆分对应个数的任务(TaskSet)。

  2. 这些任务组成TaskSet提交到TaskScheduler处理。ResultStage生成ResultTask,对于ShuffleMapStage生成ShuffleMapTask。

  3. 每个TaskSet包含了对应stage所有任务,处理逻辑完全相同只是数据不同而已(因为是分区)。

  4. TaskScheduler接收到TaskSet时,会构建一个TaskSetManager的实例,用于管理这个TaskSet的生命周期,而该TaskSetMananger会放入系统的调度池中,根据调度算法进行调度。

  5. 获取集群中可用的Executor,对TaskSet中的Task分配资源,并提交运行。

  6. 资源分配:在分配过程中会根据调度策略对TaskSetMananger进行排序,然后依次按照就近原则分配资源。

    资源分配过程:
    a) 获取可用Executor
    b) 为任务随机分配Executor,避免将Task集中分配到Worker上。
    c) 存储分配好的资源任务。
    d) 获取调度排序好的TaskSetMananger
    e) 为分配好的TaskSetMananger列表进行资源分配,就近原则。
    
  7. 分配好资源的任务提交到launchTasks中,该方法会把任务一个一个发送到Worker节点,再通过其内部的Executor来执行任务。

举例 :在这里插入图片描述

  1. 在提交stage阶段,第一次提交ShuffleMapStage0和ShuffleMapStage1,这两个stage在DAGScheduler中会根据Partition的个数拆分任务,如图所示,分别为TaskSet0和TaskSet1。
  2. TaskScheduler将接受到的TaskSet0和TaskSet1,分别构建TaskSetMananger0和TaskSetManager1,并将这两个TaskSetManager放到系统的调度池中,根据调度算法进行调度。
  3. 在TaskSchedulerImpl的resourceOffers方法中按照就近原则进行资源分配,通过LaunchTasks把任务发送到Worker上调用Executor来执行任务。
  4. ShuffleMapStage0和ShuffleMapStage1执行完毕后,ShuffleMapStage2会根据上述步骤继续执行,是不过不同的ResultStage3生成的任务类型是ResultTask。

2.5 执行任务(Excutor Task)

  1. Executor接收到LaunchTask消息时,会调用Executor的launchTask方法进行处理。
  2. launchTask初始化一个TaskRunner来封装任务,用于管理任务运行时的细节,再把TaskRunner对象放入ThreadPool(线程池)中去执行。
    在TaskRunner的run方法里:
  3. 生成内存管理taskMemoryMananger实例,用于内存管理。
  4. 执行任务开始会将Driver终端点发送任务运行开始消息(让Driver可以监控任务运行状态)。
  5. 首先会对Task本身和所依赖的Jar等文件反序列化,然后调用Task的runTask方法。
  6. Task本身是抽象类,具体的runTask方法是它的两个子类ShuffleMapTask和ResultTask来实现。

ShuffleMapTask:
    ShuffleMapTask会将结果写到BlockManager中,最终会返回DAGScheduler一个MapStatus对象。该对象中存储了ShuffleMapTask的运算结果存储到BlockManager里的相关存储信息(元信息),而不是计算结果本身。

ResultTask:
    会最终返回func函数的计算结果。

2.6 获取计算结果

对于Executor的计算结果,会根据结果大小的不同有不同的策略:

  1. 生成结果大小在(1GB,无穷大),会直接丢弃该结果,可以通过spark.driver.maxResultSize设置。
  2. 如果在(128MB-200KB,1GB):如果结果在该区间,则以taskId为编号存如到BlockManager中,并把该结果通过Netty发送给Driver端。该阈值是Netty框架传入的最大值和Netty预留空间的的差值。
  3. 生成结果的大小在(0,128MB-200KB):通过Netty直接发送到Driver终端点。

任务执行完毕后:
    任务执行完毕后,TaskRunner将任务的执行结果发送给Driver端。在终端点会转给TaskSheduler的statusUpdate方法进行处理,在该方法中对于不同的任务状态有不同的处理结果。

  1. TaskState.FINISHED,直接按照enqueueSuccessfulTask进行处理。
  2. TaskState.FAILED,TaskState.KILLED,TaskState.LOST,则按照enqueueFailedTask进行处理。

    对于ShuffleMapTask,它需要将结果通过某种机制告诉下游stage,以便其可以作为后续stage的输入。

  1. ShuffleMapTask的结果是MapStatus;
  2. 序列化后存储DirectTaskResult和IndirectTaskResult中。
  3. DAGScheduler把结果存储到MapOutputTrackerMaster中,完成ShuffleMapTask。

    对于ResultTask
只是判断是否完成,如果完成则标记完成,清除资源。

2.7 调度算法

2.7.1 应用程序之间(Application)

  1. app会尽可能分散在集群的worker节点上。
  2. 先分配的app会尽可能多的获取满足的资源,后分配的app只能再剩余的资源中再次筛选(FIFO)
  3. 如果没有何时的资源的则等待其他app程序释放。
    分配app的资源会根据worker的分配资源进行调整,有两种方式:
    1)把app运行在尽可能多的worker上,在内存充足时,充分利用集群资源。
    2)把app运行在尽可能少的worker上,适用于CPU密集型而内存较少使用的场景。

一般为第一种:具体实现:

  1. 对worker进行随机排序,可以分布的更均匀
  2. 按照顺序在集群中启动Driver,Driver尽可能在不同的Worker节点上运行。
  3. 根据参数默认在一个Worker只启动一个Executor,并尽可能多地分配资源。
    能否启动Executor满足的条件为:
    a)应用程序需要分配cpu核数>=每个Executor所需的最少cpu核数。
    b)是否有足够的cpu核数,判断条件为:可用核数-已用核数>=需要核数
    当一个worker可以启动多个Executor时,需要追加两个条件:
    a)worker可用内存-已分配内存>=Executor所需内存。
    b)已有Executor数量+已运行Executor数量 < Executor设置的最大数量。

2.7.2 作业和调度阶段之间(job & stage)

    DAGScheduler进行划分和调度阶段有两种策略:FIFO(默认)和FAIR阶段。其中FAIR阶段可以由两个参数来设置job执行的优先模式,两个参数分别是minShare(最小任务数)和weight(任务的权重)。

FIFO:

  1. 创建FIFO调度池
  2. 加入调度内容:将stage拆分成TaskSet,再将TaskSet交给管理器TaskSMananger进行管理,最后把TaTaskSMananger放入调度池中。
  3. 提供已排序的TaskSetManager(任务集管理器)
    调度池中包含了多个作业的TaskSetManager:
    a)先比较作业的优先级(作业编号越小,优先级越高)
    b)同一个作业(stage编号越小,优先级越高)

FAIR:

  1. 创建FAIR调度池
  2. 调度池有下级调度池,下级调度池中包含一组TaskSetManager
  3. 提供已排序的TaskSetManager(任务集管理器)
    FAIR中包含两层调度:
    a) 根调度池(rootPool)包含了多个下级调度池
    b)下级调度池包含多个TaskSetManager。
  4. 先获取两个stage的饥饿程度,饥饿程度为正在运行的任务数是否小于最小任务,如果是则表示该调度处理饥饿程度。
    a)饥饿和非饥饿,先满足饥饿
    b)饥饿和饥饿,先满足资源占比小的调度(stage)。
    c)非饥饿和非饥饿,则比较参数权重,满足权重小的stage

2.7.3 任务之间(Task)

数据本地性和延迟执行两个概念。
1、数据本地性
数据的计算尽可能在数据所在的节点上进行,这样可以减少数据在网络上传输,移动计算好过移动数据,而且数据如果在内存中,则可以减少磁盘IO。优先级为:
process_local > node_local > no_pref > rack_local > any
最好是内存中存储数据、次好是同一个节点,再次是同机架,最后是任意位置。
数据本地性通过以下情况下确定:

  • a) 任务处于作业开始的调度阶段,这些任务对应的RDD分区都有首选运行位置,该位置也是任务运行的首选位置,级别为node_local
  • b)任务处于非作业开头阶段,可以根据父调度阶段运行的位置得到任务的首选位置,如果父类的Executor处于活动状态,则数据本地性为process_local,如果不处于活跃状态,则则数据本地性为node_local。
  • c)没有首选位置,则本地性为node_pref。

2、延迟执行
Task分配时,会先判断最佳运行节点是否空间,如果该节点没有足够的资源,会等待一定的是时间,如果在该时间内释放出足够的资源,则运行,否则会找出次佳的节点运行。一般老师只会在processs_local和node_local两个数据本地性进行等待,默认时间为3s。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值