spark(二)-Master的资源调度

Master在收到RegisterWorker/RegisterApplication/ExecutorStateChanged/RequestSubmitDriver消息时,
或者完成主备切换后,都要执行schedule(),来

调度waiting apps中当前可用的资源(Driver,Executor on Workers),
每当加入一个新的app,或者资源可用性改变时都会被调用。

  private def schedule(): Unit = {
    // Standby master是不会参与Application资源的调度的
    if (state != RecoveryState.ALIVE) {
      return
    }
    // Drivers take strict precedence over executors
    // workers 是registerWorker()注册过来的所有worker,类型是HashSet[WorkerInfo]
    // 先对workers随机乱序
    val shuffledAliveWorkers = Random.shuffle(workers.toSeq.filter(_.state == WorkerState.ALIVE))
    val numWorkersAlive = shuffledAliveWorkers.size
    var curPos = 0
    for (driver <- waitingDrivers.toList) { // iterate over a copy of waitingDrivers
      // We assign workers to each waiting driver in a round-robin fashion. For each driver, we
      // start from the last worker that was assigned a driver, and continue onwards until we have
      // explored all alive workers.
      var launched = false
      var numWorkersVisited = 0
      // 遍历所有Alive状态的workers
      while (numWorkersVisited < numWorkersAlive && !launched) {
        val worker = shuffledAliveWorkers(curPos)
        numWorkersVisited += 1
        // 如果当前worker上的空闲内存和空闲CPU cores能够满足driver,就launch它,并从waitingDrivers中移除
        if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
          launchDriver(worker, driver)
          waitingDrivers -= driver
          launched = true
        }
        curPos = (curPos + 1) % numWorkersAlive
      }
    }
    // 启动Driver后,启动Worker上的所有Executors
    startExecutorsOnWorkers()
  }

standalone和yarn-client模式都会在本地启动Driver,不会注册给Master,
只有yarn-cluster模式才会注册进来,才需要调度Drivers。

启动Driver严格优先于Executors。

启动Driver过程

  • 1, 将driver加入对应的worker,worker加入到driver内存缓存,互相引用;
  • 2, 调用worker的endpoint发送LaunchDriver消息,让Worker来启动Driver
  • 3, 更新driver的状态为RUNNING

Worker收到LaunchDriver消息后,new 一个 DriverRunner()并启动,累计占用的cpu和memory。

DriverRunner在org.apache.spark.deploy.worker下。它实际上new Thread并启动,
  先Kill掉之前的driver,把Kill()添加给ShutdownHook;
  然后准备driver的jars开始运行;
  运行后发送DriverStateChanged消息通知driver运行的结果状态。

driver运行时,创建一个CommandUtils.buildProcessBuilder(),运行的其实是这个builder

    val builder = CommandUtils.buildProcessBuilder(driverDesc.command, securityManager,
      driverDesc.mem, sparkHome.getAbsolutePath, substituteVariables)

然后做一下stdout/stderr重定向,
最后runCommandWithRetry()。

startExecutorsOnWorkers()

调度和启动Worker上的所有Executors

spark2.4这里,Application的调度放在了 startExecutorsOnWorkers()里面,使用简单的FIFO调度算法。

  • 遍历waitingApps中的每个app,
  • 如果app还需要的cores小于一个Executor指定的cpu数,就不为app的coresLeft执行调度,不分配新的executor-core。
  • 过滤出还有可供调度的cpu和memory的workers,按剩余cores的大小降序排序,作为usableWorkers
  • 计算所有usableWorkers上要分配多少CPU(用scheduleExecutorsOnWorkers函数)
  • 遍历可用的 Workers,分配资源并执行调度,启动Executors(用allocateWorkerResourceToExecutors函数)。

这个版本的scheduleExecutorsOnWorkers()和allocateWorkerResourceToExecutors()似乎名字取反了,
schedule…()其实是资源分配的计算,allocate才是调度和执行。

app.coresLeft 定义在ApplicationInfo.scala:
private[master] def coresLeft: Int = requestedCores - coresGranted

  private def startExecutorsOnWorkers(): Unit = {
    // Right now this is a very simple FIFO scheduler. 
    for (app <- waitingApps) {
      val coresPerExecutor = app.desc.coresPerExecutor.getOrElse(1)
      // If the cores left is less than the coresPerExecutor,the cores left will not be allocated
      if (app.coresLeft >= coresPerExecutor) {
        // Filter out workers that don't have enough resources to launch an executor
        val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
          .filter(worker => worker.memoryFree >= app.desc.memoryPerExecutorMB &&
            worker.coresFree >= coresPerExecutor)
          .sortBy(_.coresFree).reverse
        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))
        }
      }
    }
  }

scheduleExecutorsOnWorkers()

计算如何调度worker上的Executors

scheduleExecutorsOnWorkers()返回值是assignedCores数组,它记录了给每个worker分配的cpu数

内部函数 canLaunchExecutor() 用来判断当前Worker是否可以启动一个新的Executor,分配新的cpu资源。

assignedExecutors数组记录了将要在每个Worker上新启动的Executor个数,默认没指定coresPerExecutor时,每个worker上只启动一个executor。

  • 如果是每个Worker下面只能够为当前的应用程序分配一个Executor的话,每次是分配一个Core!
  • 如果是spreadOutApps(默认的情况下)的时候,会尽量使用集群中所有的executors. 每次都会给executor增加一个core。
  • 如果不是spreadOutApps的时候,每次都会给executor增加一个core,会一直循环当前程序的executor上的freeCores,所以会占用本机器上的尽可能多的cores。
    参考:https://blog.csdn.net/snail_gesture/article/details/50808239
  private def scheduleExecutorsOnWorkers(
      app: ApplicationInfo,
      usableWorkers: Array[WorkerInfo],
      spreadOutApps: Boolean): Array[Int] = {
    val coresPerExecutor = app.desc.coresPerExecutor
    // 默认情况下一个executor分配一个core
    val minCoresPerExecutor = coresPerExecutor.getOrElse(1)
    val oneExecutorPerWorker = coresPerExecutor.isEmpty
    val memoryPerExecutor = app.desc.memoryPerExecutorMB
    val numUsable = usableWorkers.length
    val assignedCores = new Array[Int](numUsable) // Number of cores to give to each worker
    val assignedExecutors = new Array[Int](numUsable) // Number of new executors on each worker
    
    // 还要分配的CPU总数,取min(app需要的cpu数量, 所有可用Workers剩余cpu的和)
    // Workers可用的cores不能满足app需要的话,先把可用的分配上。
    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

      // 如果允许每个worker有多个Executors(不指定coresPerExecutor),那么可以一直启动新的executor。
      // 否则,如果这个worker上已经存在Executor, 就给这个Executor分配更多的core。
      val launchingNewExecutor = !oneExecutorPerWorker || assignedExecutors(pos) == 0
      if (launchingNewExecutor) {
        // assignedMemory:当前worker要分配的Executors * 每个Executor的Memory
        val assignedMemory = assignedExecutors(pos) * memoryPerExecutor
        // 当前worker空闲memory - 当前worker要分配的Executors总的Memory >= 一个executor的内存
        // 意思是:还有足够的内存可以调起至少一个Executor
        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
    // 不断启动executor,直到不再有Worker可以容纳更多Executor,或者达到了这个Application的要求
    var freeWorkers = (0 until numUsable).filter(canLaunchExecutor)
    while (freeWorkers.nonEmpty) {
      freeWorkers.foreach { pos =>
        var keepScheduling = true
        // 默认spreadOut方式,keepScheduling为false,每个pos只循环一次,每次分配minCoresPerExecutor个core。
        while (keepScheduling && canLaunchExecutor(pos)) {
          // 每次迭代为当前worker增加minCoresPerExecutor个core。
          // 如果在每个worker上只启动一个executor(没有指定spark.executor.cores的情况下),
          // minCoresPerExecutor的值为1,否则就一次分配指定的coresPerExecutor
          coresToAssign -= minCoresPerExecutor
          assignedCores(pos) += minCoresPerExecutor
          
          // 如果我们在每个worker上只启动一个executor,(没有指定spark.executor.cores的情况),那么assignedExecutors=1;
          // 否则,每次迭代都在worker上再启动一个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
  }

canLaunchExecutor()主要根据keepScheduling和enoughCores来判断是否为将启动的Executor分配CPU。
keepScheduling是由coresToAssign(app尚未分配的cores) 和 minCoresPerExecutor共同决定的。

spreadOut
scheduleExecutorsOnWorkers在计算资源分配的时候传入了spreadOutApps参数,它
根据配置(spark.deploy.spreadOut)决定是否用spreadOut方式的Apps调度,也就是分配计算资源。
默认的spreadOut为true,会把每个app的所有Executor任务展开,平均分布到所有可用的结点上(Round-Robin),
而不是一个结点不够用时再分布到下一个结点上。

spreadOutApps为true时,每分配一个minCoresPerExecutor,就移动指针到下一个worker,
否则只要当前worker满足条件,就一直累计assignedCores,直到一个worker没有足够资源时再移动指针。


allocateWorkerResourceToExecutors()

遍历所有usableWorkers依次执行allocateWorkerResourceToExecutors(),来分配cores,调度当前App。
  for (pos <- 0 until usableWorkers.length if assignedCores(pos) > 0) {
  allocateWorkerResourceToExecutors(
  app, assignedCores(pos), app.desc.coresPerExecutor, usableWorkers(pos))
  }

对一次 Application的调度来说,

  • 如果app.desc中配置了每个Executor的Core数(coresPerExecutor),就用计算好的assignedCores除以coresPerExecutor
    得到Executor个数,否则就只启动一个Executor,把所有assignedCores在当前一个可用Worker上运行起来。
  • 遍历Executor的个数,
       每次为当前App指定worker和Core的个数,启动一个Executor,并修改App的状态为RUNNING.
  private def allocateWorkerResourceToExecutors(
      app: ApplicationInfo,
      assignedCores: Int,
      coresPerExecutor: Option[Int],
      worker: WorkerInfo): Unit = {
    // If the number of cores per executor is specified, we divide the cores assigned
    // to this worker evenly among the executors with no remainder.
    // Otherwise, we launch a single executor that grabs all the assignedCores on this worker.
    // 如果我们在每个worker上只启动一个executor(默认没有指定spark.executor.cores 的情况下),
    // 这个单独的Executor会捕获Worker上所有可用的核数
    val numExecutors = coresPerExecutor.map { assignedCores / _ }.getOrElse(1)
    val coresToAssign = coresPerExecutor.getOrElse(assignedCores)
    for (i <- 1 to numExecutors) {
      val exec = app.addExecutor(worker, coresToAssign)
      launchExecutor(worker, exec)
      app.state = ApplicationState.RUNNING
    }
  }

可见App的调度,其实是App的所有Executor的分配资源和启动,均匀分布在可用的worker上。

launchExecutor()把ExecutorDesc添加给worker,
向worker发送LaunchExecutor消息,
最后向app的driver发送ExecutorAdded消息。

  private def launchExecutor(worker: WorkerInfo, exec: ExecutorDesc): Unit = {
    logInfo("Launching executor " + exec.fullId + " on worker " + worker.id)
    worker.addExecutor(exec)
    worker.endpoint.send(LaunchExecutor(masterUrl,
      exec.application.id, exec.id, exec.application.desc, exec.cores, exec.memory))
    exec.application.driver.send(
      ExecutorAdded(exec.id, worker.id, worker.hostPort, exec.cores, exec.memory))
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spark架构是指Spark的整体架构,包括Spark的组件和它们之间的关系。Spark架构主要由以下几个组件组成: 1. Spark CoreSpark的核心组件,提供了分布式任务调度、内存管理、错误恢复等基础功能。 2. Spark SQL:Spark的SQL查询组件,支持SQL查询和DataFrame API。 3. Spark Streaming:Spark的流处理组件,支持实时数据处理和流式计算。 4. MLlib:Spark的机器学习库,提供了常用的机器学习算法和工具。 5. GraphX:Spark的图计算库,支持图计算和图分析。 Spark架构采用了Master/Slave的分布式架构,其中Master节点负责任务调度资源管理,Slave节点负责具体的计算任务。Spark还支持多种集群管理方式,包括Standalone、YARN和Mesos等。 总之,Spark架构是一个高度灵活、可扩展的分布式计算框架,可以支持各种类型的数据处理和计算任务。 ### 回答2: Spark是一个开源的大数据处理框架,它采用内存计算的方式来提高计算效率和速度。它可以处理大规模的数据,并且可以运行在分布式的集群环境中,这样就可以利用集群中的多台服务器来共同完成大规模数据的处理。 在Spark的架构中,主要包括以下几个组件: 1. Spark集群管理器:包括YARN、Mesos和Standalone三种方式,用于分配和管理集群中的资源,以便Spark应用程序可以在集群上运行和协调任务。 2. Spark Driver: 运行在Driver节点上的进程,负责调度Spark任务的执行,启动和监控应用程序运行,并将结果返回给客户端。 3. Spark Executor: 运行在集群中的Spark节点上的进程,负责执行Spark任务,处理节点数据并将结果返回给Driver进程。 4. Spark Context:Spark应用程序与集群进行通信的接口,它负责为应用程序创建一个Spark环境,并管理与集群的通信。 5. RDD(Resilient Distributed Dataset):弹性分布式数据集,是Spark中的核心抽象。它是分布式的、只读的、可缓存的对象集合,可以并行计算,且可以容错地重新计算丢失的分区数据。 6. Spark SQL:用于处理结构化数据,提供了类似于SQL的强大查询语言。 总体来说,Spark的架构实现了高效的分布式计算,能够支持多种不同的数据计算和处理任务,并且具有较高的性能和可扩展性,可以在大数据处理场景下发挥重要作用。因此,学习Spark的架构和基本概念对于掌握大数据处理技术是非常重要的。 ### 回答3: Apache Spark是一个基于内存的分布式计算引擎,它提供了容错性和高效的执行机制,因此在大数据领域广受欢迎。Spark的分布式计算能力以及简单易用的API使得用户能够轻松地编写复杂的并行处理程序。在本文中,我们将深入探讨Spark的架构和工作原理。 Spark的架构 Spark的架构是一个典型的主从式计算,其中有一个集群管理器(Master)和多个工作节点(Worker)组成。 集群管理器(Master) 集群管理器是Spark系统的主要组成部分,它负责对所有工作节点进行协调和管理。在集群管理器中,有两个重要的组件:Spark Driver和Cluster Manager。 Spark Driver Spark Driver是Spark应用程序的输入点。它接收应用程序代码和数据,将它们分发到工作节点,并管理它们的执行。它还负责将结果传递回给客户端。 Cluster Manager Cluster Manager是Spark资源管理器。它负责将工作负载分配到各个工作节点,并监控它们的执行情况。Spark支持多种类型的Cluster Manager,包括Standalone、Apache Mesos和Hadoop YARN。 工作节点(Worker) 工作节点(Worker)是Spark系统的计算子系统。每个工作节点可以运行一个或多个执行器(Executor)。执行器负责在节点上运行Spark任务,并将结果返回给驱动程序。通常,每个执行器都有一个或多个线程,并按照设置的阈值动态调整其计算资源Spark的工作原理 Spark计算过程分为两个主要阶段:RDD转换和操作执行。在第一阶段,输入数据将划分为适当的大小,每个部分将被放置在集群中的一个节点上。在第阶段中,Spark将对这些分区进行操作,并将结果汇总到驱动程序中。 RDD转换 RDD(Resilient Distributed Dataset)是纵向分区的元素集合,它是Spark内部的主要数据抽象。RDD是不可修改的,即RDD中的数据不会被修改而是通过转换在不同RDD之间进行。Spark提供了各种类型的RDD转换操作,例如map、filter、flatMap、groupByKey、reduceByKey等。 操作执行 Spark中的操作可以分为两种:转换(Transformation)和动作(Action)。转换操作产生一个新的RDD,而动作操作则返回具体的结果。常见的动作操作有collect、reduce、count等。 在执行操作前,Spark需要创建一个作业图(job graph),它表示RDD之间的依赖关系。作业图会对RDD之间的依赖关系进行排序,并将它们分解为适当的任务。然后,Spark将这些任务分发到不同的节点上执行。 总结 Spark的架构和工作原理使其在大数据处理领域中表现出色。它的并行计算能力和简单易用的API使开发人员能够高效地处理大规模数据。然而,为了使Spark的性能最大化,需要理解RDD转换和操作执行之间的相互作用,并正确地使用Spark的API。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值