Skip to content
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
310263c
[SPARK-24248][K8S] Use the Kubernetes API to populate an event queue …
mccheah May 18, 2018
60990f1
Fix build
mccheah May 18, 2018
f3bb80a
Dependencies
mccheah May 18, 2018
3343ba6
Adding some logging.
mccheah May 18, 2018
30b7f17
Specifically initialize things with null. More logs.
mccheah May 18, 2018
522b079
Fix scalastyle
mccheah May 18, 2018
600e25f
Actually create the pods
mccheah May 18, 2018
931529a
Fix build
mccheah May 18, 2018
9e5abfb
Request only one pod at a time. Use logNonFatalError to log exceptions.
mccheah May 21, 2018
2156a20
Initial tests
mccheah May 21, 2018
aabc187
Don't use GNU Trove
mccheah May 21, 2018
ee0d196
Add another test.
mccheah May 22, 2018
c2b9733
Fix dependencies
mccheah May 22, 2018
caffe23
More tests
mccheah May 22, 2018
ca3fdb3
Publish pod updates to the pod allocator and lifecycle manager separa…
mccheah May 23, 2018
79ebaf3
Ensure we make a request round immediately
mccheah May 24, 2018
fadbe9f
Remove a comment
mccheah May 24, 2018
4f58393
Fix compilation
mccheah May 24, 2018
2a2374c
Don't use tabs
mccheah May 24, 2018
5850439
Use PublishSubject instead of a LinkedBlockingQueue at all
mccheah May 24, 2018
d4cf40f
Add tests. Adjust observable concurrency.
mccheah May 24, 2018
c398ebb
Add more tests.
mccheah May 25, 2018
45a02de
Minor style
mccheah May 25, 2018
a8a3539
Minor style
mccheah May 25, 2018
4a49677
Address comments.
mccheah May 25, 2018
57ea5dd
Address more comments
mccheah May 25, 2018
b30ed39
Address comments.
mccheah May 29, 2018
5b9c00f
Close k8s client
mccheah May 30, 2018
260d82c
Addressed comments.
mccheah May 30, 2018
bd03451
Addressed comments.
mccheah Jun 1, 2018
f294dca
Small style
mccheah Jun 4, 2018
7bf49ba
More small style tweaks
mccheah Jun 4, 2018
b5c0fbf
Remove unnecessary parens
mccheah Jun 4, 2018
c4b87d8
Various style fixes
mccheah Jun 4, 2018
8615c06
Process cluster snapshots instead of deltas.
mccheah Jun 8, 2018
0a205f6
Merge remote-tracking branch 'apache/master' into event-queue-driven-…
mccheah Jun 8, 2018
3b85ab5
Remove hanging comment
mccheah Jun 8, 2018
edc982b
Remove incorrect comment
mccheah Jun 8, 2018
e077c7e
Fix log message
mccheah Jun 8, 2018
a97fc5d
Whitespace
mccheah Jun 8, 2018
bd7b0d3
Clear all executors from the snapshot from newlyCreatedExecutors
mccheah Jun 8, 2018
e9d7c8f
Add a TODO for dynamic allocation
mccheah Jun 8, 2018
0fac4d5
Address comments. Pass whole buffer of snapshots.
mccheah Jun 8, 2018
e42dd4f
Merge remote-tracking branch 'apache/master' into event-queue-driven-…
mccheah Jun 8, 2018
03b1064
Merge remote-tracking branch 'apache/master' into event-queue-driven-…
mccheah Jun 8, 2018
c1b8431
Rename classes
mccheah Jun 8, 2018
108181d
Don't use RxJava
mccheah Jun 12, 2018
9e0b758
Update manifest
mccheah Jun 12, 2018
8b0a211
Remove extra parens
mccheah Jun 12, 2018
1a99dce
Address comments. Make subscriber thread pool instead of single thread
mccheah Jun 14, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dev/deps/spark-deps-hadoop-2.6
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ parquet-jackson-1.10.0.jar
protobuf-java-2.5.0.jar
py4j-0.10.7.jar
pyrolite-4.13.jar
reactive-streams-1.0.2.jar
rxjava-2.1.13.jar
scala-compiler-2.11.8.jar
scala-library-2.11.8.jar
scala-parser-combinators_2.11-1.0.4.jar
Expand Down
2 changes: 2 additions & 0 deletions dev/deps/spark-deps-hadoop-2.7
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ parquet-jackson-1.10.0.jar
protobuf-java-2.5.0.jar
py4j-0.10.7.jar
pyrolite-4.13.jar
reactive-streams-1.0.2.jar
rxjava-2.1.13.jar
scala-compiler-2.11.8.jar
scala-library-2.11.8.jar
scala-parser-combinators_2.11-1.0.4.jar
Expand Down
2 changes: 2 additions & 0 deletions dev/deps/spark-deps-hadoop-3.1
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ protobuf-java-2.5.0.jar
py4j-0.10.7.jar
pyrolite-4.13.jar
re2j-1.1.jar
reactive-streams-1.0.2.jar
rxjava-2.1.13.jar
scala-compiler-2.11.8.jar
scala-library-2.11.8.jar
scala-parser-combinators_2.11-1.0.4.jar
Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,11 @@
<artifactId>jline</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
<version>2.1.13</version>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.binary.version}</artifactId>
Expand All @@ -760,6 +765,12 @@
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why adding this to the top level pom?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always add to the top level and then in the lower level poms, we reference the dependent modules without listing their versions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm a bit concerned adding rxjava to the top level pom and to dev/deps/spark-deps-hadoop-*
can it be just a <arrow.version>0.8.0</arrow.version> thing and not a dependency?

it might possibly conflict with calling Spark from the Reactive Stream stack? @skonto what do you think?

@felixcheung felixcheung May 27, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also added dependency should have its LICENSE added under /license

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm a bit concerned adding rxjava to the top level pom and to dev/deps/spark-deps-hadoop-*
can it be just a <arrow.version>0.8.0</arrow.version> thing and not a dependency?

Unsure what you mean here - we're using rxjava itself specifically to do the event handling in this PR. See https://github.com/apache/spark/pull/21366/files#diff-ae4cd884779fb4c3db58958ab984db59R40. If we wanted an alternative approach we can build something from first principles (executor service / manual linked blocking queues) but I like the elegance that rx-java buys us here. The code we'd save building ourselves seems worthwhile.

@skonto skonto Jun 12, 2018

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Akka streams without JDK 9 depend on ReactiveX?

It depends on reactive streams library so you dont need to bring rx-Java in. @ktoso correct me if I am wrong.

In this regard we are no different from the other custom controllers in the Kubernetes ecosystem which have to handle managing large number of pods.

Have an example of a specific controller to get a better understanding?

@ktoso ktoso Jun 12, 2018

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Akka Streams does not depend on Rx of course, they both alternative implementations of Reactive Streams ( http://reactive-streams.org/ ) which have been included in JDK9 as java.util.concurrent.Flow.* and Akka also implements those, but does not require JDK9; you can use JDK8 + RS and if you use JDK9 you could use the JDK's types but it's not required. Both Akka and Rx implement the respective interfaces in RS / JDK, so can inter-op thanks to that (see the RS site for details).

Anything else I should clarify or review here? For inter-op purposes it would be good to not expose on a specific implementation but expose the reactive-streams types (org.reactivestreams.Publisher etc), but that only matters if the types are exposed. As for including dependencies in core Spark -- I would expect this to carry quite a bit of implications though don't know Spark's rules about it (ofc less dependencies == better for users, since less chances to version-clash with libraries they'd use)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types are not exposed - they're only implementation details in the Kubernetes module. Furthermore the RxJava dependency will be in the Spark distribution but is not a dependency pulled in by spark-core.

It sounds like there is some contention with the extra dependency though, so should we be considering implementing our own mechanisms from the ground up? I think the bottom line question is: can spark-kubernetes, NOT spark-core, pull in RxJava?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up just removing reactive programming entirely - the buffering is implemented manually. Please take a look.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanx @ktoso!

<groupId>org.jmock</groupId>
<artifactId>jmock-junit4</artifactId>
<scope>test</scope>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>org.scalacheck</groupId>
<artifactId>scalacheck_${scala.binary.version}</artifactId>
Expand Down
17 changes: 14 additions & 3 deletions resource-managers/kubernetes/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,27 @@
</dependency>
<!-- End of shaded deps. -->

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.8.1</version>
</dependency>

<dependency>
<groupId>io.reactivex.rxjava2</groupId>
<artifactId>rxjava</artifactId>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.8.1</version>
<groupId>org.jmock</groupId>
<artifactId>jmock-junit4</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.spark.scheduler.cluster.k8s

import java.util.concurrent.atomic.{AtomicInteger, AtomicLong}

import io.fabric8.kubernetes.api.model.{Pod, PodBuilder}
import io.fabric8.kubernetes.client.KubernetesClient
import scala.collection.mutable

import org.apache.spark.{SparkConf, SparkException}
import org.apache.spark.deploy.k8s.Config._
import org.apache.spark.deploy.k8s.Constants._
import org.apache.spark.deploy.k8s.KubernetesConf
import org.apache.spark.internal.Logging

private[spark] class ExecutorPodsAllocator(
conf: SparkConf,
executorBuilder: KubernetesExecutorBuilder,
kubernetesClient: KubernetesClient,
eventQueue: ExecutorPodsEventQueue) extends Logging {

private val EXECUTOR_ID_COUNTER = new AtomicLong(0L)

private val totalExpectedExecutors = new AtomicInteger(0)

private val podAllocationSize = conf.get(KUBERNETES_ALLOCATION_BATCH_SIZE)

private val podAllocationDelay = conf.get(KUBERNETES_ALLOCATION_BATCH_DELAY)

private val kubernetesDriverPodName = conf
.get(KUBERNETES_DRIVER_POD_NAME)
.getOrElse(throw new SparkException("Must specify the driver pod name"))

private val driverPod = kubernetesClient.pods()
.withName(kubernetesDriverPodName)
.get()

// Use sets of ids instead of counters to be able to handle duplicate events.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: hanging comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in reference to both mutable hash sets below. A suggestion on how to better format this?


// Executor IDs that have been requested from Kubernetes but are not running yet.
private val pendingExecutors = mutable.Set.empty[Long]

// We could use CoarseGrainedSchedulerBackend#totalRegisteredExecutors here for tallying the
// executors that are running. But, here we choose instead to maintain all state within this
// class from the persecptive of the k8s API. Therefore whether or not this scheduler loop
// believes an executor is running is dictated by the K8s API rather than Spark's RPC events.
// We may need to consider where these perspectives may differ and which perspective should
// take precedence.
private val runningExecutors = mutable.Set.empty[Long]

def start(applicationId: String): Unit = {
eventQueue.addSubscriber(podAllocationDelay) { updatedPods =>
processUpdatedPodEvents(applicationId, updatedPods)
}
}

def setTotalExpectedExecutors(total: Int): Unit = totalExpectedExecutors.set(total)

private def processUpdatedPodEvents(applicationId: String, updatedPods: Seq[Pod]): Unit = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're actually processing pods here right? Not the events themselves from the watch.

updatedPods.foreach { updatedPod =>
val execId = updatedPod.getMetadata.getLabels.get(SPARK_EXECUTOR_ID_LABEL).toLong
val phase = updatedPod.getStatus.getPhase.toLowerCase
phase match {
case "running" =>
pendingExecutors -= execId
runningExecutors += execId
case "failed" | "succeeded" | "error" =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see "error" listed as one of the pod phases: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
Also, might be worthwhile to put a debug log in case the phase doesn't match anything listed here.

pendingExecutors -= execId
runningExecutors -= execId
}
}

val currentRunningExecutors = runningExecutors.size
val currentTotalExpectedExecutors = totalExpectedExecutors.get
if (pendingExecutors.isEmpty && currentRunningExecutors < currentTotalExpectedExecutors) {
val numExecutorsToAllocate = math.min(
currentTotalExpectedExecutors - currentRunningExecutors, podAllocationSize)
logInfo(s"Going to request $numExecutorsToAllocate executors from Kubernetes.")
val newExecutorIds = mutable.Buffer.empty[Long]
val podsToAllocate = mutable.Buffer.empty[Pod]
for ( _ <- 0 until numExecutorsToAllocate) {
val newExecutorId = EXECUTOR_ID_COUNTER.incrementAndGet()
val executorConf = KubernetesConf.createExecutorConf(
conf,
newExecutorId.toString,
applicationId,
driverPod)
val executorPod = executorBuilder.buildFromFeatures(executorConf)
val podWithAttachedContainer = new PodBuilder(executorPod.pod)
.editOrNewSpec()
.addToContainers(executorPod.container)
.endSpec()
.build()
kubernetesClient.pods().create(podWithAttachedContainer)
pendingExecutors += newExecutorId
}
} else if (currentRunningExecutors == currentTotalExpectedExecutors) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>= to be more defensive?

logDebug("Current number of running executors is equal to the number of requested" +
" executors. Not scaling up further.")
} else if (pendingExecutors.nonEmpty) {
logInfo(s"Still waiting for ${pendingExecutors.size} executors to begin running before" +

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should logDebug this

s" requesting for more executors.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no s"

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.spark.scheduler.cluster.k8s

import io.fabric8.kubernetes.api.model.Pod

private[spark] trait ExecutorPodsEventQueue {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious - is there no event queue mechanism within Spark itself for reuse here? Somewhat tangentially, there looks to be EventLoop in org.apache.spark.util.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is but they don't have the concept of processing the same events at different time intervals in different components. This is what RxJava buys us that the existing event systems in the codebase do not.


def addSubscriber(processBatchIntervalMillis: Long)(onNextBatch: Seq[Pod] => Unit): Unit

def stopProcessingEvents(): Unit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think stop or close is sufficient.


def pushPodUpdate(updatedPod: Pod): Unit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simply name it enqueue given the name of the trait already tells clearly what the queue stores?


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.spark.scheduler.cluster.k8s

import java.util.concurrent.{Executor, ScheduledExecutorService, TimeUnit}

import com.google.common.collect.Lists
import io.fabric8.kubernetes.api.model.Pod
import io.reactivex.disposables.Disposable
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import scala.collection.JavaConverters._
import scala.collection.mutable

import org.apache.spark.util.Utils

private[spark] class ExecutorPodsEventQueueImpl(
bufferEventsExecutor: ScheduledExecutorService,
executeSubscriptionsExecutor: Executor)
extends ExecutorPodsEventQueue {

private val eventsObservable = PublishSubject.create[Pod]()
private val observedDisposables = mutable.Buffer.empty[Disposable]

def addSubscriber(processBatchIntervalMillis: Long)(onNextBatch: Seq[Pod] => Unit): Unit = {
observedDisposables += eventsObservable
// Group events in the time window given by the caller. These buffers are then sent
// to the caller's lambda at the given interval, with the pod updates that occurred
// in that given interval.
.buffer(
processBatchIntervalMillis,
TimeUnit.MILLISECONDS,
// For testing - specifically use the given scheduled executor service to trigger
// buffer boundaries. Allows us to inject a deterministic scheduler here.
Schedulers.from(bufferEventsExecutor))
// Trigger an event cycle immediately. Not strictly required to be fully correct, but
// in particular the pod allocator should try to request executors immediately instead
// of waiting for one pod allocation delay.
.startWith(Lists.newArrayList[Pod]())
// Force all triggered events - both the initial event above and the buffered ones in
// the following time windows - to execute asynchronously to this call's thread.
.observeOn(Schedulers.from(executeSubscriptionsExecutor))
.subscribe(toReactivexConsumer { (pods: java.util.List[Pod]) =>
Utils.tryLogNonFatalError {
onNextBatch(pods.asScala)
}
})
}

def stopProcessingEvents(): Unit = {
observedDisposables.foreach(_.dispose())
eventsObservable.onComplete()
}

def pushPodUpdate(updatedPod: Pod): Unit = eventsObservable.onNext(updatedPod)

private def toReactivexConsumer[T](consumer: T => Unit): Consumer[T] = {
new Consumer[T] {
override def accept(item: T): Unit = consumer(item)
}
}
}
Loading