-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-31608][CORE][WEBUI] Add a new type of KVStore to make loading UI faster #28412
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 all commits
0164960
7e3c39e
e6707bd
34e4564
141feed
3162a8a
a796231
c6b5392
d710e46
0321669
fe81510
09d6b3b
fd1b6b8
f58e12c
7ee2605
aa15fe3
3494307
1e514b9
d6c7d98
0d36074
76dbd18
025b13c
3412d29
b71a923
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 |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| /* | ||
| * 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.deploy.history | ||
|
|
||
| import java.util.concurrent.atomic.AtomicLong | ||
|
|
||
| import scala.collection.mutable.HashMap | ||
|
|
||
| import org.apache.spark.SparkConf | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.internal.config.History._ | ||
| import org.apache.spark.util.Utils | ||
|
|
||
| /** | ||
| * A class used to keep track of in-memory store usage by the SHS. | ||
| */ | ||
| private class HistoryServerMemoryManager( | ||
| conf: SparkConf) extends Logging { | ||
|
|
||
| private val maxUsage = conf.get(MAX_IN_MEMORY_STORE_USAGE) | ||
| private val currentUsage = new AtomicLong(0L) | ||
| private val active = new HashMap[(String, Option[String]), Long]() | ||
|
|
||
| def initialize(): Unit = { | ||
| logInfo("Initialized memory manager: " + | ||
| s"current usage = ${Utils.bytesToString(currentUsage.get())}, " + | ||
| s"max usage = ${Utils.bytesToString(maxUsage)}") | ||
| } | ||
|
|
||
| def lease( | ||
| appId: String, | ||
| attemptId: Option[String], | ||
| eventLogSize: Long, | ||
| codec: Option[String]): Unit = { | ||
| val memoryUsage = approximateMemoryUsage(eventLogSize, codec) | ||
| if (memoryUsage + currentUsage.get > maxUsage) { | ||
| throw new RuntimeException("Not enough memory to create hybrid store " + | ||
| s"for app $appId / $attemptId.") | ||
| } | ||
| active.synchronized { | ||
| active(appId -> attemptId) = memoryUsage | ||
| } | ||
| currentUsage.addAndGet(memoryUsage) | ||
| logInfo(s"Leasing ${Utils.bytesToString(memoryUsage)} memory usage for " + | ||
| s"app $appId / $attemptId") | ||
| } | ||
|
|
||
| def release(appId: String, attemptId: Option[String]): Unit = { | ||
| val memoryUsage = active.synchronized { active.remove(appId -> attemptId) } | ||
|
|
||
| memoryUsage match { | ||
| case Some(m) => | ||
| currentUsage.addAndGet(-m) | ||
| logInfo(s"Released ${Utils.bytesToString(m)} memory usage for " + | ||
| s"app $appId / $attemptId") | ||
|
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 should be exhaustive, according to the build result. If we don't do anything for the None, please add
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. fixed. |
||
| case None => | ||
| } | ||
| } | ||
|
|
||
| private def approximateMemoryUsage(eventLogSize: Long, codec: Option[String]): Long = { | ||
| codec match { | ||
| case Some("zstd") => | ||
| eventLogSize * 10 | ||
| case Some(_) => | ||
| eventLogSize * 4 | ||
| case None => | ||
| eventLogSize / 2 | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,185 @@ | |||||||||||||||||||||||||||||
| /* | |||||||||||||||||||||||||||||
| * 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.deploy.history | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| import java.io.IOException | |||||||||||||||||||||||||||||
| import java.util.Collection | |||||||||||||||||||||||||||||
| import java.util.concurrent.ConcurrentHashMap | |||||||||||||||||||||||||||||
| import java.util.concurrent.atomic.AtomicBoolean | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| import scala.collection.JavaConverters._ | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| import org.apache.spark.util.kvstore._ | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||
| * An implementation of KVStore that accelerates event logs loading. | |||||||||||||||||||||||||||||
| * | |||||||||||||||||||||||||||||
| * When rebuilding the application state from event logs, HybridStore will | |||||||||||||||||||||||||||||
| * write data to InMemoryStore at first and use a background thread to dump | |||||||||||||||||||||||||||||
| * data to LevelDB once the app store is restored. We don't expect write | |||||||||||||||||||||||||||||
| * operations (except the case for caching) after calling switch to level DB. | |||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| private[history] class HybridStore extends KVStore { | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| private val inMemoryStore = new InMemoryStore() | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| private var levelDB: LevelDB = null | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| // Flag to indicate whether we should use inMemoryStore or levelDB | |||||||||||||||||||||||||||||
| private val shouldUseInMemoryStore = new AtomicBoolean(true) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| // Flag to indicate whether this hybrid store is closed, use this flag | |||||||||||||||||||||||||||||
| // to avoid starting background thread after the store is closed | |||||||||||||||||||||||||||||
| private val closed = new AtomicBoolean(false) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| // A background thread that dumps data from inMemoryStore to levelDB | |||||||||||||||||||||||||||||
| private var backgroundThread: Thread = null | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| // A hash map that stores all classes that had been writen to inMemoryStore | |||||||||||||||||||||||||||||
| private val klassMap = new ConcurrentHashMap[Class[_], Boolean] | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def getMetadata[T](klass: Class[T]): T = { | |||||||||||||||||||||||||||||
| getStore().getMetadata(klass) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def setMetadata(value: Object): Unit = { | |||||||||||||||||||||||||||||
| getStore().setMetadata(value) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def read[T](klass: Class[T], naturalKey: Object): T = { | |||||||||||||||||||||||||||||
| getStore().read(klass, naturalKey) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def write(value: Object): Unit = { | |||||||||||||||||||||||||||||
| getStore().write(value) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| if (backgroundThread == null) { | |||||||||||||||||||||||||||||
| // New classes won't be dumped once the background thread is started | |||||||||||||||||||||||||||||
| klassMap.putIfAbsent(value.getClass(), true) | |||||||||||||||||||||||||||||
|
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 we expect a write after a background thread has started? We might want to throw an IIlegalStateException
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. write operation is only allowed for CacheQuantile objects after the rebuildAppStore() is finished. Here if we want to throw IIlegalStateException, we need to have special logic to check if the value is of class CacheQuantile. I think we would prefer to avoid that to make the HybridStore as generic as possible. 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. May be worth mentioning CacheQuantile in the comment as it can be recalculated
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 don't think we should mention specific class - we added the assumption in class doc, with trying to generalize the case. If we feel the class doc isn't enough then we can comment the assumption here as well, but let's generalize. 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. Ah ok i missed that thanks |
|||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def delete(klass: Class[_], naturalKey: Object): Unit = { | |||||||||||||||||||||||||||||
| if (backgroundThread != null) { | |||||||||||||||||||||||||||||
| throw new IllegalStateException("delete() shouldn't be called after " + | |||||||||||||||||||||||||||||
| "the hybrid store begins switching to levelDB") | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| getStore().delete(klass, naturalKey) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def view[T](klass: Class[T]): KVStoreView[T] = { | |||||||||||||||||||||||||||||
| getStore().view(klass) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def count(klass: Class[_]): Long = { | |||||||||||||||||||||||||||||
| getStore().count(klass) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def count(klass: Class[_], index: String, indexedValue: Object): Long = { | |||||||||||||||||||||||||||||
| getStore().count(klass, index, indexedValue) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def close(): Unit = { | |||||||||||||||||||||||||||||
| try { | |||||||||||||||||||||||||||||
| closed.set(true) | |||||||||||||||||||||||||||||
| if (backgroundThread != null && backgroundThread.isAlive()) { | |||||||||||||||||||||||||||||
| // The background thread is still running, wait for it to finish | |||||||||||||||||||||||||||||
| backgroundThread.join() | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } finally { | |||||||||||||||||||||||||||||
| inMemoryStore.close() | |||||||||||||||||||||||||||||
| if (levelDB != null) { | |||||||||||||||||||||||||||||
|
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. Probably we may want to guarantee this to be executed once regardless of exception being thrown in join().
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. In the current implementation, the background thread won't throw uncaught exceptions. So I think levelDB.close() is guaranteed to be executed. Here the try-catch block is trying to catch the IOException that might be thrown during levelDB.close().
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. Isn't join() able to throw InterruptedException? That's not a runtime exception but you're playing with Scala which ignores checked exception so you still need to be careful. And IMHO, generally at any case we must ensure both level DB store and in memory store are closed because that's a resource leak.
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. Friendly reminder about the comment here.
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. Sorry I missed this comment. Yeah, you are right, join() can throw InterruptedException. I will update the code. |
|||||||||||||||||||||||||||||
| levelDB.close() | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| override def removeAllByIndexValues[T]( | |||||||||||||||||||||||||||||
| klass: Class[T], | |||||||||||||||||||||||||||||
| index: String, | |||||||||||||||||||||||||||||
| indexValues: Collection[_]): Boolean = { | |||||||||||||||||||||||||||||
| if (backgroundThread != null) { | |||||||||||||||||||||||||||||
| throw new IllegalStateException("removeAllByIndexValues() shouldn't be " + | |||||||||||||||||||||||||||||
| "called after the hybrid store begins switching to levelDB") | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| getStore().removeAllByIndexValues(klass, index, indexValues) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def setLevelDB(levelDB: LevelDB): Unit = { | |||||||||||||||||||||||||||||
| this.levelDB = levelDB | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||
| * This method is called when the writing is done for inMemoryStore. A | |||||||||||||||||||||||||||||
| * background thread will be created and be started to dump data in inMemoryStore | |||||||||||||||||||||||||||||
| * to levelDB. Once the dumping is completed, the underlying kvstore will be | |||||||||||||||||||||||||||||
| * switched to levelDB. | |||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||
| def switchToLevelDB( | |||||||||||||||||||||||||||||
| listener: HybridStore.SwitchToLevelDBListener, | |||||||||||||||||||||||||||||
| appId: String, | |||||||||||||||||||||||||||||
| attemptId: Option[String]): Unit = { | |||||||||||||||||||||||||||||
| if (closed.get) { | |||||||||||||||||||||||||||||
| return | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| backgroundThread = new Thread(() => { | |||||||||||||||||||||||||||||
| try { | |||||||||||||||||||||||||||||
| for (klass <- klassMap.keys().asScala) { | |||||||||||||||||||||||||||||
| val it = inMemoryStore.view(klass).closeableIterator() | |||||||||||||||||||||||||||||
| while (it.hasNext()) { | |||||||||||||||||||||||||||||
| levelDB.write(it.next()) | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
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. Drive by comment - given I added something similar to an in-house patch.
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 the information, I will try adding a
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. If this helps, this is what I had written up for level db - for memory store, the default list traversal + write is good enough :
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, this is helpful!
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. Hi @mridulm, I updated your code and used it on in-memory store - leveldb switching, but only saw little switching time improvement. I am not sure if somewhere wrong.
The code: I think using multiple threads to write data to leveldb might shorten the switching time but it would introduce more overhead to SHS.
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. It is a function of how loaded your disk is, iops it can sustain, txn's leveldb can do concurrently.
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. Make sense. I am testing it on the mac which has SSD and the disk is not busy. I think the improvement might be more obvious on HDD or busy disk. @HeartSaVioR @tgravescs Do we need to add batch write support for leveldb on this pr?
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 don't think it's mandatory. You can file another issue as "improvement" for this, but IMHO working with this is completely optional for you. I think we have already asked so many things to do.
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. Sounds good. We can improve that afterward. |
|||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| listener.onSwitchToLevelDBSuccess() | |||||||||||||||||||||||||||||
| shouldUseInMemoryStore.set(false) | |||||||||||||||||||||||||||||
tgravescs marked this conversation as resolved.
Show resolved
Hide resolved
|
|||||||||||||||||||||||||||||
| inMemoryStore.close() | |||||||||||||||||||||||||||||
| } catch { | |||||||||||||||||||||||||||||
| case e: Exception => | |||||||||||||||||||||||||||||
| listener.onSwitchToLevelDBFail(e) | |||||||||||||||||||||||||||||
|
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 line can be moved to the catch statement.
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. done. |
|||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| }) | |||||||||||||||||||||||||||||
| backgroundThread.setDaemon(true) | |||||||||||||||||||||||||||||
| backgroundThread.setName(s"hybridstore-$appId-$attemptId") | |||||||||||||||||||||||||||||
| backgroundThread.start() | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| /** | |||||||||||||||||||||||||||||
| * This method return the store that we should use. | |||||||||||||||||||||||||||||
| */ | |||||||||||||||||||||||||||||
| private def getStore(): KVStore = { | |||||||||||||||||||||||||||||
| if (shouldUseInMemoryStore.get) { | |||||||||||||||||||||||||||||
| inMemoryStore | |||||||||||||||||||||||||||||
| } else { | |||||||||||||||||||||||||||||
| levelDB | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| private[history] object HybridStore { | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| trait SwitchToLevelDBListener { | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def onSwitchToLevelDBSuccess(): Unit | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| def onSwitchToLevelDBFail(e: Exception): Unit | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.