[Spark]-作业调度与动态资源分配
1.概述
由 Spark 集群篇 ,每个Spark应用(其中包含了一个SparkContext实例),都会运行一些独占的执行器(executor)进程.集群调度器会提供对这些 Spark 应用的资源调度.
而在各个Spark应用内部,各个线程可能并发地通过action算子提交多个Spark作业(job).这里就是Spark的作业调度
Spark作业调度默认FIFO,也支持公平调度(fair scheduler)
2.跨应用调度
在集群上运行,每个Spark应用都会SparkContext获得一批独占的执行器JVM,来运行其任务并存储数据.
如果有多个用户共享集群,那么就会有很多资源分配相关选项或者策略,这些选项或者策略都取决于具体的集群管理器.
2.1 静态资源分配
这些资源分配相关选项或者策略中最简单的,就是静态资源分配
静态资源分配,意味着每个Spark应用都会有一个资源上限,并且在应用的整个生命周期都会始终圈占住这部分资源.
这种分配方式在Standalone ,Mesos 或 YARN 都支持.具体如下:
i). Standalone
默认情况下,Spark应用在独立部署的集群中都会以FIFO(first-in-first-out)模式顺序提交运行,并且每个spark应用都会占用集群中所有可用节点
但是可以通过设置spark.cores.max或者spark.deploy.defaultCores来限制单个应用所占用的节点个数.
还可以通过设置spark.executor.memory来控制各个应用的内存占用量
ii).Mesos
在Mesos中要使用静态划分的话,需要将spark.mesos.coarse设为true,
通过配置spark.cores.max来控制各个应用的CPU总数,以及spark.executor.memory来控制各个应用的内存占用
iii).YARN
在YARN中需要使用 –num-executors 选项来控制Spark应用在集群中分配的执行器的个数
对于单个执行器(executor)所占用的资源,可以使用 –executor-memory和–executor-cores来控制
另外:
i).现如今没有任何一种资源分配模式可以做到跨Spark应用的内存共享.如果要做到这一点,必须在两个Spark应用中启用一个第三方存储应用
ii).Mesos上还有一种动态共享CPU的方式。
在这种模式下,每个Spark应用的内存占用仍然是固定且独占的(仍由spark.exexcutor.memory决定),
但是如果该Spark应用没有在某个机器上执行任务的话,那么其它应用可以占用该机器上的CPU。这种模式对集群中有大量不是很活跃应用的场景非常有效
例如:集群中有很多不同用户的Spark shell session.但这种模式不适用于低延时的场景,因为当Spark应用需要使用CPU的时候,可能需要等待一段时间才能取得对CPU的使用权。
要使用这种模式,只需要在mesos://URL上设置spark.mesos.coarse属性为false即可
2.2 动态资源分配
Spark 提供了一种基于负载来动态调节Spark应用资源占用的机制.这种机制就叫做动态资源分配
这意味着,你的应用会在资源空闲的时间将其释放给集群,需要时再重新申请,这一特性在多个应用Spark集群资源的情况下特别有用
注意:这个特性默认是禁止的.但是对所有粗粒度的集群调度器都是可用的.比如
i).独立部署模式standalone mode, YARN mode
ii).粗粒度模式Mesos coarse-grained mode
2.2.1 配置和部署
要启用动态资源的特性.必须先配置和部署以下两点:
i).spark.dynamicAllocation.enabled 设置为true
ii).在每个节点启动 external shuffle service 并且将spark.shuffle.service.enabled设为true.
external shuffle service 的目的是在移除executor的时候,能够保留executor输出的shuffle文件.它在各个集群类别下各不相同
StandAlone 模式 : 只需要在worker启动前设置spark.shuffle.service.enabled为true即可
Mesos 粗粒度模式 : 需要在各个节点上运行$SPARK_HOME/sbin/start-mesos-shuffle-service.sh 并设置 spark.shuffle.service.enabled为true即可
YARN模式 : 需要按以下步骤在各个NodeManager上启动
第一步.使用YARN profile 构建Spark (如果是使用的Spark预包装的发布可以跳过该步骤)
第二步.找到 spark-<version>-yarn-shuffle.jar (它一般在 $SPARK_HOME/common/network-yarn/target/scala-<version>)
第三步.将这个Jar放到所有 NodeManager 所在的 classpath 中
第四步.修改 yarn-site.xml 配置, 设置 yarn.nodemanager.aux-services.spark_shuffle.class 为 org.apache.spark.network.yarn.YarnShuffleService
第五步.修改 etc/hadoop/yarn-env.sh, 增加 YARN_HEAPSIZE(默认1000)来调过NodeManager的堆大小,避免在shuffle过程中出现GC
第六步.重启 NodeManager
2.2.2 资源分配策略
总体上来说,Spark应该在执行器空闲时将其关闭,而在后续要用时再申请
因为没有办法可以确定分配给的执行器之后会被分配去执行其它任务还是空闲的,所以需要通过试探是否真正有效,以决定使用这个执行器或重新再申请一个
2.2.2.1 申请策略
一个启用的动态资源分配的执行器,在面对有等待任务需要调度的时候,会尝试申请额外的执行器(申请必然意味着现有的执行器不足以并行的执行所有任务,否则不会申请)
Spark会定时的分批次来申请执行器
实际的资源申请,会在任务挂起(spark.dynamicAllocation.schedulerBacklogTimeout)秒后首次触发
其后如果等待队列中仍有挂起的任务,则每过(spark.dynamicAllocation.sustainedSchedulerBacklogTimeout)秒后触发一次资源申请
每一轮申请的执行器个数以指数形式增长
例如: 一个Spark应用可能在首轮申请1个执行器,后续的轮次申请个数可能是2个、4个、8个….
采用指数级增长的申请策略的原因有两个:
第一,对于任何一个Spark应用如果只需要多申请少数几个执行器的话,那么必须非常谨慎的启动资源申请,这和TCP慢启动有些类似
第二,如果一旦Spark应用确实需要申请多个执行器的话,那么可以确保其所需的计算资源及时增长
2.2.2.2 移除策略
移除执行器的策略非常简单,Spark应用会在某个执行器空闲超过 spark.dynamicAllocation.executorIdleTimeout秒后将其删除.
在大多数情况下,执行器的移除条件和申请条件都是互斥的,也就是说,执行器在有等待执行任务挂起时,不应该空闲
2.2.3 优雅的关闭执行器
静态资源分配,执行器的关闭可能是执行失败或SparkContext已关闭.但不管哪种原因,都意味着可以安全的释放执行器的所有资源
但在动态资源分配中,执行器有可能在运行时被移除.这时如果想要去访问其存储的状态,就必须重算这部分的数据.
因此,在动态资源分配中,需要一种机制,能优雅的关闭某个执行器,并保留这个执行器的存储状态.
这种保留,对于Shuffle操作尤其重要.因为在shuffle操作中,map会首先溢写到磁盘,而执行器本身又是一个文件系统.这样其它执行器就可以直接读取对应数据
如果在动态资源分配中,执行器被移除代表其存储的数据也会被移除导致其它执行器读取时不得不重算.而这种重算本身是不必要的
解决这一问题的机制就是 external shuffle service . 它会在Spark每个节点上运行一个不依赖任何executor的独立进程.
一旦启动该服务,执行器不再从各个执行器获取shuffle文件,而将从这个服务获取.(这也意味着shuffle数据本身有可能会比执行器活的更久)
除了shuffle文件本身,执行器也会在内存和磁盘缓存数据.一旦执行器被异常,这些缓存将不能被访问.
为了减少这种情况,默认情况下包含缓存数据的执行程序永远不会被删除
也可以使用spark.dynamicAllocation.cachedExecutorIdleTimeout配置此行为。在未来的版本中,缓存的数据可以通过堆外存储来保存
3.应用内调度
在一个 Spark 应用内部.会以多个线程并行的执行作业(由 Spark action 算子触发的一系列计算任务的集合).并且能够支持 Spark 应用同时处理多个请求.
3.1 FIFO 调度
Spark 默认采用 FIFO 调度.在作业执行时,每个作业会划分为多个阶段(stage)(map stage 或 reduce stage).
具体策略是:
第一个作业可以使用全部资源.如果没有占满,则可以启动第二个作业申请,否则延迟等待.以此类推
3.2 公平(Fair)调度
3.2.1 公平策略
公平调度时,Spark 以轮询的方式给每个作业分配资源,因此所有的作业获得的资源大体上是平均分配
这意味着,即使有大作业在运行,小的作业再提交也能立即获得计算资源而不是等待前面的作业结束,大大减少了延迟时间。这种模式特别适合于多用户配置。
要启用公平调度器,只需设置一下 SparkContext 中 spark.scheduler.mode 属性为 FAIR
conf.set("spark.scheduler.mode", "FAIR")
3.2.2 公平调度资源池
公平调度器还可以支持将作业分组放入资源池(pool),然后给每个资源池配置不同的选项(如:权重) .这样就可以给相对重要的作业创建一个“高优先级”资源池
Spark 公平调度的实现方式基本都是模仿 Hadoop Fair Scheduler. 来实现的
3.2.2.1 使用&移除
默认情况下,新提交的作业都会进入到默认资源池中.
可以在提交作业的线程中用 SparkContext.setLocalProperty 设定 spark.scheduler.pool 属性
sc.setLocalProperty("spark.scheduler.pool", "pool1")
一旦设好了局部属性,所有该线程所提交的作业(即 : 在该线程中调用action算子,如 : RDD.save/count/collect 等)都会使用这个资源池。
这个设置是以线程为单位保存的,你很容易实现用同一线程来提交同一用户的所有作业到同一个资源池中
如果需要清除资源池设置,只需在对应线程中调用如下代码
sc.setLocalProperty("spark.scheduler.pool", null)
3.2.2.2 资源池的默认行为
默认地,各个资源池之间平分整个集群的资源(包括 default 资源池),但在资源池内部,默认情况下,作业是 FIFO 顺序执行的。
例如,如果你为每个用户创建了一个资源池,那意味着各个用户之间共享整个集群的资源,但每个用户自己提交的作业依然是按顺序执行的
3.2.2.3 资源池的配置
资源池的属性需要通过配置文件来指定。每个资源池都支持以下3个属性 :
schedulingMode
: 可以是 FIFO 或 FAIR,控制资源池内部的作业是如何调度的。
weight
: 控制资源池相对其他资源池,可以分配到资源的比例。默认所有资源池的 weight 都是 1
minShare
: 除了整体 weight 之外,每个资源池还能指定一个最小资源分配值(CPU 个数).
公平调度器总是会尝试优先满足所有活跃(active)资源池的最小资源分配值,然后再根据各个池子的 weight 来分配剩下的资源。
因此,minShare 属性能够确保每个资源池都能至少获得一定量的集群资源。minShare 的默认值是 0
资源池属性是一个 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>