-
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 29 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 |
|---|---|---|
|
|
@@ -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, eq => meq} | ||
| import org.mockito.Mockito._ | ||
| import org.mockito.invocation.InvocationOnMock | ||
|
|
||
| import org.apache.spark._ | ||
|
|
@@ -68,17 +68,27 @@ 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 | ||
| var numSingleHostInvocation = 0 | ||
|
|
||
| def cleanUp() { | ||
| hostToRack.clear() | ||
| numBatchInvocation = 0 | ||
| numSingleHostInvocation = 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 | ||
| } else if (hosts.length == 1) { | ||
| numSingleHostInvocation += 1 | ||
| } | ||
| hosts.map(hostToRack.get(_)) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -98,6 +108,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) | ||
| } | ||
|
|
@@ -143,8 +156,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) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1311,7 +1325,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( | ||
| (invocationOnMock: InvocationOnMock) => { | ||
| val task: Int = invocationOnMock.getArgument(0) | ||
| assert(taskSetManager.taskSetBlacklistHelperOpt.get. | ||
|
|
@@ -1323,7 +1337,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(meq(0), meq(false)) | ||
|
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. You can actually omit the |
||
| } | ||
|
|
||
| test("SPARK-21563 context's added jars shouldn't change mid-TaskSet") { | ||
|
|
@@ -1595,4 +1609,50 @@ 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, 0L) | ||
| .set(config.LOCALITY_WAIT_RACK, 1L) | ||
| 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 | ||
| // make sure we only do one rack resolution call, for the entire batch of hosts, as this | ||
| // can be expensive. The FakeTaskScheduler calls rack resolution more than the real one | ||
| // -- that is outside of the scope of this test, we just want to check the task set manager. | ||
| FakeRackUtil.numBatchInvocation = 0 | ||
| FakeRackUtil.numSingleHostInvocation = 0 | ||
| val manager = new TaskSetManager(sched, taskSet, MAX_TASK_FAILURES, clock = clock) | ||
| assert(FakeRackUtil.numBatchInvocation === 1) | ||
| assert(FakeRackUtil.numSingleHostInvocation === 0) | ||
| // 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) | ||
| } | ||
| } | ||
| // check no more expensive calls to the rack resolution. manager.resourceOffer() will call | ||
| // the single-host resolution, but the real rack resolution would have cached all hosts | ||
| // by that point. | ||
| 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 | ||
| } | ||
|
|
||
| } | ||
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.
There's an implicit assumption here that
map.keySetandmap.valuesiterate in the same order. I'm not sure if that's guaranteed, and at the same time I don't see why that wouldn't be the case, but just wanted to point this out.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.
I had the exact same thought when I reached that line.
Even thought about a possible solutions:
racks.entrySetand generating the keys and values from this entry set (as in entry set the key and value is bound together the ordering will be fixed; even with one iteration the key and the value can be generated).racks.asScala.unzip.Both solutions has some performance cost.
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.
Correction for last sentence: the first solution can be done without performance cost but probably the code will be a bit less elegant.
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.
great point, I have updated this to use
unzip