spark内核揭秘-spark资源调度系统个人理解

开发Spark应用程序的大致流程

基于Spark写Application->资源调度器->任务调度器->分布式并行计算

此次我们只讨论资源调度:

资源调度的原理

以下是Standalone集群的一个申请资源的过程:
这个是cluster方式提交,当以client方式提交时,没有waitingDriver集合,因为他在client节点开启的。
在这里插入图片描述

  1. Mater节点运行start-all.sh命令后开启集群,它会通过扫描slaves配置文件中的每一个IP,通过ssh协议登陆它们,开启Worker进程,当Worker开启成功后,他们会通过spark-env.sh中Master的IP和端口发送成功启动的信息.
  2. 当开启集群的时候,Worker会向Master发送他们各自的资源信息,Master把他们的信息全部封装到一个workers = new HashSetWorkerInfo,还会存放他们的IP和端口。
  3. 客户端client以cluster方式提交一个Application的话,spark-submit --master spark://node01:7077 --deploy-mode cluster …,首先client节点会启动一个spark-submit进程,它为Driver向Master申请资源,Master中的waitingDrivers = new ArrayBuffer[DriverInfo]对象会存放为Driver申请资源的信息。找到符合要求的节点,启动Driver。
  4. Driver正常启动后,spark-submit进程被杀掉,Driver开始向Master申请Application的资源,Master中的waitingApps = new ArrayBuffer[ApplicationInfo]来存放为Application申请资源的信息
  5. Executor进程开启

waitingDrivers集合

当waitingDrivers集合中元素不为空,说明有用户单向Master申请资源了,此时应该查看当前集群的资源情况(产看一下works集合),找到符合要求的节点,启动Driver,当Driver正常启动,这个申请资源的信息从waitingDrivers中删除掉。

waitingApps集合

当waitingApps集合不为空,说明有Driver向Master为当前的Application申请资源,查看集群的资源情况(Workers集合)找到合适的Worker节点,开启Executor进程;默认情况下,每一个Worker为当前的Application只是启动一个Executor进程。这个Executor会使用1G内存和这个Worker所管理的所有的core。

对waitingApps、waitingDrivers集合的监控

因为他们这两个集合一旦发生变化,就证明有人向Master申请了资源,Master就必须得知道具体情况,所以需要时时监控他们。

所以Master里有一个schedule()方法,每当这两个集合中添加元素的时候,就会反调这个方法,这个方法里有2套逻辑,分别对应这两个集合,当某个集合反调这个函数时,它会按照上述处理过程来处理。

资源调度的结论

粗资源调度和细资源调度

粗粒度的资源调度

描述:在任务执行之前,会先将资源申请完毕,当所有的task执行完毕,才会释放资源(代表:Spark)

优点:task启动时间变快,task执行时间缩短
缺点:因为所有task执行完毕才释放资源,所以集群的资源无法充分利用

细粒度的资源调度

描述:在任务提交的时候,每一个task自己去申请资源,申请到了才会执行,执行完立马释放资源

优点:充分利用集群资源
缺点:task启动时间变长,导致stage、job、application时间变长

Executor的默认机制

  1. 默认情况下,每一个Worker为当前的Application只会启动一个Executor(他默认使用当前Worker所能管理的所有的核,1G内存)
  2. 如果想要在一个Worker上启动多个Executor进程,再提交Application的时候,要指定executor使用的核数;spark-submit --executor-cores
  3. 默认情况下,Executor的启动,是轮训方式启动的,轮训的启动方式在一定的程度上有利于数据的本地化

为什么轮训的方式比阻塞的方式好?

轮训方式在一定的程度上有利于数据的本地化,若全开在一台节点上,此时计算的数据有可能不在此节点上,此时数据要走网络传输,这就导致数据找计算。

轮训方式启动Executor的公式

–executor-cores:每个executor需要的核;ec
–executor-memory:每个executor需要内存;em
–total-executor-cores:这个Application一共需要多少个核;tec
worker num:Worker节点个数;wn
worker memory:Worker节点的内存;wm
worker core:Worker节点的核;wc

min(min(wm/em,wc/ec)*wn,tec/ec)

Spark运行在yarn集群上的2种提交方式

          首先spark跑在yarn集群上,不必开启,也必须不能开启Stand alone集群,其他节点不必需要spark包,必须开启HDFS,因为包要被上传到HDFS中。

client

spark在Stand alone集群上启动,提交任务的命令和运行在yarn集群上提交任务的命令有所不同

  1. Stand alone:spark-submit --master spark://node01:7077
  2. yarn:spark-submit --master yarn-client… (需要在spark-env.sh中加入HADOOP_CNF_DIR:Hadoop配置文件的目录,用来寻找yarn集群)

执行原理图

在这里插入图片描述
如图所示,Driver进程并不与yarn集群的ApplicationMaster冲突

执行流程

  1. 客户端提交一个Application,在客户端启动一个Driver进程
  2. Driver进程会向RS(ResourceManager)发送请求,启动AM(ApplicationMaster)的资源。
  3. RS收到请求,随机选择一台NM(NodeManager)启动AM。这里的NM相当于Standalone中的Worker节点。
  4. AM启动后,会向RS请求一批container资源,用于启动Executor
  5. RS会找到一批NM返回给AM,用于启动Executor。
  6. AM会向NM发送命令启动Executor。
  7. Executor启动后,会反向注册给Driver,Driver发送task到Executor,执行情况和结果返回给Driver端。

cluster

  1. 提交命令
    ./spark-submit --master yarn --deploy-mode cluster --class org.apache.spark.examples.SparkPi …/lib/spark-examples-1.6.0-hadoop2.6.0.jar 100

    ./spark-submit --master yarn-cluster --class org.apache.spark.examples.SparkPi …/lib/spark-examples-1.6.0-hadoop2.6.0.jar 100

执行原理

在这里插入图片描述

执行流程

  1. 客户机提交Application应用程序,发送请求到RS(ResourceManager),请求启动AM(ApplicationMaster)。
  2. RS收到请求后随机在一台NM(NodeManager)上启动AM(相当于Driver端)。
  3. AM启动,AM发送请求到RS,请求一批container用于启动Executor。
  4. RS返回一批NM节点给AM。
  5. AM连接到NM,发送请求到NM启动Executor。
  6. Executor反向注册到AM所在的节点的Driver。Driver发送task到Executor。
    总结

资源调度的源码分析

Master节点三个集合的结构查看

val workers = new HashSet[WorkerInfo]   // 存储每一个Worker节点的基本信息
val waitingApps = new ArrayBuffer[ApplicationInfo]
private val waitingDrivers = new ArrayBuffer[DriverInfo]

在这里插入图片描述

  1. WorkerInfo
    - host:Worker所在的节点
    - port:端口号
    - cores:worker所有的核数
    - memory:它所有的内存
    - endpoint:spark内部通信属性,类似于邮箱
    - webUiAddress:外部UI的地址,默认端口8081
  2. DriverInfo
    - StartTime:启动时间
    - id:id号
    - desc:Driver的资源描述信息
    1. jarUrl
    2. mem:Worker用的内存(–driver-memory设定)
    3. cores:Worker用的核(–driver-cores设定)
    4. supervise
    5. command
    - submitDate:提交时间
  3. ApplicationInfo
    - startTime:开启时间
    - id:id号
    - desc:App的使用资源信息
    1. name
    2. maxCores:最大核数(–total-executor-cores );默认Int.MAXVALUE
    3. memoryPerExecutorMB:每一个Executor使用的内存数(–executor-memory)
    4. coresPerExecutor:每一个Executor使用的核数(–executor-cores)
    - submitDate:提交时间
    - driver:
    - defaultCores:默认使用核数

schedule()方法解析

  /**
   * 调度当前可用的资源给waiting apps
   * 当有一个新的app加入,或者资源变化的时候此函数被调用
   */
 private def schedule(): Unit = {
    /**
      * state是当前Master的状态,在web ui可以查看
      * 它的状态有:
      *    STANDBY, ALIVE, RECOVERING, COMPLETING_RECOVERY 
      * 备用Master切换为Alive状态的过程为:STANDBY- RECOVERING-COMPLETING_RECOVERY-ALIVE
      */
    if (state != RecoveryState.ALIVE) {
      return
    }
    // 此shuffle是将正在工作的Worker集合打散,以便于随便拿取一个Worker节点来开
    启Driver进程
    val shuffledAliveWorkers = Random.shuffle(workers.toSeq.filter(_.state == WorkerState.ALIVE))
    // 可用的work个数
    val numWorkersAlive = shuffledAliveWorkers.size
    var curPos = 0

    /**
      * 遍历每一个为Driver申请资源的请求(waitingDrivers),通过遍历打乱顺序的
      * Workers(保证随机取一台开启Driver进程)
      */
    for (driver <- waitingDrivers.toList) { 
      var launched = false
      var numWorkersVisited = 0
      while (numWorkersVisited < numWorkersAlive && !launched) {
      // 从打乱顺序后的Workers集合中拿到位置为curPos的Worker
        val worker = shuffledAliveWorkers(curPos)
        // 遍历的索引
        numWorkersVisited += 1
        // 查看该Worker剩余的内存是否大于Driver进程所需要的内存并且剩余的核数是否
        // 大于Driver进程所需要的核数
        if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
        // 启动Driver,把当前的Worker信息核Driver信息传入,在这个Worker上开启此
        // Driver进程
          launchDriver(worker, driver)
        // 对于这个请求启动Driver的请求,从waitingDrivers集合中删除,因为已经启动了  
          waitingDrivers -= driver
          // 已经启动成功后,置为true,下次不会循环,若此次没有开启,则不会执行,
          // 他还是false,还会进行下一次循环
          launched = true
        }
        // 遍历下一个Worker
        curPos = (curPos + 1) % numWorkersAlive
      }
    }
    // 对于application来说executor是资源,为application分配资源,实质上就是启动
    // executor
    // schelduer方法中开启了Driver之后便是开启Executor
    startExecutorsOnWorkers()      
  }

launchDriver()方法启动Driver分析

/**
 * 打印日志,并把Driver进程发送
 * 把Driver的状态置为running
 * /
private def launchDriver(worker: WorkerInfo, driver: DriverInfo) {
    logInfo("Launching driver " + driver.id + " on worker " + worker.id)
    worker.addDriver(driver)
    driver.worker = Some(worker)
    worker.endpoint.send(LaunchDriver(driver.id, driver.desc))
    driver.state = DriverState.RUNNING
  }

startExecutorsOnWorkers()启动Executor分析

 private def startExecutorsOnWorkers(): Unit = {
    // Right now this is a very simple FIFO scheduler. We keep trying to fit in the first app
    // in the queue, then the second app, etc.
    for (app <- waitingApps) {
      /**
        * 从APP请求对象中拿到每一个Executor需要的核数
        * 若没有使用--executor-cores指定
        * coresPerExecutor的默认值是1
        */
      val coresPerExecutor = app.desc.coresPerExecutor.getOrElse(1)

      /**
        * app.coresLeft = requestedCores - coresGranted
        * requestedCores:总共需要的核数(--total-executor-cores指定,默认Int的最大值)
        * coresGranted:已经分配的值
        */
      if (app.coresLeft >= coresPerExecutor) {
        // 对Worker集合进程双层过滤,1.根据状态  2.根据资源情况来过滤
        val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
          .filter(worker => worker.memoryFree >= app.desc.memoryPerExecutorMB &&
            worker.coresFree >= coresPerExecutor)
          .sortBy(_.coresFree).reverse
        /**
          * 调度每一个Worker节点上上启动Executor的方案
          * 把当前请求对象,可用的Worker集合,preadOutApps:executor启动模式默认false,即轮训
          * 传递过去
          */
        val assignedCores = scheduleExecutorsOnWorkers(app, usableWorkers, spreadOutApps)

        // Now that we've decided how many cores to allocate on each worker, let's allocate them
        for (pos <- 0 until usableWorkers.length if assignedCores(pos) > 0) {
          allocateWorkerResourceToExecutors(
            app, assignedCores(pos), app.desc.coresPerExecutor, usableWorkers(pos))
        }
      }
    }
  }

启动Executor的方案scheduleExecutorsOnWorkers()方法解析

private def scheduleExecutorsOnWorkers(
      app: ApplicationInfo,
      usableWorkers: Array[WorkerInfo],
      spreadOutApps: Boolean): Array[Int] = {
    // 每一个Executor所需要的核数,若没有设置则为null
    val coresPerExecutor = app.desc.coresPerExecutor
    // 加入为空,给他设置默认最小值1
    val minCoresPerExecutor = coresPerExecutor.getOrElse(1)
    val oneExecutorPerWorker = coresPerExecutor.isEmpty
    // 每一个Executor所需要的内存
    val memoryPerExecutor = app.desc.memoryPerExecutorMB
    // 可用的Worker个数
    val numUsable = usableWorkers.length
    val assignedCores = new Array[Int](numUsable) // 每一个Worker可以贡献的核
    val assignedExecutors = new Array[Int](numUsable) // 每一个Worker启动的executor的个数
    // 计算所有可用worker的可用核数
    var coresToAssign = math.min(app.coresLeft, usableWorkers.map(_.coresFree).sum)

    /** Return whether the specified worker can launch an executor for this app. */
    def canLaunchExecutor(pos: Int): Boolean = {
      val keepScheduling = coresToAssign >= minCoresPerExecutor
      val enoughCores = usableWorkers(pos).coresFree - assignedCores(pos) >= minCoresPerExecutor

      // If we allow multiple executors per worker, then we can always launch new executors.
      // Otherwise, if there is already an executor on this worker, just give it more cores.
      val launchingNewExecutor = !oneExecutorPerWorker || assignedExecutors(pos) == 0
      if (launchingNewExecutor) {
        val assignedMemory = assignedExecutors(pos) * memoryPerExecutor
        val enoughMemory = usableWorkers(pos).memoryFree - assignedMemory >= memoryPerExecutor
        val underLimit = assignedExecutors.sum + app.executors.size < app.executorLimit
        keepScheduling && enoughCores && enoughMemory && underLimit
      } else {
        // We're adding cores to an existing executor, so no need
        // to check memory and executor limits
        keepScheduling && enoughCores
      }
    }

    // Keep launching executors until no more workers can accommodate any
    // more executors, or if we have reached this application's limits
    var freeWorkers = (0 until numUsable).filter(canLaunchExecutor)
    while (freeWorkers.nonEmpty) {
      freeWorkers.foreach { pos =>
        var keepScheduling = true
        while (keepScheduling && canLaunchExecutor(pos)) {
          coresToAssign -= minCoresPerExecutor
          assignedCores(pos) += minCoresPerExecutor

          // If we are launching one executor per worker, then every iteration assigns 1 core
          // to the executor. Otherwise, every iteration assigns cores to a new executor.
          if (oneExecutorPerWorker) {
            assignedExecutors(pos) = 1
          } else {
            assignedExecutors(pos) += 1
          }

          // Spreading out an application means spreading out its executors across as
          // many workers as possible. If we are not spreading out, then we should keep
          // scheduling executors on this worker until we use all of its resources.
          // Otherwise, just move on to the next worker.
          if (spreadOutApps) {
            keepScheduling = false
          }
        }
      }
      freeWorkers = freeWorkers.filter(canLaunchExecutor)
    }
    assignedCores
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值