Spark有几个工具用于在计算之间调度资源。Spark运行的集群管理器(cluster manager)(主要包括 Standalone、Mesos和YARN这三种)为跨应用程序调度提供了便利,有静态分区方式和动态资源分配方式两种。 其次,在各个Spark应用内部,Spark包含一个公平调度器来调度每个SparkContext中的资源。
1. 跨应用调度
1.1 使用静态分区方式分配集群资源
所有集群管理器都可以使用的最简单的选项是资源的静态分区。 这种方式就意味着,每个Spark应用都是设定一个最大可用资源总量,并且该应用在整个生命周期内都会占住这个资源.。这是Standalone和YARN模式以及粗粒度Mesos模式中使用的方法。
1.2 动态资源分配方式
Spark提供了一种机制,可根据工作负载动态调整应用程序占用的资源。 这意味着您的应用程序可能会在资源不再使用时将资源返回给群集,并在需求时可以再次请求使用。 如果多个应用程序共享Spark群集中的资源,此功能特别有用。
此功能在默认情况下处于禁用状态,可用于所有粗粒度集群管理器,即独立模式,YARN模式和Mesos粗粒度模式。
下面介绍配置和部署方式:
要使用这一特性有两个前提条件。首先,你的应用必须设置spark.dynamicAllocation.enabled为true。其次,你必须在每个节点上启动external shuffle service,并将spark.shuffle.service.enabled设为true。external shuffle service 的目的是在移除executor的时候,能够保留executor输出的shuffle文件。启用external shuffle service 的方式在各个集群管理器上各不相同:
- 在Spark独立部署的集群中,你只需要在worker启动前设置spark.shuffle.service.enabled为true即可。
- 在Mesos粗粒度模式下,你需要在各个节点上运行$SPARK_HOME/sbin/start-mesos-shuffle-service.sh 并设置 spark.shuffle.service.enabled为true即可。
- 在YARN模式下,需要按以下步骤在各个NodeManager上启动:这里
动态资源分配的具体实现:
总体上来说,Spark应该在执行器(executor)空闲时将其关闭,而在后续要用时再申请。因为没有一个固定的方法,可以预测一个执行器在后续是否马上会被分配去执行任务,或者一个新分配的执行器实际上是空闲的,所以我们需要一个试探性的方法,来决定是否申请或是移除一个执行器。
- 请求executor的策略:
一个启用了动态分配的Spark应用会有等待任务需要调度的时候,申请额外的executors。在这种情况下,必定意味着已有的executors已经不足以同时执行所有未完成的任务。
Spark会分轮次来申请执行器。实际的资源申请,会在任务挂起spark.dynamicAllocation.schedulerBacklogTimeout
秒后首次触发,其后如果等待队列中仍有挂起的任务,则每过spark.dynamicAllocation.sustainedSchedulerBacklogTimeout
秒后触发一次资源申请。另外,每一轮申请的执行器个数以指数形式增长。例如:一个Spark应用可能在首轮申请1个执行器,后续的轮次申请个数可能是2个、4个、8个….。
采用指数级增长策略的原因有两个:第一,应用程序应该在开始时谨慎地请求执行器,以防只需要少数的执行者就已经足够了;第二,如果一旦Spark应用确实需要申请多个执行器的话,那么可以确保其所需的计算资源及时增长。 - 移除executor的策略:
移除执行器的策略就简单得多了。Spark应用会在某个执行器空闲超过spark.dynamicAllocation.executorIdleTimeout
秒后将其删除,在大多数情况下,执行器的移除条件和申请条件都是互斥的,也就是说,执行器在有等待执行任务挂起时,不应该空闲。
非动态分配模式下,执行器可能的退出原因有执行失败或是相关Spark应用已经退出。不管是哪种原因,执行器的所有状态都已经不再需要,可以丢弃掉。但是在动态分配的情况下,执行器有可能在Spark应用运行期间被移除。这时候,如果Spark应用尝试去访问该执行器存储的状态,就必须重算这一部分数据。因此,Spark需要一种机制,能够优雅的关闭执行器,同时还保留其状态数据。要解决这一问题,就需要用到 external shuffle service ,该服务在 Spark 1.2 引入。该服务在每个节点上都会启动一个不依赖于任何 Spark 应用或执行器的独立进程,就可以优雅的来关闭executor。
2. 应用内调度
在指定的 Spark 应用内部(对应同一 SparkContext 实例),多个线程可能并发地提交 Spark 作业(job)。Spark 调度器是完全线程安全的,并且能够支持 Spark 应用同时处理多个请求(比如 : 来自不同用户的查询)。
2.1 FIFO 调度策略
默认情况下,Spark 应用内部使用 FIFO 调度策略。每个作业被划分为多个阶段(stage)(例如 : map 阶段和 reduce 阶段),第一个作业在其启动后会优先获取所有的可用资源,然后是第二个作业再申请,再第三个……。如果前面的作业没有把集群资源占满,则后续的作业可以立即启动运行,否则,后提交的作业会有明显的延迟等待。
2.2 公平(Fair)调度策略
不过从 Spark 0.8 开始,Spark 也能支持各个作业间的公平(Fair)调度。公平调度时,Spark 以轮询的方式给每个作业分配资源,因此所有的作业获得的资源大体上是平均分配。这意味着,即使有大作业在运行,小的作业再提交也能立即获得计算资源而不是等待前面的作业结束,大大减少了延迟时间。这种模式特别适合于多用户配置。 要启用公平调度器,只需设置一下 SparkContext 中 spark.scheduler.mode 属性为 FAIR 即可 :
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.scheduler.mode", "FAIR")
val sc = new SparkContext(conf)
如果启用了公平调度策略,默认情况下,各个资源池之间平分整个集群的资源,但在资源池内部,默认情况下,作业是 FIFO 顺序执行的。举例来说,如果你为每个用户创建了一个资源池,那么久意味着各个用户之间共享整个集群的资源,但每个用户自己提交的作业是按顺序执行的,而不会出现后提交的作业抢占前面作业的资源。
默认情况下,新提交的作业都会进入到默认(default)资源池中,不过作业对应于哪个资源池,可以在提交作业的线程中用 SparkContext.setLocalProperty 设定 spark.scheduler.pool 属性。示例代码如下 :
sc.setLocalProperty("spark.scheduler.pool", "pool1")
一旦设好了局部属性,所有该线程所提交的作业(即 : 在该线程中调用action算子,如 : RDD.save/count/collect 等)都会使用这个资源池。同样,如果需要清除资源池设置,只需在对应线程中调用如下代码 :
sc.setLocalProperty("spark.scheduler.pool", null)
当然这一切都是可以修改的,比如:公平调度器还可以支持将作业分组放入资源池(pool),然后给每个资源池配置不同的选项(如 : 权重)。这样你就可以给一些比较重要的作业创建一个“高优先级”资源池,或者你也可以把每个用户的作业分到一组,这样一来就是各个用户平均分享集群资源,而不是各个作业平分集群资源。下面就介绍一下具体的配置:
资源池属性是一个 XML 文件,可以基于 conf/fairscheduler.xml.template 修改,然后在 SparkConf 的 spark.scheduler.allocation.file 属性指定文件路径:
conf.set("spark.scheduler.allocation.file", "/path/to/file")
资源池 XML 配置文件格式如下,其中每个池子对应一个 元素,每个资源池可以有其独立的配置 :
<?xml version="1.0"?>
<allocations>
<pool name="production">
<schedulingMode>FAIR</schedulingMode>
<weight>1</weight>
<minShare>2</minShare>
</pool>
<pool name="test">
<schedulingMode>FIFO</schedulingMode>
<weight>2</weight>
<minShare>3</minShare>
</pool>
</allocations>
资源池的属性需要通过配置文件来指定。每个资源池都支持以下3个属性 :
- schedulingMode: 控制资源池内部的作业是如何调度的,可以是 FIFO 或 FAIR。
- weight: 控制资源池相对其他资源池可以分配到资源的比例。默认所有资源池的 weight 都是 1。如果你将某个资源池的 weight 设为 2,那么该资源池中的资源将是其他池子的2倍。如果将 weight 设得很高,如 1000,可以实现资源池之间的调度优先级 。 也就是说,weight=1000 的资源池总能立即启动其对应的作业。
- minShare: 除了整体 weight 之外,每个资源池还能指定一个最小资源分配值(CPU 个数),管理员可能会需要这个设置。公平调度器总是会尝试优先满足所有活跃(active)资源池的最小资源分配值,然后再根据各个池子的 weight 来分配剩下的资源。因此,minShare 属性能够确保每个资源池都能至少获得一定量的集群资源。minShare 的默认值是 0。
注意,没有在配置文件中配置的资源池都会使用默认配置(schedulingMode : FIFO,weight : 1,minShare : 0)。