1 Driver 和 Executor 资源调度
当 Application 提交注册到 Master 后,Master 会返回 RegisteredApplication
,
之后便会调用 schedule()
这个方法,来分配 Driver 的资源,和启动 Executor 的资源。
schedule()
方法是来调度当前可用资源的调度方法,它管理还在排队等待的 Apps 资源的分配,
这个方法是每次在集群资源发生变动的时候都会调用,根据当前集群最新的资源来进行 Apps 的资源分配。
Driver
资源调度:随机的将 Driver
分配到空闲的 Worker
上去,详细流程请看我写的注释 :
// First schedule drivers, they take strict precedence over applications
val shuffledWorkers = Random.shuffle(workers) // 把当前workers这个HashSet的顺序随机打乱
for (worker <- shuffledWorkers if worker.state == WorkerState.ALIVE) { //遍历活着的workers
for (driver <- waitingDrivers) { //在等待队列中的Driver们会进行资源分配
if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) { //当前的worker内存和cpu均大于当前driver请求的mem和cpu,则启动
launchDriver(worker, driver) //启动Driver 内部实现是发送启动Driver命令给指定Worker,Worker来启动Driver。
waitingDrivers -= driver //把启动过的Driver从队列移除
}
}
}
Executor
资源调度:Spark 默认提供了一种在各个节点进行 round-robin 的调度,用户可以自己设置这个 flag:
val spreadOutApps = conf.getBoolean("spark.deploy.spreadOut", true)
在介绍之前我们先介绍一个概念:
可用的 Worker:什么是可用?可用就是 资源空闲足够且满足一定的 规则来启动当前 App 的 Executor。
Spark 定义了一个 canUse
方法:这个方法接受一个 ApplicationInfo
的描述信息和当前 Worker 的描述信息。
-
当前 worker 的空闲内存比该 app 在每个 slave 要占用的内存 (
executor.memory
默认512M) 大 -
当前 app 从未在此 worker 启动过 App
总结:从这点看出,要满足:该 Worker
的当前可用最小内存要比配置的 executor 内存大,并且对于同一个 App 只能在一个 Worker 里启动一个 Exeutor,
如果要启动第二个 Executor,那么请到其它 Worker
里。这样的才算是对 App 可用的 Worker
。
/**
* Can an app use the given worker? True if the worker has enough memory and we haven't already
* launched an executor for the app on it (right now the standalone backend doesn't like having
* two executors on the same worker).
*/
def canUse(app: ApplicationInfo, worker: WorkerInfo): Boolean = {
worker.memoryFree >= app.desc.memoryPerSlave && !worker.hasExecutor(app)
}
SpreadOut 分配策略:
SpreadOut 分配策略是一种以 round-robin
方式遍历集群所有可用 Worker,分配 Worker
资源,
来启动创建 Executor 的策略,好处是尽可能的将 cores 分配到各个节点,最大化负载均衡和高并行。
下面看看,默认的 spreadOutApps
模式启动 App 的过程:
-
等待分配资源的 apps 队列默认是 FIFO 的。
-
app.coresLeft
表示的是该 app 还有 cpu 资源没申请到:app.coresLeft
= 当前 app 申请的 maxcpus - granted 的cpus -
遍历未分配完全的 apps,继续给它们分配资源
-
usableWorkers
= 从当前ALIVE
的 Workers 中过滤找出上文描述的可用 Worker,然后根据 cpus 的资源空闲,从大到小给 Workers 排序。 -
当
toAssign
(即将要分配的的 core 数 > 0,就找到可以的 Worker 持续分配) -
当可用 Worker 的 free cores 大于目前该 Worker 已经分配的 core 时,再给它分配 1 个 core,这样分配是很平均的方法。
-
round-robin
轮询可用的 Worker 循环 -
toAssign=0
时结束循环,开始根据分配策略去真正的启动 Executor。
举例: 1 个 APP 申请了 6 个 core, 现在有 2 个 Worker 可用。那么:toAssign = 6,assigned = 2
那么就会在 assigned(1)
和 assigned(0)
中轮询平均分配 cores,以 +1 core 的方式,
最终每个 Worker 分到3个 core,即每个 Worker
的启动一个Executor
,每个 Executor
获得 3 个 cores。
// 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.
if (spreadOutApps) {
// Try to spread out each app among all the nodes, until it has all its cores
for (app <- waitingApps if app.coresLeft > 0) { //对还未被完全分配资源的apps处理
val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
.filter(canUse(app, _)).sortBy(_.coresFree).reverse //根据core Free对可用Worker进行降序排序。
val numUsable = usableWorkers.length //可用worker的个数 eg:可用5个worker
val assigned = new Array[Int](numUsable) //候选Worker,每个Worker一个下标,是一个数组,初始化默认都是0
var toAssign = math.min(app.coresLeft, usableWorkers.map(_.coresFree).sum)//还要分配的cores = 集群中可用Worker的可用cores总和(10), 当前未分配core(5)中找最小的
var pos = 0
while (toAssign > 0) {
if (usableWorkers(pos).coresFree - assigned(pos) > 0) { //以round robin方式在所有可用Worker里判断当前worker空闲cpu是否大于当前数组已经分配core值
toAssign -= 1
assigned(pos) += 1 //当前下标pos的Worker分配1个core +1
}
pos = (pos + 1) % numUsable //round-robin轮询寻找有资源的Worker
}
// Now that we've decided how many cores to give on each node, let's actually give them
for (pos <- 0 until numUsable) {
if (assigned(pos) > 0) { //如果assigned数组中的值>0,将启动一个executor在,指定下标的机器上。
val exec = app.addExecutor(usableWorkers(pos), assigned(pos)) //更新app里的Executor信息
launchExecutor(usableWorkers(pos), exec) //通知可用Worker去启动Executor
app.state = ApplicationState.RUNNING
}
}
}
} else {
非 SpreadOut
分配策略:
非 SpreadOut
策略,该策略:会尽可能的根据每个 Worker 的剩余资源来启动 Executor,
这样启动的 Executor 可能只在集群的一小部分机器的 Worker 上。这样做对 node 较少的集群还可以,集群规模大了,Executor
的并行度和机器负载均衡就不能够保证了。
当用户设定了参数 spark.deploy.spreadOut
为 false
时,触发此分支 。
-
遍历可用 Workers
-
且遍历 Apps
-
比较当前 Worker 的可用 core 和 app 还需要分配的 core,取最小值当做还需要分配的 core
-
如果
coreToUse
大于 0,则直接拿可用的 core 来启动 Executor。奉献当前 Worker 全部资源。(Ps:挨个榨干每个Worker的剩余资源。。。。)
举例: App 申请 12 个 core,3 个 Worker,Worker1 剩余 1 个 core, Worke2 剩 7 个 core, Worker3 剩余 4 个 core.
这样会启动 3 个 Executor,Executor1 占用 1 个 core, Executor2 占用 7 个 core, Executor3 占用 4 个 core.
总结:这样是尽可能的满足 App,让其尽快执行,而忽略了其并行效率和负载均衡。
} else {
// Pack each app into as few nodes as possible until we've assigned all its cores
for (worker <- workers if worker.coresFree > 0 && worker.state == WorkerState.ALIVE) {
for (app <- waitingApps if app.coresLeft > 0) {
if (canUse(app, worker)) { //直接问当前worker是有空闲的core
val coresToUse = math.min(worker.coresFree, app.coresLeft) //有则取,不管多少
if (coresToUse > 0) { //有
val exec = app.addExecutor(worker, coresToUse) //直接启动
launchExecutor(worker, exec)
app.state = ApplicationState.RUNNING
}
}
}
}
}
}
2 总结:
-
在
Worker Actor
中,每次LaunchExecutor
会创建一个CoarseGrainedExecutorBackend
进程,一个Executor
对应一个CoarseGrainedExecutorBackend
-
针对同一个 App,每个 Worker 里只能有一个针对该 App 的 Executor 存在,切记。如果想让整个 App 的 Executor 变多,设置
SPARK_WORKER_INSTANCES
,让Worker
变多。 -
Executor 的资源分配有2种策略:
-
SpreadOut :一种以
round-robin
方式遍历集群所有可用Worker
,分配Worker
资源,来启动创建Executor
的策略,好处是尽可能的将 cores 分配到各个节点,最大化负载均衡和高并行。 -
非 SpreadOut:会尽可能的根据每个
Worker
的剩余资源来启动Executor
,这样启动的Executor
可能只在集群的一小部分机器的Worker
上。这样做对node
较少的集群还可以,集群规模大了,Executor
的并行度和机器负载均衡就不能够保证了。
-