-
Notifications
You must be signed in to change notification settings - Fork 447
[CELEBORN-2336] Support decommission shutdown for worker scale-down scenarios #3698
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
base: main
Are you sure you want to change the base?
Changes from 9 commits
9bc9eb1
ad76e88
c8227df
81d825b
df73277
4cd9d6b
d5ff48c
abc0013
a5ac067
c604d25
de748dd
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 |
|---|---|---|
|
|
@@ -1357,7 +1357,12 @@ class CelebornConf(loadDefaults: Boolean) extends Cloneable with Logging with Se | |
| // ////////////////////////////////////////////////////// | ||
| // Graceful Shutdown & Recover // | ||
| // ////////////////////////////////////////////////////// | ||
| def workerDecommissionShutdown: Boolean = get(WORKER_DECOMMISSION_SHUTDOWN_ENABLED) | ||
| def workerGracefulShutdown: Boolean = get(WORKER_GRACEFUL_SHUTDOWN_ENABLED) | ||
| // Decommission shutdown overrides graceful shutdown: a decommissioned worker will not | ||
| // restart, so recovery state (recovery DB, sorter state) should not be persisted. | ||
| def effectiveWorkerGracefulShutdown: Boolean = | ||
| workerGracefulShutdown && !workerDecommissionShutdown | ||
| def workerGracefulShutdownTimeoutMs: Long = get(WORKER_GRACEFUL_SHUTDOWN_TIMEOUT) | ||
| def workerGracefulShutdownCheckSlotsFinishedInterval: Long = | ||
| get(WORKER_CHECK_SLOTS_FINISHED_INTERVAL) | ||
|
|
@@ -4477,6 +4482,23 @@ object CelebornConf extends Logging { | |
| .timeConf(TimeUnit.MILLISECONDS) | ||
| .createWithDefaultString("6h") | ||
|
|
||
| val WORKER_DECOMMISSION_SHUTDOWN_ENABLED: ConfigEntry[Boolean] = | ||
| buildConf("celeborn.worker.decommission.shutdown.enabled") | ||
|
chenghuichen marked this conversation as resolved.
|
||
| .categories("worker") | ||
| .doc("When true, the worker will decommission on shutdown signal (e.g. SIGTERM), " + | ||
| "waiting for all shuffle data to be consumed or expired before exiting. " + | ||
| "This is suitable for permanent scale-down scenarios where the worker will not restart. " + | ||
| "When enabled, this overrides celeborn.worker.graceful.shutdown.enabled " + | ||
| "(recovery state will not be saved since the worker is not expected to come back). " + | ||
| "Operators should set the pod's terminationGracePeriodSeconds to " + | ||
|
Member
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.
|
||
| "celeborn.worker.decommission.forceExitTimeout + " + | ||
| "celeborn.worker.decommission.checkInterval plus a small buffer, to ensure " + | ||
| "the shutdown hook has enough time to complete resource cleanup before " + | ||
| "being killed.") | ||
| .version("0.7.0") | ||
| .booleanConf | ||
| .createWithDefault(false) | ||
|
|
||
| val WORKER_GRACEFUL_SHUTDOWN_ENABLED: ConfigEntry[Boolean] = | ||
| buildConf("celeborn.worker.graceful.shutdown.enabled") | ||
| .categories("worker") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -156,7 +156,7 @@ private[celeborn] class Worker( | |
|
|
||
| private val WORKER_SHUTDOWN_PRIORITY = 100 | ||
| val shutdown = new AtomicBoolean(false) | ||
| private val gracefulShutdown = conf.workerGracefulShutdown | ||
| private val gracefulShutdown = conf.effectiveWorkerGracefulShutdown | ||
| if (gracefulShutdown) { | ||
| var checkPortMap = Map( | ||
| WORKER_RPC_PORT -> conf.workerRpcPort, | ||
|
|
@@ -619,29 +619,34 @@ private[celeborn] class Worker( | |
| if (!stopped) { | ||
| logInfo("Stopping Worker.") | ||
|
|
||
| // Both graceful shutdown and decommission have drained data, so in-flight | ||
| // tasks are allowed to finish instead of being force-cancelled. | ||
| val drainBeforeExit = exitKind == CelebornExitKind.WORKER_GRACEFUL_SHUTDOWN || | ||
|
Member
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. Drain intent isn't carried through to the partition sorter. This comment/ |
||
| exitKind == CelebornExitKind.WORKER_DECOMMISSION | ||
|
|
||
| if (jvmProfiler != null) { | ||
| jvmProfiler.stop() | ||
| } | ||
| if (jvmQuake != null) { | ||
| jvmQuake.stop() | ||
| } | ||
| if (sendHeartbeatTask != null) { | ||
| if (exitKind == CelebornExitKind.WORKER_GRACEFUL_SHUTDOWN) { | ||
| if (drainBeforeExit) { | ||
| sendHeartbeatTask.cancel(false) | ||
| } else { | ||
| sendHeartbeatTask.cancel(true) | ||
| } | ||
| sendHeartbeatTask = null | ||
| } | ||
| if (checkFastFailTask != null) { | ||
| if (exitKind == CelebornExitKind.WORKER_GRACEFUL_SHUTDOWN) { | ||
| if (drainBeforeExit) { | ||
| checkFastFailTask.cancel(false) | ||
| } else { | ||
| checkFastFailTask.cancel(true) | ||
| } | ||
| checkFastFailTask = null | ||
| } | ||
| if (exitKind == CelebornExitKind.WORKER_GRACEFUL_SHUTDOWN) { | ||
| if (drainBeforeExit) { | ||
| forwardMessageScheduler.shutdown() | ||
| replicateThreadPool.shutdown() | ||
| commitThreadPool.shutdown() | ||
|
|
@@ -950,7 +955,7 @@ private[celeborn] class Worker( | |
| exitType.toUpperCase(Locale.ROOT) match { | ||
| case "DECOMMISSION" => | ||
| ShutdownHookManager.get().updateTimeout( | ||
|
Member
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. Duplicated hook-timeout expression. |
||
| conf.workerDecommissionForceExitTimeout, | ||
| conf.workerDecommissionForceExitTimeout + conf.workerDecommissionCheckInterval, | ||
| TimeUnit.MILLISECONDS) | ||
| workerStatusManager.doTransition(WorkerEventType.Decommission) | ||
| case "GRACEFUL" => | ||
|
|
@@ -1031,7 +1036,9 @@ private[celeborn] class Worker( | |
|
|
||
| def waitTime: Long = waitTimes * interval | ||
|
|
||
| while (!storageManager.shuffleKeySet().isEmpty && waitTime < timeout) { | ||
| // Bound the total wait strictly by the timeout so that the remaining shutdown hook | ||
| // budget is left for stop(WORKER_DECOMMISSION) to clean up resources. | ||
|
Member
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. Worst case can truncate |
||
| while (!storageManager.shuffleKeySet().isEmpty && waitTime + interval <= timeout) { | ||
|
Member
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.
|
||
| Thread.sleep(interval) | ||
| waitTimes += 1 | ||
| } | ||
|
|
@@ -1080,22 +1087,28 @@ private[celeborn] class Worker( | |
| workerStatusManager.exitEventType match { | ||
| case WorkerEventType.Graceful => | ||
| shutdownGracefully() | ||
| stop(CelebornExitKind.WORKER_GRACEFUL_SHUTDOWN) | ||
| case WorkerEventType.Decommission => | ||
| decommissionWorker() | ||
|
Member
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. Decommission report failure has no retry/fallback on the SIGTERM path. |
||
| stop(CelebornExitKind.WORKER_DECOMMISSION) | ||
|
Member
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. Behavior change for runtime REST decommission. This branch is reached not only by the new SIGTERM feature but also by the existing
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. Not a regression — the existing REST |
||
| case _ => | ||
| exitImmediately() | ||
| } | ||
|
|
||
| if (workerStatusManager.exitEventType == WorkerEventType.Graceful) { | ||
| stop(CelebornExitKind.WORKER_GRACEFUL_SHUTDOWN) | ||
| } else { | ||
| stop(CelebornExitKind.EXIT_IMMEDIATELY) | ||
| stop(CelebornExitKind.EXIT_IMMEDIATELY) | ||
| } | ||
| } | ||
| }, | ||
| "worker-shutdown-hook-thread"), | ||
| WORKER_SHUTDOWN_PRIORITY) | ||
|
|
||
| if (conf.workerDecommissionShutdown) { | ||
|
chenghuichen marked this conversation as resolved.
|
||
| // The wait loop in decommissionWorker() is bounded by forceExitTimeout, so the extra | ||
| // checkInterval reserves headroom for stop(WORKER_DECOMMISSION) to finish cleanup | ||
| // before the hook is cancelled. | ||
| ShutdownHookManager.get().updateTimeout( | ||
|
Member
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. Altitude: |
||
| conf.workerDecommissionForceExitTimeout + conf.workerDecommissionCheckInterval, | ||
|
chenghuichen marked this conversation as resolved.
Outdated
chenghuichen marked this conversation as resolved.
Outdated
|
||
| TimeUnit.MILLISECONDS) | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| def getPushFetchServerPort: (Int, Int) = (pushPort, fetchPort) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,8 +39,13 @@ private[celeborn] class WorkerStatusManager(conf: CelebornConf) extends Logging | |
| private var worker: Worker = _ | ||
| private var shutdown: AtomicBoolean = _ | ||
| private var storageManager: StorageManager = _ | ||
| private val decommissionShutdown = conf.workerDecommissionShutdown | ||
| private val gracefulShutdown = conf.workerGracefulShutdown | ||
| if (gracefulShutdown) { | ||
| if (decommissionShutdown) { | ||
| exitEventType = WorkerEventType.Decommission | ||
|
Member
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.
|
||
| logInfo("Decommission shutdown enabled, worker will decommission on SIGTERM" + | ||
| " (overrides graceful shutdown)") | ||
| } else if (gracefulShutdown) { | ||
| exitEventType = WorkerEventType.Graceful | ||
| } | ||
|
chenghuichen marked this conversation as resolved.
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,24 +57,55 @@ class WorkerStatusManagerSuite extends AnyFunSuite { | |
| statusManager.init(worker) | ||
|
|
||
| statusManager.doTransition(WorkerEventType.DecommissionThenIdle) | ||
| Assert.assertEquals(statusManager.getWorkerState(), PbWorkerStatus.State.InDecommissionThenIdle) | ||
| Assert.assertEquals(PbWorkerStatus.State.InDecommissionThenIdle, statusManager.getWorkerState()) | ||
| Assert.assertEquals( | ||
| worker.workerInfo.getWorkerStatus().getStateValue, | ||
| PbWorkerStatus.State.InDecommissionThenIdle.getNumber) | ||
| PbWorkerStatus.State.InDecommissionThenIdle.getNumber, | ||
| worker.workerInfo.getWorkerStatus().getStateValue) | ||
|
|
||
| // Rerun state Transition | ||
| statusManager.doTransition(WorkerEventType.DecommissionThenIdle) | ||
| Assert.assertEquals(statusManager.getWorkerState(), PbWorkerStatus.State.InDecommissionThenIdle) | ||
| Assert.assertEquals(PbWorkerStatus.State.InDecommissionThenIdle, statusManager.getWorkerState()) | ||
|
|
||
| // Reset shuffleKeys | ||
| shuffleKeys.clear() | ||
| statusManager.doTransition(WorkerEventType.DecommissionThenIdle) | ||
| Assert.assertEquals(statusManager.getWorkerState(), PbWorkerStatus.State.Idle) | ||
| Assert.assertEquals(PbWorkerStatus.State.Idle, statusManager.getWorkerState()) | ||
|
|
||
| statusManager.doTransition(WorkerEventType.Recommission) | ||
| Assert.assertEquals(statusManager.getWorkerState(), PbWorkerStatus.State.Normal) | ||
| Assert.assertEquals(PbWorkerStatus.State.Normal, statusManager.getWorkerState()) | ||
|
|
||
| statusManager.doTransition(WorkerEventType.Recommission) | ||
| Assert.assertEquals(statusManager.getWorkerState(), PbWorkerStatus.State.Normal) | ||
| Assert.assertEquals(PbWorkerStatus.State.Normal, statusManager.getWorkerState()) | ||
| } | ||
|
|
||
| test("Test exitEventType initialization based on config") { | ||
| // Neither graceful nor decommission → Immediately. Set both keys explicitly so the | ||
| // assertion does not depend on system properties leaked from other tests. | ||
| val conf1 = new CelebornConf() | ||
|
chenghuichen marked this conversation as resolved.
|
||
| conf1.set("celeborn.worker.graceful.shutdown.enabled", "false") | ||
| conf1.set("celeborn.worker.decommission.shutdown.enabled", "false") | ||
| val mgr1 = new WorkerStatusManager(conf1) | ||
| Assert.assertEquals(WorkerEventType.Immediately, mgr1.exitEventType) | ||
|
|
||
| // Graceful shutdown only → Graceful | ||
| val conf2 = new CelebornConf() | ||
| conf2.set("celeborn.worker.graceful.shutdown.enabled", "true") | ||
|
Member
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. Non-defensive test setup. |
||
| val mgr2 = new WorkerStatusManager(conf2) | ||
| Assert.assertEquals(WorkerEventType.Graceful, mgr2.exitEventType) | ||
|
|
||
|
chenghuichen marked this conversation as resolved.
|
||
| // Decommission shutdown only → Decommission | ||
| val conf3 = new CelebornConf() | ||
| conf3.set("celeborn.worker.decommission.shutdown.enabled", "true") | ||
| val mgr3 = new WorkerStatusManager(conf3) | ||
| Assert.assertEquals(WorkerEventType.Decommission, mgr3.exitEventType) | ||
|
|
||
| // Both enabled → Decommission overrides graceful | ||
| val conf4 = new CelebornConf() | ||
| conf4.set("celeborn.worker.graceful.shutdown.enabled", "true") | ||
| conf4.set("celeborn.worker.decommission.shutdown.enabled", "true") | ||
| val mgr4 = new WorkerStatusManager(conf4) | ||
| Assert.assertEquals(WorkerEventType.Decommission, mgr4.exitEventType) | ||
|
chenghuichen marked this conversation as resolved.
|
||
| Assert.assertTrue(conf4.workerGracefulShutdown) | ||
| Assert.assertTrue(conf4.workerDecommissionShutdown) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.