-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-13704][CORE][YARN] Reduce rack resolution time #24245
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 24 commits
56b7f70
342a6fd
1e1180c
e521bcf
8514b3e
63666ad
69b62e4
6246e57
7e4c729
d4a7cde
d3e1592
47add6f
e8ab99e
11fdd41
ade4caa
c9dace8
e2faee6
aca97f1
75d22ed
011b4c0
2876ad3
06a6264
92ef335
fa7daa4
99ea54b
6bcbc88
e598984
f5efc74
cd97b62
ad63e15
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 |
|---|---|---|
|
|
@@ -186,8 +186,23 @@ private[spark] class TaskSetManager( | |
|
|
||
| // Add all our tasks to the pending lists. We do this in reverse order | ||
| // of task index so that tasks with low indices get launched first. | ||
| for (i <- (0 until numTasks).reverse) { | ||
| addPendingTask(i) | ||
| addPendingTasks() | ||
|
|
||
| private def addPendingTasks(): Unit = { | ||
| val (_, duration) = Utils.timeTakenMs { | ||
| for (i <- (0 until numTasks).reverse) { | ||
| addPendingTask(i, resolveRacks = false) | ||
| } | ||
| // Resolve the rack for each host. This can be slow, so de-dupe the list of hosts, | ||
| // and assign the rack to all relevant task indices. | ||
| val racks = sched.getRacksForHosts(pendingTasksForHost.keySet.toSeq) | ||
|
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. There's an implicit assumption here that
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 had the exact same thought when I reached that line.
Both solutions has some performance cost.
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. Correction for last sentence: the first solution can be done without performance cost but probably the code will be a bit less elegant.
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. great point, I have updated this to use |
||
| racks.zip(pendingTasksForHost.values).foreach { | ||
| case (Some(rack), indices) => | ||
| pendingTasksForRack.getOrElseUpdate(rack, new ArrayBuffer) ++= indices | ||
| case (None, _) => // no rack, nothing to do | ||
| } | ||
| } | ||
| logDebug(s"Adding pending tasks took $duration ms") | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -214,7 +229,9 @@ private[spark] class TaskSetManager( | |
| private[scheduler] var emittedTaskSizeWarning = false | ||
|
|
||
| /** Add a task to all the pending-task lists that it should be on. */ | ||
| private[spark] def addPendingTask(index: Int) { | ||
| private[spark] def addPendingTask( | ||
| index: Int, | ||
| resolveRacks: Boolean = true): Unit = { | ||
| for (loc <- tasks(index).preferredLocations) { | ||
| loc match { | ||
| case e: ExecutorCacheTaskLocation => | ||
|
|
@@ -234,8 +251,11 @@ private[spark] class TaskSetManager( | |
| case _ => | ||
| } | ||
| pendingTasksForHost.getOrElseUpdate(loc.host, new ArrayBuffer) += index | ||
| for (rack <- sched.getRackForHost(loc.host)) { | ||
| pendingTasksForRack.getOrElseUpdate(rack, new ArrayBuffer) += index | ||
|
|
||
| if (resolveRacks) { | ||
| sched.getRackForHost(loc.host).foreach { rack => | ||
| pendingTasksForRack.getOrElseUpdate(rack, new ArrayBuffer) += index | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -331,7 +351,7 @@ private[spark] class TaskSetManager( | |
| val executors = prefs.flatMap(_ match { | ||
| case e: ExecutorCacheTaskLocation => Some(e.executorId) | ||
| case _ => None | ||
| }); | ||
| }) | ||
| if (executors.contains(execId)) { | ||
| speculatableTasks -= index | ||
| return Some((index, TaskLocality.PROCESS_LOCAL)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,8 @@ import java.util.{Properties, Random} | |
| import scala.collection.mutable | ||
| import scala.collection.mutable.ArrayBuffer | ||
|
|
||
| import org.mockito.ArgumentMatchers.{any, anyInt, anyString} | ||
| import org.mockito.Mockito.{mock, never, spy, times, verify, when} | ||
| import org.mockito.ArgumentMatchers.{any, anyBoolean, anyInt, anyString} | ||
| import org.mockito.Mockito._ | ||
| import org.mockito.invocation.InvocationOnMock | ||
| import org.mockito.stubbing.Answer | ||
|
|
||
|
|
@@ -69,17 +69,23 @@ class FakeDAGScheduler(sc: SparkContext, taskScheduler: FakeTaskScheduler) | |
| // Get the rack for a given host | ||
| object FakeRackUtil { | ||
| private val hostToRack = new mutable.HashMap[String, String]() | ||
| var numBatchInvocation = 0 | ||
|
|
||
| def cleanUp() { | ||
| hostToRack.clear() | ||
| numBatchInvocation = 0 | ||
| } | ||
|
|
||
| def assignHostToRack(host: String, rack: String) { | ||
| hostToRack(host) = rack | ||
| } | ||
|
|
||
| def getRackForHost(host: String): Option[String] = { | ||
| hostToRack.get(host) | ||
| def getRacksForHosts(hosts: Seq[String]): Seq[Option[String]] = { | ||
| assert(hosts.toSet.size == hosts.size) // no dups in hosts | ||
| if (hosts.nonEmpty && hosts.length != 1) { | ||
| numBatchInvocation += 1 | ||
| } | ||
| hosts.map(hostToRack.get(_)) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -99,6 +105,9 @@ class FakeTaskScheduler(sc: SparkContext, liveExecutors: (String, String)* /* ex | |
| val speculativeTasks = new ArrayBuffer[Int] | ||
|
|
||
| val executors = new mutable.HashMap[String, String] | ||
|
|
||
| // this must be initialized before addExecutor | ||
| override val defaultRackValue: Option[String] = Some("default") | ||
| for ((execId, host) <- liveExecutors) { | ||
| addExecutor(execId, host) | ||
| } | ||
|
|
@@ -144,8 +153,9 @@ class FakeTaskScheduler(sc: SparkContext, liveExecutors: (String, String)* /* ex | |
| } | ||
| } | ||
|
|
||
|
|
||
| override def getRackForHost(value: String): Option[String] = FakeRackUtil.getRackForHost(value) | ||
| override def getRacksForHosts(hosts: Seq[String]): Seq[Option[String]] = { | ||
| FakeRackUtil.getRacksForHosts(hosts) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1316,7 +1326,7 @@ class TaskSetManagerSuite extends SparkFunSuite with LocalSparkContext with Logg | |
| val taskDesc = taskSetManagerSpy.resourceOffer(exec, host, TaskLocality.ANY) | ||
|
|
||
| // Assert the task has been black listed on the executor it was last executed on. | ||
| when(taskSetManagerSpy.addPendingTask(anyInt())).thenAnswer( | ||
| when(taskSetManagerSpy.addPendingTask(anyInt(), anyBoolean())).thenAnswer( | ||
| new Answer[Unit] { | ||
| override def answer(invocationOnMock: InvocationOnMock): Unit = { | ||
| val task: Int = invocationOnMock.getArgument(0) | ||
|
|
@@ -1330,7 +1340,7 @@ class TaskSetManagerSuite extends SparkFunSuite with LocalSparkContext with Logg | |
| val e = new ExceptionFailure("a", "b", Array(), "c", None) | ||
| taskSetManagerSpy.handleFailedTask(taskDesc.get.taskId, TaskState.FAILED, e) | ||
|
|
||
| verify(taskSetManagerSpy, times(1)).addPendingTask(anyInt()) | ||
| verify(taskSetManagerSpy, times(1)).addPendingTask(anyInt(), anyBoolean()) | ||
|
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. We can match here for exact arguments: verify(taskSetManagerSpy, times(1))
.addPendingTask(argEq(0), argEq(false))Assuming the import:
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. good point. while I like your naming more, the standard we've used in spark is to rename it to |
||
| } | ||
|
|
||
| test("SPARK-21563 context's added jars shouldn't change mid-TaskSet") { | ||
|
|
@@ -1602,4 +1612,40 @@ class TaskSetManagerSuite extends SparkFunSuite with LocalSparkContext with Logg | |
| verify(sched.dagScheduler).taskEnded(manager.tasks(3), Success, result.value(), | ||
| result.accumUpdates, info3) | ||
| } | ||
|
|
||
| test("SPARK-13704 Rack Resolution is done with a batch of de-duped hosts") { | ||
| val conf = new SparkConf() | ||
| .set(config.LOCALITY_WAIT.key, "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. Nit: |
||
| .set(config.LOCALITY_WAIT_RACK.key, "1s") | ||
| sc = new SparkContext("local", "test", conf) | ||
| // Create a cluster with 20 racks, with hosts spread out among them | ||
| val execAndHost = (0 to 199).map { i => | ||
| FakeRackUtil.assignHostToRack("host" + i, "rack" + (i % 20)) | ||
| ("exec" + i, "host" + i) | ||
| } | ||
| sched = new FakeTaskScheduler(sc, execAndHost: _*) | ||
| // make a taskset with preferred locations on the first 100 hosts in our cluster | ||
| val locations = new ArrayBuffer[Seq[TaskLocation]]() | ||
| for (i <- 0 to 99) { | ||
| locations += Seq(TaskLocation("host" + i)) | ||
| } | ||
| val taskSet = FakeTask.createTaskSet(100, locations: _*) | ||
| val clock = new ManualClock | ||
| val manager = new TaskSetManager(sched, taskSet, MAX_TASK_FAILURES, clock = clock) | ||
| // with rack locality, reject an offer on a host with an unknown rack | ||
| assert(manager.resourceOffer("otherExec", "otherHost", TaskLocality.RACK_LOCAL).isEmpty) | ||
| (0 until 20).foreach { rackIdx => | ||
| (0 until 5).foreach { offerIdx => | ||
| // if we offer hosts which are not in preferred locations, | ||
| // we'll reject them at NODE_LOCAL level, | ||
| // but accept them at RACK_LOCAL level if they're on OK racks | ||
| val hostIdx = 100 + rackIdx | ||
| assert(manager.resourceOffer("exec" + hostIdx, "host" + hostIdx, TaskLocality.NODE_LOCAL) | ||
| .isEmpty) | ||
| assert(manager.resourceOffer("exec" + hostIdx, "host" + hostIdx, TaskLocality.RACK_LOCAL) | ||
| .isDefined) | ||
| } | ||
| } | ||
| assert(FakeRackUtil.numBatchInvocation === 1) | ||
|
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. As I assume we are thinking about any potential future code which would use the |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,24 +17,105 @@ | |
|
|
||
| package org.apache.spark.deploy.yarn | ||
|
|
||
| import scala.collection.JavaConverters._ | ||
| import scala.collection.mutable.ArrayBuffer | ||
|
|
||
| import com.google.common.base.Strings | ||
| import org.apache.hadoop.conf.Configuration | ||
| import org.apache.hadoop.fs.CommonConfigurationKeysPublic | ||
| import org.apache.hadoop.net._ | ||
| import org.apache.hadoop.util.ReflectionUtils | ||
| import org.apache.hadoop.yarn.util.RackResolver | ||
| import org.apache.log4j.{Level, Logger} | ||
|
|
||
| import org.apache.spark.internal.Logging | ||
|
|
||
| /** | ||
| * Wrapper around YARN's [[RackResolver]]. This allows Spark tests to easily override the | ||
| * Re-implement YARN's [[RackResolver]]. This allows Spark tests to easily override the | ||
| * default behavior, since YARN's class self-initializes the first time it's called, and | ||
| * future calls all use the initial configuration. | ||
| */ | ||
| private[yarn] class SparkRackResolver { | ||
| private[spark] class SparkRackResolver(conf: Configuration) extends Logging { | ||
|
|
||
| // RackResolver logs an INFO message whenever it resolves a rack, which is way too often. | ||
| if (Logger.getLogger(classOf[RackResolver]).getLevel == null) { | ||
| Logger.getLogger(classOf[RackResolver]).setLevel(Level.WARN) | ||
| } | ||
|
|
||
| def resolve(conf: Configuration, hostName: String): String = { | ||
| RackResolver.resolve(conf, hostName).getNetworkLocation() | ||
| private val dnsToSwitchMapping: DNSToSwitchMapping = { | ||
| val dnsToSwitchMappingClass = | ||
| conf.getClass(CommonConfigurationKeysPublic.NET_TOPOLOGY_NODE_SWITCH_MAPPING_IMPL_KEY, | ||
| classOf[ScriptBasedMapping], classOf[DNSToSwitchMapping]) | ||
| ReflectionUtils.newInstance(dnsToSwitchMappingClass, conf) | ||
| .asInstanceOf[DNSToSwitchMapping] match { | ||
| case c: CachedDNSToSwitchMapping => c | ||
| case o => new CachedDNSToSwitchMapping(o) | ||
| } | ||
| } | ||
|
|
||
| def resolve(hostName: String): String = { | ||
| coreResolve(Seq(hostName)).head.getNetworkLocation | ||
| } | ||
|
|
||
| /** | ||
| * Added in SPARK-13704. | ||
| * This should be changed to `RackResolver.resolve(conf, hostNames)` | ||
| * in hadoop releases with YARN-9332. | ||
| */ | ||
| def resolve(hostNames: Seq[String]): Seq[Node] = { | ||
| coreResolve(hostNames) | ||
| } | ||
|
|
||
| private def coreResolve(hostNames: Seq[String]): Seq[Node] = { | ||
| val nodes = new ArrayBuffer[Node] | ||
| // dnsToSwitchMapping is thread-safe | ||
| val rNameList = dnsToSwitchMapping.resolve(hostNames.toList.asJava).asScala | ||
| if (rNameList == null || rNameList.isEmpty) { | ||
| hostNames.foreach(nodes += new NodeBase(_, NetworkTopology.DEFAULT_RACK)) | ||
| logInfo(s"Got an error when resolving hostNames. " + | ||
| s"Falling back to ${NetworkTopology.DEFAULT_RACK} for all") | ||
| } else { | ||
| for ((hostName, rName) <- hostNames.zip(rNameList)) { | ||
| if (Strings.isNullOrEmpty(rName)) { | ||
| nodes += new NodeBase(hostName, NetworkTopology.DEFAULT_RACK) | ||
| logDebug(s"Could not resolve $hostName. " + | ||
| s"Falling back to ${NetworkTopology.DEFAULT_RACK}") | ||
| } else { | ||
| nodes += new NodeBase(hostName, rName) | ||
| } | ||
| } | ||
| } | ||
| nodes.toList | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Utility to resolve the rack for hosts in an efficient manner. | ||
| * It will cache the rack for individual hosts to avoid | ||
| * repeatedly performing the same expensive lookup. | ||
| * | ||
| * Its logic refers [[org.apache.hadoop.yarn.util.RackResolver]] and enhanced. | ||
|
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. This paragraph actually refers to the code in the class, now, not the object anymore. |
||
| * This will be unnecessary in hadoop releases with YARN-9332. | ||
| * With that, we could just directly use [[org.apache.hadoop.yarn.util.RackResolver]]. | ||
| * In the meantime, this is a re-implementation for spark's use. | ||
| */ | ||
| object SparkRackResolver extends Logging { | ||
| @volatile private var instance: SparkRackResolver = _ | ||
|
|
||
| /** | ||
| * It will return the static resolver instance. If there is already an instance, the passed | ||
| * conf is entirely ignored. If there is not a shared instance, it will create one with the | ||
| * given conf. | ||
|
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. Should we explain how to instantiate a separate resolver with a separate config here?
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. its kinda obvious, no? |
||
| */ | ||
| def get(conf: Configuration): SparkRackResolver = { | ||
| if (instance == null) { | ||
| synchronized { | ||
| if (instance == null) { | ||
| instance = new SparkRackResolver(conf) | ||
| } | ||
| } | ||
| } | ||
| instance | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,7 +83,7 @@ private[spark] class YarnRMClient extends Logging { | |
| localResources: Map[String, LocalResource]): YarnAllocator = { | ||
| require(registered, "Must register AM before creating allocator.") | ||
| new YarnAllocator(driverUrl, driverRef, conf, sparkConf, amClient, appAttemptId, securityMgr, | ||
| localResources, new SparkRackResolver()) | ||
| localResources, new SparkRackResolver(conf)) | ||
|
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. Do you want the shared instance here instead?
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. oh good point, i do want the shared instance, to use a shared |
||
| } | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: rename hosts to host