-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-30481][CORE] Integrate event log compactor into Spark History Server #27208
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
05e5074
04c9cab
8500453
ec3ffd9
d2240f9
cb7f884
c567781
ddd6788
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 |
|---|---|---|
|
|
@@ -158,6 +158,9 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| new HistoryServerDiskManager(conf, path, listing, clock) | ||
| } | ||
|
|
||
| private val fileCompactor = new EventLogFileCompactor(conf, hadoopConf, fs, | ||
| conf.get(EVENT_LOG_ROLLING_MAX_FILES_TO_RETAIN), conf.get(EVENT_LOG_COMPACTION_SCORE_THRESHOLD)) | ||
|
|
||
| // Used to store the paths, which are being processed. This enable the replay log tasks execute | ||
| // asynchronously and make sure that checkForLogs would not process a path repeatedly. | ||
| private val processing = ConcurrentHashMap.newKeySet[String] | ||
|
|
@@ -475,10 +478,9 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| } | ||
|
|
||
| if (shouldReloadLog(info, reader)) { | ||
| // ignore fastInProgressParsing when the status of application is changed from | ||
| // in-progress to completed, which is needed for rolling event log. | ||
| if (info.appId.isDefined && (info.isComplete == reader.completed) && | ||
| fastInProgressParsing) { | ||
| // ignore fastInProgressParsing when rolling event log is enabled on the log path, | ||
| // to ensure proceeding compaction even fastInProgressParsing is turned on. | ||
| if (info.appId.isDefined && reader.lastIndex.isEmpty && fastInProgressParsing) { | ||
| // When fast in-progress parsing is on, we don't need to re-parse when the | ||
| // size changes, but we do need to invalidate any existing UIs. | ||
| // Also, we need to update the `lastUpdated time` to display the updated time in | ||
|
|
@@ -518,7 +520,7 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| // to parse it. This will allow the cleaner code to detect the file as stale later on | ||
| // if it was not possible to parse it. | ||
| listing.write(LogInfo(reader.rootPath.toString(), newLastScanTime, LogType.EventLogs, | ||
| None, None, reader.fileSizeForLastIndex, reader.lastIndex, | ||
| None, None, reader.fileSizeForLastIndex, reader.lastIndex, None, | ||
| reader.completed)) | ||
| reader.fileSizeForLastIndex > 0 | ||
| } | ||
|
|
@@ -532,16 +534,8 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| } | ||
|
|
||
| updated.foreach { entry => | ||
| processing(entry.rootPath) | ||
| try { | ||
| val task: Runnable = () => mergeApplicationListing(entry, newLastScanTime, true) | ||
| replayExecutor.submit(task) | ||
| } catch { | ||
| // let the iteration over the updated entries break, since an exception on | ||
| // replayExecutor.submit (..) indicates the ExecutorService is unable | ||
| // to take any more submissions at this time | ||
| case e: Exception => | ||
| logError(s"Exception while submitting event log for replay", e) | ||
| submitLogProcessTask(entry.rootPath) { () => | ||
| mergeApplicationListing(entry, newLastScanTime, true) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -661,27 +655,37 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| reader: EventLogFileReader, | ||
| scanTime: Long, | ||
| enableOptimizations: Boolean): Unit = { | ||
| val rootPath = reader.rootPath | ||
| try { | ||
| val lastEvaluatedForCompaction: Option[Long] = try { | ||
| listing.read(classOf[LogInfo], rootPath.toString).lastEvaluatedForCompaction | ||
| } catch { | ||
| case _: NoSuchElementException => None | ||
| } | ||
|
|
||
| pendingReplayTasksCount.incrementAndGet() | ||
| doMergeApplicationListing(reader, scanTime, enableOptimizations) | ||
| doMergeApplicationListing(reader, scanTime, enableOptimizations, lastEvaluatedForCompaction) | ||
| if (conf.get(CLEANER_ENABLED)) { | ||
| checkAndCleanLog(reader.rootPath.toString) | ||
| checkAndCleanLog(rootPath.toString) | ||
| } | ||
| } catch { | ||
| case e: InterruptedException => | ||
| throw e | ||
| case e: AccessControlException => | ||
| // We don't have read permissions on the log file | ||
| logWarning(s"Unable to read log ${reader.rootPath}", e) | ||
| blacklist(reader.rootPath) | ||
| logWarning(s"Unable to read log $rootPath", e) | ||
| blacklist(rootPath) | ||
| // SPARK-28157 We should remove this blacklisted entry from the KVStore | ||
| // to handle permission-only changes with the same file sizes later. | ||
| listing.delete(classOf[LogInfo], reader.rootPath.toString) | ||
| listing.delete(classOf[LogInfo], rootPath.toString) | ||
| case e: Exception => | ||
| logError("Exception while merging application listings", e) | ||
| } finally { | ||
| endProcessing(reader.rootPath) | ||
| endProcessing(rootPath) | ||
| pendingReplayTasksCount.decrementAndGet() | ||
|
|
||
| // triggering another task for compaction task | ||
| submitLogProcessTask(rootPath) { () => compact(reader) } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -692,7 +696,8 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| private[history] def doMergeApplicationListing( | ||
| reader: EventLogFileReader, | ||
| scanTime: Long, | ||
| enableOptimizations: Boolean): Unit = { | ||
| enableOptimizations: Boolean, | ||
| lastEvaluatedForCompaction: Option[Long]): Unit = { | ||
| val eventsFilter: ReplayEventsFilter = { eventString => | ||
| eventString.startsWith(APPL_START_EVENT_PREFIX) || | ||
| eventString.startsWith(APPL_END_EVENT_PREFIX) || | ||
|
|
@@ -770,8 +775,8 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| invalidateUI(app.info.id, app.attempts.head.info.attemptId) | ||
| addListing(app) | ||
| listing.write(LogInfo(logPath.toString(), scanTime, LogType.EventLogs, Some(app.info.id), | ||
| app.attempts.head.info.attemptId, reader.fileSizeForLastIndex, | ||
| reader.lastIndex, reader.completed)) | ||
| app.attempts.head.info.attemptId, reader.fileSizeForLastIndex, reader.lastIndex, | ||
| lastEvaluatedForCompaction, reader.completed)) | ||
|
|
||
| // For a finished log, remove the corresponding "in progress" entry from the listing DB if | ||
| // the file is really gone. | ||
|
|
@@ -795,15 +800,42 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| // mean the end event is before the configured threshold, so call the method again to | ||
| // re-parse the whole log. | ||
| logInfo(s"Reparsing $logPath since end event was not found.") | ||
| doMergeApplicationListing(reader, scanTime, enableOptimizations = false) | ||
| doMergeApplicationListing(reader, scanTime, enableOptimizations = false, | ||
| lastEvaluatedForCompaction) | ||
|
|
||
| case _ => | ||
| // If the app hasn't written down its app ID to the logs, still record the entry in the | ||
| // listing db, with an empty ID. This will make the log eligible for deletion if the app | ||
| // does not make progress after the configured max log age. | ||
| listing.write( | ||
| LogInfo(logPath.toString(), scanTime, LogType.EventLogs, None, None, | ||
| reader.fileSizeForLastIndex, reader.lastIndex, reader.completed)) | ||
| reader.fileSizeForLastIndex, reader.lastIndex, lastEvaluatedForCompaction, | ||
| reader.completed)) | ||
| } | ||
| } | ||
|
|
||
| private def compact(reader: EventLogFileReader): Unit = { | ||
| val rootPath = reader.rootPath | ||
| try { | ||
| reader.lastIndex match { | ||
| case Some(lastIndex) => | ||
| try { | ||
| val info = listing.read(classOf[LogInfo], reader.rootPath.toString) | ||
| if (info.lastEvaluatedForCompaction.isEmpty || | ||
| info.lastEvaluatedForCompaction.get < lastIndex) { | ||
| // haven't tried compaction for this index, do compaction | ||
| fileCompactor.compact(reader.listEventLogFiles) | ||
| listing.write(info.copy(lastEvaluatedForCompaction = Some(lastIndex))) | ||
| } | ||
| } catch { | ||
| case _: NoSuchElementException => | ||
| // this should exist, but ignoring doesn't hurt much | ||
| } | ||
|
|
||
| case None => // This is not applied to single event log file. | ||
| } | ||
| } finally { | ||
| endProcessing(rootPath) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -962,7 +994,7 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| case e: NoSuchElementException => | ||
| // For every new driver log file discovered, create a new entry in listing | ||
| listing.write(LogInfo(f.getPath().toString(), currentTime, LogType.DriverLogs, None, | ||
| None, f.getLen(), None, false)) | ||
| None, f.getLen(), None, None, false)) | ||
| false | ||
| } | ||
| if (deleteFile) { | ||
|
|
@@ -989,9 +1021,9 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| } | ||
|
|
||
| /** | ||
| * Rebuilds the application state store from its event log. | ||
| * Rebuilds the application state store from its event log. Exposed for testing. | ||
| */ | ||
| private def rebuildAppStore( | ||
| private[spark] def rebuildAppStore( | ||
| store: KVStore, | ||
| reader: EventLogFileReader, | ||
| lastUpdated: Long): Unit = { | ||
|
|
@@ -1010,8 +1042,9 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| } replayBus.addListener(listener) | ||
|
|
||
| try { | ||
| val eventLogFiles = reader.listEventLogFiles | ||
| logInfo(s"Parsing ${reader.rootPath} to re-build UI...") | ||
| parseAppEventLogs(reader.listEventLogFiles, replayBus, !reader.completed) | ||
| parseAppEventLogs(eventLogFiles, replayBus, !reader.completed) | ||
| trackingStore.close(false) | ||
| logInfo(s"Finished parsing ${reader.rootPath}") | ||
| } catch { | ||
|
|
@@ -1122,30 +1155,59 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| // At this point the disk data either does not exist or was deleted because it failed to | ||
| // load, so the event log needs to be replayed. | ||
|
|
||
| val reader = EventLogFileReader(fs, new Path(logDir, attempt.logPath), | ||
| attempt.lastIndex) | ||
| val isCompressed = reader.compressionCodec.isDefined | ||
| logInfo(s"Leasing disk manager space for app $appId / ${attempt.info.attemptId}...") | ||
| val lease = dm.lease(reader.totalSize, isCompressed) | ||
| val newStorePath = try { | ||
| Utils.tryWithResource(KVUtils.open(lease.tmpPath, metadata)) { store => | ||
| rebuildAppStore(store, reader, attempt.info.lastUpdated.getTime()) | ||
| var retried = false | ||
| var newStorePath: File = null | ||
| while (newStorePath == null) { | ||
| val reader = EventLogFileReader(fs, new Path(logDir, attempt.logPath), | ||
| attempt.lastIndex) | ||
| val isCompressed = reader.compressionCodec.isDefined | ||
| logInfo(s"Leasing disk manager space for app $appId / ${attempt.info.attemptId}...") | ||
| val lease = dm.lease(reader.totalSize, isCompressed) | ||
| try { | ||
| Utils.tryWithResource(KVUtils.open(lease.tmpPath, metadata)) { store => | ||
| rebuildAppStore(store, reader, attempt.info.lastUpdated.getTime()) | ||
| } | ||
| newStorePath = lease.commit(appId, attempt.info.attemptId) | ||
| } catch { | ||
| case _: IOException if !retried => | ||
| // compaction may touch the file(s) which app rebuild wants to read | ||
| // compaction wouldn't run in short interval, so try again... | ||
| logWarning(s"Exception occurred while rebuilding app $appId - trying again...") | ||
| lease.rollback() | ||
| retried = true | ||
|
|
||
| case e: Exception => | ||
| lease.rollback() | ||
| throw e | ||
| } | ||
| lease.commit(appId, attempt.info.attemptId) | ||
| } catch { | ||
| case e: Exception => | ||
| lease.rollback() | ||
| throw e | ||
| } | ||
|
|
||
| KVUtils.open(newStorePath, metadata) | ||
| } | ||
|
|
||
| private def createInMemoryStore(attempt: AttemptInfoWrapper): KVStore = { | ||
| val store = new InMemoryStore() | ||
| val reader = EventLogFileReader(fs, new Path(logDir, attempt.logPath), | ||
| attempt.lastIndex) | ||
| rebuildAppStore(store, reader, attempt.info.lastUpdated.getTime()) | ||
| var retried = false | ||
| var store: KVStore = null | ||
| while (store == null) { | ||
| try { | ||
| val s = new InMemoryStore() | ||
| val reader = EventLogFileReader(fs, new Path(logDir, attempt.logPath), | ||
| attempt.lastIndex) | ||
| rebuildAppStore(s, reader, attempt.info.lastUpdated.getTime()) | ||
| store = s | ||
| } catch { | ||
| case _: IOException if !retried => | ||
| // compaction may touch the file(s) which app rebuild wants to read | ||
| // compaction wouldn't run in short interval, so try again... | ||
| logWarning(s"Exception occurred while rebuilding log path ${attempt.logPath} - " + | ||
| "trying again...") | ||
| retried = true | ||
|
|
||
| case e: Exception => | ||
| throw e | ||
| } | ||
| } | ||
|
|
||
| store | ||
| } | ||
|
|
||
|
|
@@ -1175,6 +1237,21 @@ private[history] class FsHistoryProvider(conf: SparkConf, clock: Clock) | |
| } | ||
| deleted | ||
| } | ||
|
|
||
| /** NOTE: 'task' should ensure it executes 'endProcessing' at the end */ | ||
|
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. Hmm, perhaps this method could take care of 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.
If we move endProcessing from So either we need to make lock much smarter, or document the requirement on caller side. I'm feeling that former one is more complicated than latter one. |
||
| private def submitLogProcessTask(rootPath: Path)(task: Runnable): Unit = { | ||
| try { | ||
| processing(rootPath) | ||
| replayExecutor.submit(task) | ||
| } catch { | ||
| // let the iteration over the updated entries break, since an exception on | ||
| // replayExecutor.submit (..) indicates the ExecutorService is unable | ||
| // to take any more submissions at this time | ||
| case e: Exception => | ||
| logError(s"Exception while submitting task", e) | ||
| endProcessing(rootPath) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private[history] object FsHistoryProvider { | ||
|
|
@@ -1218,6 +1295,8 @@ private[history] case class LogInfo( | |
| fileSize: Long, | ||
| @JsonDeserialize(contentAs = classOf[JLong]) | ||
| lastIndex: Option[Long], | ||
| @JsonDeserialize(contentAs = classOf[JLong]) | ||
| lastEvaluatedForCompaction: Option[Long], | ||
| isComplete: Boolean) | ||
|
|
||
| private[history] class AttemptInfoWrapper( | ||
|
|
||
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.
So one thing that feels a tiny bit odd is that when deciding whether to compact, you're actually considering the last log file, which you won't consider during actual compaction, right?
Wouldn't that cause unnecessary (or too aggressive) compaction at the end of the application, when potentially a bunch of jobs finish and "release" lots of tasks, inflating the compation scoe?
Uh oh!
There was an error while loading. Please reload this page.
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.
That's the intention that callers of compactor don't care about how many files are actually affected. Callers of compactor just need to know that same list of log files would bring same result, unless it fails and throws exception. How many files are excluded in compaction is just a configuration, and the last log file should be excluded is an implementation detail. (We prevent it in both configuration and compactor via having 1 as min value for max retain log file.)
Compactor will ignore the last log file in any way as configured, so unless the rare case where the log is rolled just before the app is finished, it won't happen. And most probably end users would avoid to set the value to 1 if they read the doc and understand how it works.