Skip to content

Commit 8f8dc45

Browse files
CodingCatAndrew Or
authored andcommitted
SPARK-1706: Allow multiple executors per worker in Standalone mode
resubmit of #636 for a totally different algorithm https://issues.apache.org/jira/browse/SPARK-1706 In current implementation, the user has to start multiple workers in a server for starting multiple executors in a server, which introduces additional overhead due to the more JVM processes... In this patch, I changed the scheduling logic in master to enable the user to start multiple executor processes within the same JVM process. 1. user configure spark.executor.maxCoreNumPerExecutor to suggest the maximum core he/she would like to allocate to each executor 2. Master assigns the executors to the workers with the major consideration on the memoryPerExecutor and the worker.freeMemory, and tries to allocate as many as possible cores to the executor ```min(min(memoryPerExecutor, worker.freeCore), maxLeftCoreToAssign)``` where ```maxLeftCoreToAssign = maxExecutorCanAssign * maxCoreNumPerExecutor``` --------------------------------------- Other small changes include change memoryPerSlave in ApplicationDescription to memoryPerExecutor, as "Slave" is overrided to represent both worker and executor in the documents... (we have some discussion on this before?) Author: CodingCat <zhunansjtu@gmail.com> Closes #731 from CodingCat/SPARK-1706-2 and squashes the following commits: 6dee808 [CodingCat] change filter predicate fbeb7e5 [CodingCat] address the comments 940cb42 [CodingCat] avoid unnecessary allocation b8ca561 [CodingCat] revert a change 45967b4 [CodingCat] remove unused method 2eeff77 [CodingCat] stylistic fixes 12a1b32 [CodingCat] change the semantic of coresPerExecutor to exact core number f035423 [CodingCat] stylistic fix d9c1685 [CodingCat] remove unused var f595bd6 [CodingCat] recover some unintentional changes 63b3df9 [CodingCat] change the description of the parameter in the submit script 4cf61f1 [CodingCat] improve the code and docs ff011e2 [CodingCat] start multiple executors on the worker by rewriting startExeuctor logic 2c2bcc5 [CodingCat] fix wrong usage info 497ec2c [CodingCat] address andrew's comments 878402c [CodingCat] change the launching executor code f64a28d [CodingCat] typo fix 387f4ec [CodingCat] bug fix 35c462c [CodingCat] address Andrew's comments 0b64fea [CodingCat] fix compilation issue 19d3da7 [CodingCat] address the comments 5b81466 [CodingCat] remove outdated comments ec7d421 [CodingCat] test commit e5efabb [CodingCat] more java docs and consolidate canUse function a26096d [CodingCat] stylistic fix a5d629a [CodingCat] java doc b34ec0c [CodingCat] make master support multiple executors per worker
1 parent 25998e4 commit 8f8dc45

10 files changed

Lines changed: 96 additions & 73 deletions

File tree

core/src/main/scala/org/apache/spark/deploy/ApplicationDescription.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,27 @@ import java.net.URI
2222
private[spark] class ApplicationDescription(
2323
val name: String,
2424
val maxCores: Option[Int],
25-
val memoryPerSlave: Int,
25+
val memoryPerExecutorMB: Int,
2626
val command: Command,
2727
var appUiUrl: String,
2828
val eventLogDir: Option[URI] = None,
2929
// short name of compression codec used when writing event logs, if any (e.g. lzf)
30-
val eventLogCodec: Option[String] = None)
30+
val eventLogCodec: Option[String] = None,
31+
val coresPerExecutor: Option[Int] = None)
3132
extends Serializable {
3233

3334
val user = System.getProperty("user.name", "<unknown>")
3435

3536
def copy(
3637
name: String = name,
3738
maxCores: Option[Int] = maxCores,
38-
memoryPerSlave: Int = memoryPerSlave,
39+
memoryPerExecutorMB: Int = memoryPerExecutorMB,
3940
command: Command = command,
4041
appUiUrl: String = appUiUrl,
4142
eventLogDir: Option[URI] = eventLogDir,
4243
eventLogCodec: Option[String] = eventLogCodec): ApplicationDescription =
4344
new ApplicationDescription(
44-
name, maxCores, memoryPerSlave, command, appUiUrl, eventLogDir, eventLogCodec)
45+
name, maxCores, memoryPerExecutorMB, command, appUiUrl, eventLogDir, eventLogCodec)
4546

4647
override def toString: String = "ApplicationDescription(" + name + ")"
4748
}

core/src/main/scala/org/apache/spark/deploy/JsonProtocol.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ private[deploy] object JsonProtocol {
4646
("name" -> obj.desc.name) ~
4747
("cores" -> obj.desc.maxCores) ~
4848
("user" -> obj.desc.user) ~
49-
("memoryperslave" -> obj.desc.memoryPerSlave) ~
49+
("memoryperslave" -> obj.desc.memoryPerExecutorMB) ~
5050
("submitdate" -> obj.submitDate.toString) ~
5151
("state" -> obj.state.toString) ~
5252
("duration" -> obj.duration)
@@ -55,7 +55,7 @@ private[deploy] object JsonProtocol {
5555
def writeApplicationDescription(obj: ApplicationDescription): JObject = {
5656
("name" -> obj.name) ~
5757
("cores" -> obj.maxCores) ~
58-
("memoryperslave" -> obj.memoryPerSlave) ~
58+
("memoryperslave" -> obj.memoryPerExecutorMB) ~
5959
("user" -> obj.user) ~
6060
("command" -> obj.command.toString)
6161
}

core/src/main/scala/org/apache/spark/deploy/SparkSubmit.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,8 @@ object SparkSubmit {
406406
OptionAssigner(args.jars, YARN, CLUSTER, clOption = "--addJars"),
407407

408408
// Other options
409+
OptionAssigner(args.executorCores, STANDALONE, ALL_DEPLOY_MODES,
410+
sysProp = "spark.executor.cores"),
409411
OptionAssigner(args.executorMemory, STANDALONE | MESOS | YARN, ALL_DEPLOY_MODES,
410412
sysProp = "spark.executor.memory"),
411413
OptionAssigner(args.totalExecutorCores, STANDALONE | MESOS, ALL_DEPLOY_MODES,

core/src/main/scala/org/apache/spark/deploy/SparkSubmitArguments.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,10 +482,13 @@ private[deploy] class SparkSubmitArguments(args: Seq[String], env: Map[String, S
482482
| Spark standalone and Mesos only:
483483
| --total-executor-cores NUM Total cores for all executors.
484484
|
485+
| Spark standalone and YARN only:
486+
| --executor-cores NUM Number of cores per executor. (Default: 1 in YARN mode,
487+
| or all available cores on the worker in standalone mode)
488+
|
485489
| YARN-only:
486490
| --driver-cores NUM Number of cores used by the driver, only in cluster mode
487491
| (Default: 1).
488-
| --executor-cores NUM Number of cores per executor (Default: 1).
489492
| --queue QUEUE_NAME The YARN queue to submit to (Default: "default").
490493
| --num-executors NUM Number of executors to launch (Default: 2).
491494
| --archives ARCHIVES Comma separated list of archives to be extracted into the

core/src/main/scala/org/apache/spark/deploy/master/ApplicationInfo.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ private[deploy] class ApplicationInfo(
7575
}
7676
}
7777

78-
private[master] def addExecutor(worker: WorkerInfo, cores: Int, useID: Option[Int] = None):
79-
ExecutorDesc = {
80-
val exec = new ExecutorDesc(newExecutorId(useID), this, worker, cores, desc.memoryPerSlave)
78+
private[master] def addExecutor(
79+
worker: WorkerInfo,
80+
cores: Int,
81+
useID: Option[Int] = None): ExecutorDesc = {
82+
val exec = new ExecutorDesc(newExecutorId(useID), this, worker, cores, desc.memoryPerExecutorMB)
8183
executors(exec.id) = exec
8284
coresGranted += cores
8385
exec

core/src/main/scala/org/apache/spark/deploy/master/Master.scala

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -524,52 +524,28 @@ private[master] class Master(
524524
}
525525

526526
/**
527-
* Can an app use the given worker? True if the worker has enough memory and we haven't already
528-
* launched an executor for the app on it (right now the standalone backend doesn't like having
529-
* two executors on the same worker).
530-
*/
531-
private def canUse(app: ApplicationInfo, worker: WorkerInfo): Boolean = {
532-
worker.memoryFree >= app.desc.memoryPerSlave && !worker.hasExecutor(app)
533-
}
534-
535-
/**
536-
* Schedule the currently available resources among waiting apps. This method will be called
537-
* every time a new app joins or resource availability changes.
527+
* Schedule executors to be launched on the workers.
528+
*
529+
* There are two modes of launching executors. The first attempts to spread out an application's
530+
* executors on as many workers as possible, while the second does the opposite (i.e. launch them
531+
* on as few workers as possible). The former is usually better for data locality purposes and is
532+
* the default.
533+
*
534+
* The number of cores assigned to each executor is configurable. When this is explicitly set,
535+
* multiple executors from the same application may be launched on the same worker if the worker
536+
* has enough cores and memory. Otherwise, each executor grabs all the cores available on the
537+
* worker by default, in which case only one executor may be launched on each worker.
538538
*/
539-
private def schedule() {
540-
if (state != RecoveryState.ALIVE) { return }
541-
542-
// First schedule drivers, they take strict precedence over applications
543-
// Randomization helps balance drivers
544-
val shuffledAliveWorkers = Random.shuffle(workers.toSeq.filter(_.state == WorkerState.ALIVE))
545-
val numWorkersAlive = shuffledAliveWorkers.size
546-
var curPos = 0
547-
548-
for (driver <- waitingDrivers.toList) { // iterate over a copy of waitingDrivers
549-
// We assign workers to each waiting driver in a round-robin fashion. For each driver, we
550-
// start from the last worker that was assigned a driver, and continue onwards until we have
551-
// explored all alive workers.
552-
var launched = false
553-
var numWorkersVisited = 0
554-
while (numWorkersVisited < numWorkersAlive && !launched) {
555-
val worker = shuffledAliveWorkers(curPos)
556-
numWorkersVisited += 1
557-
if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
558-
launchDriver(worker, driver)
559-
waitingDrivers -= driver
560-
launched = true
561-
}
562-
curPos = (curPos + 1) % numWorkersAlive
563-
}
564-
}
565-
539+
private def startExecutorsOnWorkers(): Unit = {
566540
// Right now this is a very simple FIFO scheduler. We keep trying to fit in the first app
567541
// in the queue, then the second app, etc.
568542
if (spreadOutApps) {
569-
// Try to spread out each app among all the nodes, until it has all its cores
543+
// Try to spread out each app among all the workers, until it has all its cores
570544
for (app <- waitingApps if app.coresLeft > 0) {
571545
val usableWorkers = workers.toArray.filter(_.state == WorkerState.ALIVE)
572-
.filter(canUse(app, _)).sortBy(_.coresFree).reverse
546+
.filter(worker => worker.memoryFree >= app.desc.memoryPerExecutorMB &&
547+
worker.coresFree >= app.desc.coresPerExecutor.getOrElse(1))
548+
.sortBy(_.coresFree).reverse
573549
val numUsable = usableWorkers.length
574550
val assigned = new Array[Int](numUsable) // Number of cores to give on each node
575551
var toAssign = math.min(app.coresLeft, usableWorkers.map(_.coresFree).sum)
@@ -582,32 +558,61 @@ private[master] class Master(
582558
pos = (pos + 1) % numUsable
583559
}
584560
// Now that we've decided how many cores to give on each node, let's actually give them
585-
for (pos <- 0 until numUsable) {
586-
if (assigned(pos) > 0) {
587-
val exec = app.addExecutor(usableWorkers(pos), assigned(pos))
588-
launchExecutor(usableWorkers(pos), exec)
589-
app.state = ApplicationState.RUNNING
590-
}
561+
for (pos <- 0 until numUsable if assigned(pos) > 0) {
562+
allocateWorkerResourceToExecutors(app, assigned(pos), usableWorkers(pos))
591563
}
592564
}
593565
} else {
594-
// Pack each app into as few nodes as possible until we've assigned all its cores
566+
// Pack each app into as few workers as possible until we've assigned all its cores
595567
for (worker <- workers if worker.coresFree > 0 && worker.state == WorkerState.ALIVE) {
596568
for (app <- waitingApps if app.coresLeft > 0) {
597-
if (canUse(app, worker)) {
598-
val coresToUse = math.min(worker.coresFree, app.coresLeft)
599-
if (coresToUse > 0) {
600-
val exec = app.addExecutor(worker, coresToUse)
601-
launchExecutor(worker, exec)
602-
app.state = ApplicationState.RUNNING
603-
}
604-
}
569+
allocateWorkerResourceToExecutors(app, app.coresLeft, worker)
570+
}
571+
}
572+
}
573+
}
574+
575+
/**
576+
* Allocate a worker's resources to one or more executors.
577+
* @param app the info of the application which the executors belong to
578+
* @param coresToAllocate cores on this worker to be allocated to this application
579+
* @param worker the worker info
580+
*/
581+
private def allocateWorkerResourceToExecutors(
582+
app: ApplicationInfo,
583+
coresToAllocate: Int,
584+
worker: WorkerInfo): Unit = {
585+
val memoryPerExecutor = app.desc.memoryPerExecutorMB
586+
val coresPerExecutor = app.desc.coresPerExecutor.getOrElse(coresToAllocate)
587+
var coresLeft = coresToAllocate
588+
while (coresLeft >= coresPerExecutor && worker.memoryFree >= memoryPerExecutor) {
589+
val exec = app.addExecutor(worker, coresPerExecutor)
590+
coresLeft -= coresPerExecutor
591+
launchExecutor(worker, exec)
592+
app.state = ApplicationState.RUNNING
593+
}
594+
}
595+
596+
/**
597+
* Schedule the currently available resources among waiting apps. This method will be called
598+
* every time a new app joins or resource availability changes.
599+
*/
600+
private def schedule(): Unit = {
601+
if (state != RecoveryState.ALIVE) { return }
602+
// Drivers take strict precedence over executors
603+
val shuffledWorkers = Random.shuffle(workers) // Randomization helps balance drivers
604+
for (worker <- shuffledWorkers if worker.state == WorkerState.ALIVE) {
605+
for (driver <- waitingDrivers) {
606+
if (worker.memoryFree >= driver.desc.mem && worker.coresFree >= driver.desc.cores) {
607+
launchDriver(worker, driver)
608+
waitingDrivers -= driver
605609
}
606610
}
607611
}
612+
startExecutorsOnWorkers()
608613
}
609614

610-
private def launchExecutor(worker: WorkerInfo, exec: ExecutorDesc) {
615+
private def launchExecutor(worker: WorkerInfo, exec: ExecutorDesc): Unit = {
611616
logInfo("Launching executor " + exec.fullId + " on worker " + worker.id)
612617
worker.addExecutor(exec)
613618
worker.actor ! LaunchExecutor(masterUrl,

core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private[ui] class ApplicationPage(parent: MasterWebUI) extends WebUIPage("app")
9494
</li>
9595
<li>
9696
<strong>Executor Memory:</strong>
97-
{Utils.megabytesToString(app.desc.memoryPerSlave)}
97+
{Utils.megabytesToString(app.desc.memoryPerExecutorMB)}
9898
</li>
9999
<li><strong>Submit Date:</strong> {app.submitDate}</li>
100100
<li><strong>State:</strong> {app.state}</li>

core/src/main/scala/org/apache/spark/deploy/master/ui/MasterPage.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ private[ui] class MasterPage(parent: MasterWebUI) extends WebUIPage("") {
208208
<td>
209209
{app.coresGranted}
210210
</td>
211-
<td sorttable_customkey={app.desc.memoryPerSlave.toString}>
212-
{Utils.megabytesToString(app.desc.memoryPerSlave)}
211+
<td sorttable_customkey={app.desc.memoryPerExecutorMB.toString}>
212+
{Utils.megabytesToString(app.desc.memoryPerExecutorMB)}
213213
</td>
214214
<td>{UIUtils.formatDate(app.submitDate)}</td>
215215
<td>{app.desc.user}</td>

core/src/main/scala/org/apache/spark/scheduler/cluster/SparkDeploySchedulerBackend.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,11 @@ private[spark] class SparkDeploySchedulerBackend(
8282
val command = Command("org.apache.spark.executor.CoarseGrainedExecutorBackend",
8383
args, sc.executorEnvs, classPathEntries ++ testingClassPath, libraryPathEntries, javaOpts)
8484
val appUIAddress = sc.ui.map(_.appUIAddress).getOrElse("")
85-
val appDesc = new ApplicationDescription(sc.appName, maxCores, sc.executorMemory, command,
86-
appUIAddress, sc.eventLogDir, sc.eventLogCodec)
87-
85+
val coresPerExecutor = conf.getOption("spark.executor.cores").map(_.toInt)
86+
val appDesc = new ApplicationDescription(sc.appName, maxCores, sc.executorMemory,
87+
command, appUIAddress, sc.eventLogDir, sc.eventLogCodec, coresPerExecutor)
8888
client = new AppClient(sc.env.actorSystem, masters, appDesc, this, conf)
8989
client.start()
90-
9190
waitForRegistration()
9291
}
9392

docs/configuration.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,17 @@ Apart from these, the following properties are also available, and may be useful
723723
this duration will be cleared as well.
724724
</td>
725725
</tr>
726+
<tr>
727+
<td><code>spark.executor.cores</code></td>
728+
<td>1 in YARN mode, all the available cores on the worker in standalone mode.</td>
729+
<td>
730+
The number of cores to use on each executor. For YARN and standalone mode only.
731+
732+
In standalone mode, setting this parameter allows an application to run multiple executors on
733+
the same worker, provided that there are enough cores on that worker. Otherwise, only one
734+
executor per application will run on each worker.
735+
</td>
736+
</tr>
726737
<tr>
727738
<td><code>spark.default.parallelism</code></td>
728739
<td>

0 commit comments

Comments
 (0)