This is an automated email from the ASF dual-hosted git repository.
SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new 9ebbc6b36 [CELEBORN-2324] Fix JVMQuake threshold and JVMStat timer
unit conversion
9ebbc6b36 is described below
commit 9ebbc6b36ea94b1b665954d699d76e4711c3dd94
Author: pithecuse527 <[email protected]>
AuthorDate: Wed May 13 15:35:43 2026 +0800
[CELEBORN-2324] Fix JVMQuake threshold and JVMStat timer unit conversion
### What changes were proposed in this pull request?
This PR fixes JVMQuake time accounting by preserving threshold config
values as milliseconds and converting JVMStat GC timer tick deltas to
nanoseconds before updating the token bucket.
### Why are the changes needed?
JVMQuake thresholds were parsed as milliseconds but wrapped as
microseconds, making values such as 60s behave like 60ms. JVMStat GC timer
metrics are reported in ticks, so using them directly can misaccount GC time.
### Does this PR resolve a correctness bug?
Yes
### Does this PR introduce _any_ user-facing change?
Yes
### How was this patch tested?
1. UT - Added unit coverage for JVMQuake threshold parsing and JVMStat
tick-to-nanosecond conversion.
2. E2E - Verified the patched image in a Kubernetes spark namespace with
JVMQuake enabled using `dump.threshold=30s`, `kill.threshold=60s`, and
`runtimeWeight=0`. Under repeated GC, the worker stayed Ready with restart
count 0, while the original image terminated early because the configured `60s`
threshold was effectively interpreted as `60ms`.
Verified the kill path with a low-threshold configuration:
`dump.threshold=30ms`, `kill.threshold=60ms`, `runtimeWeight=0`, and
`check.interval=100ms`. Under GC activity, the worker logged bucket: `62995087`
and killThreshold: `60000000`, exited via JVMQuake, and Kubernetes restarted
the pod, increasing the restart count from 0 -> 1.
Closes #3682 from pithecuse527/CELEBORN-2324.
Authored-by: pithecuse527 <[email protected]>
Signed-off-by: SteNicholas <[email protected]>
---
.../org/apache/celeborn/common/CelebornConf.scala | 4 ++--
.../apache/celeborn/common/CelebornConfSuite.scala | 9 ++++++++
.../service/deploy/worker/monitor/JVMQuake.scala | 27 ++++++++++++++++++----
.../deploy/worker/monitor/JVMQuakeSuite.scala | 11 +++++++++
4 files changed, 44 insertions(+), 7 deletions(-)
diff --git
a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
index 796fdf7d4..ed7013ed5 100644
--- a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
@@ -896,11 +896,11 @@ class CelebornConf(loadDefaults: Boolean) extends
Cloneable with Logging with Se
def workerJvmQuakeDumpThreshold: Duration =
getTimeAsMs(
WORKER_JVM_QUAKE_DUMP_THRESHOLD.key,
- WORKER_JVM_QUAKE_DUMP_THRESHOLD.defaultValueString).microsecond
+ WORKER_JVM_QUAKE_DUMP_THRESHOLD.defaultValueString).millisecond
def workerJvmQuakeKillThreshold: Duration =
getTimeAsMs(
WORKER_JVM_QUAKE_KILL_THRESHOLD.key,
- WORKER_JVM_QUAKE_KILL_THRESHOLD.defaultValueString).microsecond
+ WORKER_JVM_QUAKE_KILL_THRESHOLD.defaultValueString).millisecond
def workerJvmQuakeExitCode: Int = get(WORKER_JVM_QUAKE_EXIT_CODE)
// //////////////////////////////////////////////////////
diff --git
a/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
b/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
index ff2584ac6..953255cde 100644
--- a/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
+++ b/common/src/test/scala/org/apache/celeborn/common/CelebornConfSuite.scala
@@ -25,6 +25,15 @@ import org.apache.celeborn.common.protocol.StorageInfo
class CelebornConfSuite extends CelebornFunSuite {
+ test("JVMQuake thresholds should preserve configured time units") {
+ val conf = new CelebornConf()
+ .set(WORKER_JVM_QUAKE_DUMP_THRESHOLD.key, "30s")
+ .set(WORKER_JVM_QUAKE_KILL_THRESHOLD.key, "60s")
+
+ assert(conf.workerJvmQuakeDumpThreshold.toMillis == 30000L)
+ assert(conf.workerJvmQuakeKillThreshold.toMillis == 60000L)
+ }
+
test("celeborn.master.endpoints support multi nodes") {
val conf = new CelebornConf()
.set(CelebornConf.MASTER_ENDPOINTS.key,
"localhost1:9097,localhost2:9097")
diff --git
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuake.scala
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuake.scala
index 40b14c6d0..477b1ad2f 100644
---
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuake.scala
+++
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuake.scala
@@ -87,11 +87,15 @@ class JVMQuake(conf: CelebornConf, uniqueId: String =
UUID.randomUUID().toString
private def run(): Unit = {
val currentExitTime = getLastExitTime
val currentGCTime = getLastGCTime
- val gcTime = currentGCTime - lastGCTime
- val runTime = currentExitTime - lastExitTime - gcTime
-
- bucket = Math.max(0, bucket + gcTime - BigDecimal(runTime *
runtimeWeight).toLong)
- logDebug(s"Time: (gc time: ${Utils.msDurationToString(gcTime)}, execution
time: ${Utils.msDurationToString(runTime)})")
+ val gcTimeTicks = currentGCTime - lastGCTime
+ val runTimeTicks = currentExitTime - lastExitTime - gcTimeTicks
+ // JVMStat time monitors are reported in ticks. Convert deltas to nanos
before comparing
+ // them against JVMQuake thresholds, which are stored as nanos.
+ val gcTime = ticksToNanos(gcTimeTicks)
+ val runTime = ticksToNanos(runTimeTicks)
+
+ bucket = Math.max(0, bucket + gcTime - (BigDecimal(runTime) *
BigDecimal(runtimeWeight)).toLong)
+ logDebug(s"Time: (gc time: ${Utils.nanoDurationToString(gcTime)},
execution time: ${Utils.nanoDurationToString(runTime)})")
logDebug(
s"Capacity: (bucket: $bucket, dump threshold: $dumpThreshold, kill
threshold: $killThreshold)")
@@ -161,6 +165,8 @@ class JVMQuake(conf: CelebornConf, uniqueId: String =
UUID.randomUUID().toString
object JVMQuake {
+ private[this] val NANOS_PER_SECOND = 1000000000L
+
private[this] var quake: JVMQuake = _
def create(conf: CelebornConf, uniqueId: String): JVMQuake = {
@@ -187,6 +193,8 @@ object JVMQuake {
monitoredVm.findByName("sun.gc.collector.1.lastExitTime")
private[this] lazy val ygcTimeMonitor: Monitor =
monitoredVm.findByName("sun.gc.collector.0.time")
private[this] lazy val fgcTimeMonitor: Monitor =
monitoredVm.findByName("sun.gc.collector.1.time")
+ private[this] lazy val hrtFrequencyMonitor: Monitor =
+ monitoredVm.findByName("sun.os.hrt.frequency")
private def getLastExitTime: Long = Math.max(
ygcExitTimeMonitor.getValue.asInstanceOf[Long],
@@ -194,4 +202,13 @@ object JVMQuake {
private def getLastGCTime: Long =
ygcTimeMonitor.getValue.asInstanceOf[Long] +
fgcTimeMonitor.getValue.asInstanceOf[Long]
+
+ private def hrtFrequency: Long =
hrtFrequencyMonitor.getValue.asInstanceOf[Long]
+
+ private[monitor] def ticksToNanos(ticks: Long): Long = ticksToNanos(ticks,
hrtFrequency)
+
+ private[monitor] def ticksToNanos(ticks: Long, frequency: Long): Long = {
+ require(frequency > 0, s"Invalid JVMStat high-resolution timer frequency:
$frequency")
+ ((BigInt(ticks) * BigInt(NANOS_PER_SECOND)) / BigInt(frequency)).toLong
+ }
}
diff --git
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuakeSuite.scala
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuakeSuite.scala
index 7ef49f225..fa3b7f36b 100644
---
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuakeSuite.scala
+++
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/monitor/JVMQuakeSuite.scala
@@ -37,6 +37,17 @@ class JVMQuakeSuite extends CelebornFunSuite {
System.gc()
}
+ test("Convert JVMStat timer ticks to nanoseconds") {
+ assert(JVMQuake.ticksToNanos(1L, 1000000000L) === 1L)
+ assert(JVMQuake.ticksToNanos(1000L, 1000L) === 1000000000L)
+ assert(JVMQuake.ticksToNanos(1500L, 1000L) === 1500000000L)
+ assert(JVMQuake.ticksToNanos(-1000L, 1000L) === -1000000000L)
+
+ intercept[IllegalArgumentException] {
+ JVMQuake.ticksToNanos(1L, 0L)
+ }
+ }
+
test("[CELEBORN-1092] Introduce JVM monitoring in Celeborn Worker using
JVMQuake") {
val quake = new JVMQuake(new
CelebornConf().set(WORKER_JVM_QUAKE_ENABLED.key, "true")
.set(WORKER_JVM_QUAKE_RUNTIME_WEIGHT.key, "1")