-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[WIP][SPARK-18886][CORE] Make locality wait time be the time since a TSM's available slots were fully utilized #26696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
06ca01f
624c417
168ab30
f9a85f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,4 +119,29 @@ private[spark] class Pool( | |
| parent.decreaseRunningTasks(taskNum) | ||
| } | ||
| } | ||
|
|
||
| override def updateAvailableSlots(numSlots: Float): Unit = { | ||
| schedulingMode match { | ||
| case SchedulingMode.FAIR => | ||
| val usableWeights = schedulableQueue.asScala | ||
| .map(s => if (s.getSortedTaskSetQueue.nonEmpty) (s, s.weight) else (s, 0)) | ||
| val totalWeights = usableWeights.map(_._2).sum | ||
| usableWeights.foreach { case (schedulable, usableWeight) => | ||
| schedulable.updateAvailableSlots( | ||
| Math.max(numSlots * usableWeight / totalWeights, schedulable.minShare)) | ||
| } | ||
| // for FIFO, allocate all slots to the first taskset in the queue | ||
| case SchedulingMode.FIFO => | ||
| val sortedSchedulableQueue = | ||
| schedulableQueue.asScala.toSeq.sortWith(taskSetSchedulingAlgorithm.comparator) | ||
| var isFirst = true | ||
| for (schedulable <- sortedSchedulableQueue) { | ||
| schedulable.updateAvailableSlots(if (isFirst) numSlots else 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so I think this is only first because only one rootPool with FIFO, maybe add comment here, I assume not really needed since sortedScheduleQueeu should just have 1 thing, but here just in case.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I do think a comment is necessary here to explain this. If I understood right, the idea here is that all other tasksets follow the old logic on resetting locality level -- for every single task that is scheduled, the locality timer gets reset. But for the first taskset, it only resets when the taskset is using all slots
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @squito is correct here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this makes sense. by default we run FIFO and we can have multiple tasksets at the same time with a single Job. so if we have plenty of slots to fit both tasksets in we could end up with same broken behavior we have now in the second taskset. Its perhaps slightly better as when first tasket finishes then it does go more quickly but in the mean time you could have wasted a lot of time.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for pointing this out!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so something like that is definitely better. I think you have to make sure that n tasks is the number of tasks running or pending, because a taskSet could have 10000 tasks and if 9999 of them are finished it really only needs 1 and the rest should go to the other tasksets. I'm not sure that is easily accessible right now from this location. It kind of feels to me that FIFO should just have some boolean that is passed to the TSM to tell if it there are remaining slots. with FIFO, if I have multiple tasks set, they do go in priority order, but if the higher priority one passes slots up there is nothing keeping the other tasksets from using it. So I don't know why we should make the locality preference skewed. its definitely not any worse then we have now and I'm not sure how you would really get away from this without going to something more like the node delay vs task delay. The fair scheduler side I need to think about more. I think it has similar issue with the totalSlots vs how many tasks it actually has left to run. For instance you have 2 pools with 1 taskset each, both with equal fair shares, the first one has run enough where it only has 2 tasks left so do you send the rest of the free slots to the second tsm so it can use more of the cluster. I would generally say yes and I think that is the way it acts now without this patch. The shares for the Fair scheduler just appear to sort the tasksets and they are visited in that order but there doesn't appear to be any real hard enforcement of the limits. |
||
| isFirst = false | ||
| } | ||
| case _ => | ||
| val msg = s"Unsupported scheduling mode: $schedulingMode. Use FAIR or FIFO instead." | ||
| throw new IllegalArgumentException(msg) | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,7 @@ import org.apache.spark.rpc.RpcEndpoint | |
| import org.apache.spark.scheduler.SchedulingMode.SchedulingMode | ||
| import org.apache.spark.scheduler.TaskLocality.TaskLocality | ||
| import org.apache.spark.storage.BlockManagerId | ||
| import org.apache.spark.util.{AccumulatorV2, SystemClock, ThreadUtils, Utils} | ||
| import org.apache.spark.util.{AccumulatorV2, Clock, SystemClock, ThreadUtils, Utils} | ||
|
|
||
| /** | ||
| * Schedules tasks for multiple types of clusters by acting through a SchedulerBackend. | ||
|
|
@@ -61,7 +61,8 @@ import org.apache.spark.util.{AccumulatorV2, SystemClock, ThreadUtils, Utils} | |
| private[spark] class TaskSchedulerImpl( | ||
| val sc: SparkContext, | ||
| val maxTaskFailures: Int, | ||
| isLocal: Boolean = false) | ||
| isLocal: Boolean = false, | ||
| clock: Clock = new SystemClock) | ||
| extends TaskScheduler with Logging { | ||
|
|
||
| import TaskSchedulerImpl._ | ||
|
|
@@ -125,10 +126,12 @@ private[spark] class TaskSchedulerImpl( | |
|
|
||
| protected val hostsByRack = new HashMap[String, HashSet[String]] | ||
|
|
||
| protected val executorIdToCores = new HashMap[String, Int] | ||
| protected var totalSlots = 0 | ||
|
|
||
| protected val executorIdToHost = new HashMap[String, String] | ||
|
|
||
| private val abortTimer = new Timer(true) | ||
| private val clock = new SystemClock | ||
| // Exposed for testing | ||
| val unschedulableTaskSetToExpiryTime = new HashMap[TaskSetManager, Long] | ||
|
|
||
|
|
@@ -403,6 +406,8 @@ private[spark] class TaskSchedulerImpl( | |
| if (!executorIdToRunningTaskIds.contains(o.executorId)) { | ||
| hostToExecutors(o.host) += o.executorId | ||
| executorAdded(o.executorId, o.host) | ||
| executorIdToCores(o.executorId) = o.cores | ||
| totalSlots += o.cores / CPUS_PER_TASK | ||
| executorIdToHost(o.executorId) = o.host | ||
| executorIdToRunningTaskIds(o.executorId) = HashSet[Long]() | ||
| newExecAvail = true | ||
|
|
@@ -439,6 +444,8 @@ private[spark] class TaskSchedulerImpl( | |
| } | ||
| } | ||
|
|
||
| rootPool.updateAvailableSlots(totalSlots) | ||
|
|
||
| // Take each TaskSet in our scheduling order, and then offer it each node in increasing order | ||
| // of locality levels so that it gets a chance to launch local tasks on all of them. | ||
| // NOTE: the preferredLocality order: PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY | ||
|
|
@@ -808,6 +815,7 @@ private[spark] class TaskSchedulerImpl( | |
| // happen below in the rootPool.executorLost() call. | ||
| taskIds.foreach(cleanupTaskState) | ||
| } | ||
| executorIdToCores.remove(executorId).foreach(totalSlots -= _ / CPUS_PER_TASK) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that rather than calling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updateAvailableSlots is dependent on both the total number of slots + what tasksets/pools are present. Will take a look to see how feasible doing the update incrementally is. |
||
|
|
||
| val host = executorIdToHost(executorId) | ||
| val execs = hostToExecutors.getOrElse(host, new HashSet) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,7 @@ private[spark] class TaskSetManager( | |
| .map { case (t, idx) => t.partitionId -> idx }.toMap | ||
| val numTasks = tasks.length | ||
| val copiesRunning = new Array[Int](numTasks) | ||
| var numAvailableSlot: Float = 0 | ||
|
|
||
| val speculationEnabled = conf.get(SPECULATION_ENABLED) | ||
| // Quantile of tasks at which to start speculation | ||
|
|
@@ -200,10 +201,13 @@ private[spark] class TaskSetManager( | |
| private[scheduler] var localityWaits = myLocalityLevels.map(getLocalityWait) | ||
|
|
||
| // Delay scheduling variables: we keep track of our current locality level and the time we | ||
| // last launched a task at that level, and move up a level when localityWaits[curLevel] expires. | ||
| // last launched a task at that level and were also fully utilizing all slots | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: period at end of sentence. I'd also reference here that the idea of "all slots" is dependent on the scheduling mode and more details are in |
||
| // Move up a level when localityWaits[curLevel] expires. | ||
| // We then move down if we manage to launch a "more local" task. | ||
| private var currentLocalityIndex = 0 // Index of our current locality level in validLocalityLevels | ||
| private var lastLaunchTime = clock.getTimeMillis() // Time we last launched a task at this level | ||
| // Time we last launched a task at this level | ||
| // and number of running tasks was greater than or equal to numAvailableSlot | ||
| private var lastFullUtilizationTime = clock.getTimeMillis() | ||
|
|
||
| override def schedulableQueue: ConcurrentLinkedQueue[Schedulable] = null | ||
|
|
||
|
|
@@ -410,11 +414,13 @@ private[spark] class TaskSetManager( | |
| execId, host, taskLocality, speculative) | ||
| taskInfos(taskId) = info | ||
| taskAttempts(index) = info :: taskAttempts(index) | ||
| // Update our locality level for delay scheduling | ||
| // Update our locality level for delay scheduling if all allocated slots are being utilized | ||
| // NO_PREF will not affect the variables related to delay scheduling | ||
| if (maxLocality != TaskLocality.NO_PREF) { | ||
| // Adding 1 to runningTasks, since runningTasks doesn't include this task yet | ||
| val utilizingAllSlots = runningTasks + 1 >= numAvailableSlot | ||
| if (maxLocality != TaskLocality.NO_PREF && utilizingAllSlots) { | ||
| currentLocalityIndex = getLocalityIndex(taskLocality) | ||
| lastLaunchTime = curTime | ||
| lastFullUtilizationTime = curTime | ||
| } | ||
| // Serialize and return the task | ||
| val serializedTask: ByteBuffer = try { | ||
|
|
@@ -534,14 +540,14 @@ private[spark] class TaskSetManager( | |
| // This is a performance optimization: if there are no more tasks that can | ||
| // be scheduled at a particular locality level, there is no point in waiting | ||
| // for the locality wait timeout (SPARK-4939). | ||
| lastLaunchTime = curTime | ||
| lastFullUtilizationTime = curTime | ||
| logDebug(s"No tasks for locality level ${myLocalityLevels(currentLocalityIndex)}, " + | ||
| s"so moving to locality level ${myLocalityLevels(currentLocalityIndex + 1)}") | ||
| currentLocalityIndex += 1 | ||
| } else if (curTime - lastLaunchTime >= localityWaits(currentLocalityIndex)) { | ||
| // Jump to the next locality level, and reset lastLaunchTime so that the next locality | ||
| // wait timer doesn't immediately expire | ||
| lastLaunchTime += localityWaits(currentLocalityIndex) | ||
| } else if (curTime - lastFullUtilizationTime >= localityWaits(currentLocalityIndex)) { | ||
| // Jump to the next locality level, and reset lastFullUtilizationTime | ||
| // so that the next locality wait timer doesn't immediately expire | ||
| lastFullUtilizationTime += localityWaits(currentLocalityIndex) | ||
| logDebug(s"Moving to ${myLocalityLevels(currentLocalityIndex + 1)} after waiting for " + | ||
| s"${localityWaits(currentLocalityIndex)}ms") | ||
| currentLocalityIndex += 1 | ||
|
|
@@ -1054,6 +1060,10 @@ private[spark] class TaskSetManager( | |
| def executorAdded(): Unit = { | ||
| recomputeLocality() | ||
| } | ||
|
|
||
| override def updateAvailableSlots(numSlots: Float): Unit = { | ||
| numAvailableSlot = numSlots | ||
| } | ||
| } | ||
|
|
||
| private[spark] object TaskSetManager { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,68 @@ class PoolSuite extends SparkFunSuite with LocalSparkContext { | |
| assert(nextTaskSetToSchedule.get.stageId === expectedStageId) | ||
| } | ||
|
|
||
| test("validate FIFO slot distributions") { | ||
|
viirya marked this conversation as resolved.
|
||
| sc = new SparkContext(LOCAL, APP_NAME) | ||
| val taskScheduler = new TaskSchedulerImpl(sc) | ||
|
|
||
| val rootPool = new Pool("", FIFO, 0, 0) | ||
| val schedulableBuilder = new FIFOSchedulableBuilder(rootPool) | ||
|
|
||
| val taskSetManager0 = createTaskSetManager(0, 2, taskScheduler) | ||
| val taskSetManager1 = createTaskSetManager(1, 2, taskScheduler) | ||
| val taskSetManager2 = createTaskSetManager(2, 2, taskScheduler) | ||
| schedulableBuilder.addTaskSetManager(taskSetManager0, null) | ||
| schedulableBuilder.addTaskSetManager(taskSetManager1, null) | ||
| schedulableBuilder.addTaskSetManager(taskSetManager2, null) | ||
|
|
||
| rootPool.updateAvailableSlots(9) | ||
| assert(taskSetManager0.numAvailableSlot === 9) | ||
| assert(taskSetManager1.numAvailableSlot === 0) | ||
| assert(taskSetManager2.numAvailableSlot === 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also check its right after you remove tasksets and add more. |
||
| } | ||
|
|
||
| test("validate FAIR slot distributions") { | ||
| val xmlPath = getClass.getClassLoader.getResource("fairscheduler-with-valid-data.xml").getFile() | ||
| val conf = new SparkConf().set(SCHEDULER_ALLOCATION_FILE, xmlPath) | ||
| sc = new SparkContext(LOCAL, APP_NAME, conf) | ||
| val taskScheduler = new TaskSchedulerImpl(sc) | ||
|
|
||
| val rootPool = new Pool("", FAIR, 0, 0) | ||
| val schedulableBuilder = new FairSchedulableBuilder(rootPool, sc.conf) | ||
| schedulableBuilder.buildPools() | ||
|
|
||
| val taskSetManager0 = createTaskSetManager(0, 2, taskScheduler) | ||
| val taskSetManager1 = createTaskSetManager(1, 2, taskScheduler) | ||
| sc.setLocalProperty(SparkContext.SPARK_SCHEDULER_POOL, "pool1") | ||
| var localProperties = sc.getLocalProperties | ||
| schedulableBuilder.addTaskSetManager(taskSetManager0, localProperties) | ||
| schedulableBuilder.addTaskSetManager(taskSetManager1, localProperties) | ||
|
|
||
| sc.setLocalProperty(SparkContext.SPARK_SCHEDULER_POOL, "pool2") | ||
| localProperties = sc.getLocalProperties | ||
| val taskSetManager2 = createTaskSetManager(2, 2, taskScheduler) | ||
| val taskSetManager3 = createTaskSetManager(3, 2, taskScheduler) | ||
| schedulableBuilder.addTaskSetManager(taskSetManager2, localProperties) | ||
| schedulableBuilder.addTaskSetManager(taskSetManager3, localProperties) | ||
|
|
||
|
|
||
| rootPool.updateAvailableSlots(12) | ||
| // pool 1 weight is 1, pool 2 weight is 2, so they get 4 and 8 slots respectively | ||
| // since pool 1 is FIFO TSM 0 gets all 4 slots of pool 1 | ||
| // since pool 2 is FAIR TSM 3 and 4 each get half (4) of pool 2's 8 slots | ||
| assert(taskSetManager0.numAvailableSlot === 4) | ||
| assert(taskSetManager1.numAvailableSlot === 0) | ||
| assert(taskSetManager2.numAvailableSlot === 4) | ||
| assert(taskSetManager3.numAvailableSlot === 4) | ||
|
|
||
| rootPool.updateAvailableSlots(6) | ||
| // similar to above but pool 1 has 3 slots due to having minShare of 3 | ||
| assert(taskSetManager0.numAvailableSlot === 3) | ||
| assert(taskSetManager1.numAvailableSlot === 0) | ||
| assert(taskSetManager2.numAvailableSlot === 2) | ||
| assert(taskSetManager3.numAvailableSlot === 2) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, expand this to include removing and adding tasksets |
||
| } | ||
|
|
||
| test("FIFO Scheduler Test") { | ||
| sc = new SparkContext(LOCAL, APP_NAME) | ||
| val taskScheduler = new TaskSchedulerImpl(sc) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -195,6 +195,63 @@ class TaskSchedulerImplSuite extends SparkFunSuite with LocalSparkContext with B | |
| assert(!failedTaskSet) | ||
| } | ||
|
|
||
| test("executors should not sit idle for too long") { | ||
| val LOCALITY_WAIT_MS = 3000 | ||
| val clock = new ManualClock | ||
| val conf = new SparkConf() | ||
| sc = new SparkContext("local", "TaskSchedulerImplSuite", conf) | ||
| val taskScheduler = new TaskSchedulerImpl(sc, | ||
| sc.conf.get(config.TASK_MAX_FAILURES), | ||
| clock = clock) { | ||
| override def createTaskSetManager(taskSet: TaskSet, maxTaskFailures: Int): TaskSetManager = { | ||
| new TaskSetManager(this, taskSet, maxTaskFailures, blacklistTrackerOpt, clock) | ||
| } | ||
| override def shuffleOffers(offers: IndexedSeq[WorkerOffer]): IndexedSeq[WorkerOffer] = { | ||
| // Don't shuffle the offers around for this test. Instead, we'll just pass in all | ||
| // the permutations we care about directly. | ||
| offers | ||
| } | ||
| } | ||
| // Need to initialize a DAGScheduler for the taskScheduler to use for callbacks. | ||
| new DAGScheduler(sc, taskScheduler) { | ||
| override def taskStarted(task: Task[_], taskInfo: TaskInfo): Unit = {} | ||
|
|
||
| override def executorAdded(execId: String, host: String): Unit = {} | ||
| } | ||
| taskScheduler.initialize(new FakeSchedulerBackend) | ||
| val taskSet = FakeTask.createTaskSet(5, 1, 1, | ||
| Seq(TaskLocation("host1", "exec1")), | ||
| Seq(TaskLocation("host1", "exec1")), | ||
| Seq(TaskLocation("host1", "exec1")), | ||
| Seq(TaskLocation("host1", "exec1")), | ||
| Seq(TaskLocation("host1", "exec1")) | ||
| ) | ||
| taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1))) | ||
| taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1))) | ||
| taskScheduler.submitTasks(taskSet) | ||
|
|
||
| // First offer host2, exec2: no task should be chosen due to bad data locality | ||
| assert(taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec2", "host2", 1))) | ||
| .flatten.isEmpty) | ||
| val task0 = taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1))) | ||
| .flatten.head | ||
| assert(task0.index === 0) | ||
| taskScheduler.statusUpdate(task0.taskId, TaskState.FINISHED, ByteBuffer.allocate(0)) | ||
|
|
||
| clock.advance(LOCALITY_WAIT_MS * 2) | ||
| val task1 = taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1))) | ||
| .flatten.head | ||
| assert(task1.index === 1) | ||
| val task2 = taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec2", "host2", 1))) | ||
| .flatten.head | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. before the fix, this would fail here, with a NoSuchElementException, rather than at the assert. Would be a little bit more clear to assert its nonempty. Also a comment here explaining that this is the most important check -- even though we've just scheduled something task local, we don't delay our use of exec2. |
||
| assert(task2.index === 2) | ||
| taskScheduler.statusUpdate(task1.taskId, TaskState.FINISHED, ByteBuffer.allocate(0)) | ||
| assert(taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec1", "host1", 1))) | ||
| .flatten.head.index === 3) | ||
| assert(taskScheduler.resourceOffers(IndexedSeq(WorkerOffer("exec2", "host2", 1))) | ||
| .flatten.isEmpty) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shoudl add some tests with more executors getting added & removed. |
||
| } | ||
|
|
||
| test("Scheduler does not crash when tasks are not serializable") { | ||
| val taskCpus = 2 | ||
| val taskScheduler = setupSchedulerWithMaster( | ||
|
|
@@ -910,6 +967,7 @@ class TaskSchedulerImplSuite extends SparkFunSuite with LocalSparkContext with B | |
| test("SPARK-16106 locality levels updated if executor added to existing host") { | ||
| val taskScheduler = setupScheduler() | ||
|
|
||
| taskScheduler.resourceOffers(IndexedSeq(new WorkerOffer("executor0", "host0", 1))) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note of why this line was necessary. The current locality level (currentLocalityIndex) of a TSM is defaulted to the current "most local" level when a taskset is submitted. Before this line was added, that level was ANY as the scheduler was not aware of any resources. Locality levels are recomputed whenever a new executor is detected, but the current locality level remains the same. Previously the test was passing because the first task that began running was NODE_LOCAL, hence the locality level was set to node local, causing the second task not to run. Since this PR only resets locality level when all slots are utilized, this locality level reset not happening. |
||
| taskScheduler.submitTasks(FakeTask.createTaskSet(2, stageId = 0, stageAttemptId = 0, | ||
| (0 until 2).map { _ => Seq(TaskLocation("host0", "executor2")) }: _* | ||
| )) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.