Spark提供了一种机制,可以根据工作负载动态调整应用程序占用的资源。 这意味着,如果不再使用资源,应用程序可以将资源返还给群集,并在以后有需求时再次请求它们。 如果多个应用程序共享您的Spark集群中的资源,则此功能特别有用。
默认情况下,此功能是禁用的,并且在各种类型的集群管理器中都可使用用。
从较高的层次上讲,Spark应该在不再使用executor时将其放弃,并在需要它们时获取他们。由于没有确定的方法可以预测即将被删除的执行程序是否将在不久的将来运行任务,或者即将被添加的新执行程序实际上将处于空闲状态,因此我们需要一组启发式方法来确定何时删除或者请求executor。
启用了动态分配的Spark应用程序在有待调度的待处理任务时会请求额外的executor。这种情况必然意味着现有的executor集合不足以同时运行所有已提交但尚未完成的任务。
Spark会一轮轮请求执行者。当存在待处理任务的spark.dynamicAllocation.schedulerBacklogTimeout秒时触发实际请求,然后如果待处理任务队列持续存在,则每 spark.dynamicAllocation.sustainedSchedulerBacklogTimeout秒再次触发一次实际请求。此外,每轮请求的executor数量与上一轮相比呈指数增长。例如,一个应用程序将在第一轮中添加1个executor,然后在随后的轮中添加2、4、8,依此类推。
采取指数增长政策的动机是双重的。首先,如果事实证明只有几个额外的executor就足够了,那么应用程序应在开始时就谨慎地请求执行者。这呼应了TCP缓慢启动的理由。其次,如果事实证明确实需要许多执行程序,则应用程序应该能够及时提高其资源使用率。
删除执行程序的策略要简单得多。当执行程序闲置时间超过spark.dynamicAllocation.executorIdleTimeout秒时,Spark应用程序将其删除。请注意,在大多数情况下,此条件与请求条件是互斥的,因为如果仍有待调度的任务要安排,则executor不应处于空闲状态。
在动态分配之前,Spark的执行程序会在失败时或在关联的应用程序也已退出时退出。在这两种情况下,都不再需要与执行程序关联的所有状态,并且可以安全地将其丢弃。但是,通过动态分配,当显式删除执行程序时,应用程序仍在运行。如果应用程序尝试访问存储在执行程序中或由执行程序编写的状态,则它必须执行重新计算状态。因此,Spark需要一种机制,通过在删除执行程序之前保留其状态来优雅地解除执行程序的运行。
此要求对于随机播放尤其重要。随机播放期间,Spark执行程序首先将自己的映射输出本地写入磁盘,然后在其他执行程序尝试获取这些文件时充当这些文件的服务器。如果出现散乱的任务(其运行时间比同伴更长),则动态分配可能会在重排完成之前删除执行程序,在这种情况下,必须重新计算该执行程序写入的重排文件。
保留随机播放文件的解决方案是使用外部随机播放服务,该服务也在Spark 1.2中引入。该服务指的是一个长期运行的进程,该进程独立于Spark应用程序及其执行程序在群集的每个节点上运行。如果启用该服务,Spark执行程序将从该服务而非彼此之间获取随机播放文件。这意味着由执行者编写的任何混洗状态都可以在执行者的生命周期之外继续提供。
除了编写随机播放文件外,执行程序还可以将数据缓存在磁盘或内存中。但是,当删除执行程序时,将不再访问所有缓存的数据。为了减轻这种情况,默认情况下,永远不会删除包含缓存数据的执行程序。您可以使用spark.dynamicAllocation.cachedExecutorIdleTimeout配置此行为。在将来的版本中,缓存的数据可能会通过堆外存储来保留,其本质上类似于通过外部随机播放服务来保留随机播放文件的方式。
在给定的Spark应用程序(SparkContext实例)中,如果多个并行作业是从单独的线程提交的,则它们可以同时运行。在本节中,“作业”指的是Spark动作(例如保存,收集)以及需要运行以评估该动作的所有任务。 Spark的调度程序是完全线程安全的,并支持该用例,以启用可处理多个请求(例如,针对多个用户的查询)的应用程序。
默认情况下,Spark的调度程序以FIFO方式运行作业。每个作业都分为“阶段”(例如,映射和简化阶段),第一个作业在所有可用资源上都具有优先级,而其各个阶段都有要启动的任务,则第二个作业具有优先级,依此类推。队列不需要使用整个集群,以后的作业可以立即开始运行,但是如果队列开头的作业很大,则以后的作业可能会大大延迟。
从Spark 0.8开始,还可以配置作业之间的公平共享。在公平共享下,Spark以“循环”方式在作业之间分配任务,以便所有作业都获得大致相等的群集资源份额。这意味着在执行长作业时提交的短作业可以立即开始接收资源,并且仍然获得良好的响应时间,而无需等待长作业完成。此模式最适合多用户设置。
要启用公平调度程序,只需在配置SparkContext时将spark.scheduler.mode属性设置为FAIR即可:
val conf = new SparkConf().setMaster(...).setAppName(...)
conf.set("spark.scheduler.mode", "FAIR")
val sc = new SparkContext(conf)
公平调度程序还支持将作业分组到池中,并为每个池设置不同的调度选项(例如权重)。 例如,这对于创建用于更重要的作业的“高优先级”池或将每个用户的作业分组在一起并为用户提供相等的份额而不管他们有多少并发作业,而不是给作业相等的份额很有用。 该方法以Hadoop Fair Scheduler为例。
没有任何干预,新提交的作业将进入默认池,但是可以通过在提交线程的SparkContext中将spark.scheduler.pool“本地属性”添加到SparkContext中来设置作业的池。 这样做如下:
// Assuming sc is your SparkContext variable
sc.setLocalProperty("spark.scheduler.pool", "pool1")
设置此本地属性后,在此线程内提交的所有作业(通过在该线程中调用RDD.save,count,collect等)都将使用此池名称。 该设置是按线程设置的,以使一个线程轻松代表同一用户运行多个作业。 如果您想清除与线程关联的池,只需调用:
sc.setLocalProperty("spark.scheduler.pool", null)
默认情况下,每个池都获得群集的相等份额(也与默认池中的每个作业的份额相等),但是在每个池中,作业以FIFO顺序运行。 例如,如果您为每个用户创建一个池,则意味着每个用户将获得集群的平等份额,并且每个用户的查询将按顺序运行,而不是随后的查询从该用户的较早查询中获取资源。
特定池的属性也可以通过配置文件进行修改。每个池支持三个属性:
scheduleMode:可以是FIFO或FAIR,以控制池中的作业是排在彼此后面(默认),还是公平地共享池的资源。
权重:控制相对于其他池的池在集群中的份额。默认情况下,所有池的权重均为1。例如,如果给特定池赋予权重2,则它将获得比其他活动池多2倍的资源。设置较高的权重(例如1000)还可以在池之间实现优先级-本质上,权重1000的池总是在作业处于活动状态时首先启动任务。
minShare:除总权重外,还可以为每个池分配管理员希望拥有的最小份额(作为CPU核心数)。公平的调度程序总是尝试满足所有活动池的最小份额,然后根据权重重新分配额外的资源。因此,minShare属性可以是确保池始终快速获取特定数量的资源(例如10个核心)的另一种方式,而不会为集群的其余部分赋予较高的优先级。默认情况下,每个池的minShare为0。
The pool properties can be set by creating an XML file, similar to conf/fairscheduler.xml.template, and either putting a file named fairscheduler.xml on the classpath, or setting spark.scheduler.allocation.file property in your SparkConf.
conf.set("spark.scheduler.allocation.file", "/path/to/file")
默认情况下,PySpark不支持将PVM线程与JVM线程同步,并且在多个PVM线程中启动多个作业不能保证在每个对应的JVM线程中启动每个作业。由于此限制,它无法通过sc.setJobGroup在单独的PVM线程中设置其他作业组,这也不允许以后通过sc.cancelJobGroup取消作业。
为了使PVM线程与JVM线程同步,应将PYSPARK_PIN_THREAD环境变量设置为true。这种固定线程模式允许一个PVM线程具有一个对应的JVM线程。
但是,当前,尽管它使用各自的本地属性隔离了每个线程,但是它无法从父线程继承本地属性。要变通解决此问题,您应该在PVM中创建另一个线程时手动将本地属性从父线程复制并设置到子线程。
请注意,PYSPARK_PIN_THREAD当前处于试验阶段,不建议在生产中使用。
Dynamic Resource Allocation