Skip to content
Merged
11 changes: 9 additions & 2 deletions core/src/main/scala/kafka/log/LogCleaner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class LogCleaner(initialConfig: CleanerConfig,
private[log] val cleanerManager = new LogCleanerManager(logDirs, logs, logDirFailureChannel)

/* a throttle used to limit the I/O of all the cleaner threads to a user-specified maximum rate */
private val throttler = new Throttler(desiredRatePerSec = config.maxIoBytesPerSecond,
private[log] val throttler = new Throttler(desiredRatePerSec = config.maxIoBytesPerSecond,
checkIntervalMs = 300,
throttleDown = true,
"cleaner-io",
Expand Down Expand Up @@ -186,11 +186,18 @@ class LogCleaner(initialConfig: CleanerConfig,
}

/**
* Reconfigure log clean config. This simply stops current log cleaners and creates new ones.
* Reconfigure log clean config. It will (1) update desiredRatePerSec in Throttler with logCleanerIoMaxBytesPerSecond if necessary (2) stop current log cleaners and create new ones.
Comment thread
tyamashi-oss marked this conversation as resolved.
Outdated
* That ensures that if any of the cleaners had failed, new cleaners are created to match the new config.
*/
override def reconfigure(oldConfig: KafkaConfig, newConfig: KafkaConfig): Unit = {
Comment thread
showuon marked this conversation as resolved.
config = LogCleaner.cleanerConfig(newConfig)

val maxIoBytesPerSecond = config.maxIoBytesPerSecond;
if (maxIoBytesPerSecond != oldConfig.logCleanerIoMaxBytesPerSecond) {
info(s"Updating logCleanerIoMaxBytesPerSecond: $maxIoBytesPerSecond")
throttler.updateDesiredRatePerSec(maxIoBytesPerSecond)
}
Comment thread
showuon marked this conversation as resolved.

shutdown()
startup()
}
Expand Down
12 changes: 8 additions & 4 deletions core/src/main/scala/kafka/utils/Throttler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import scala.math._
* @param time: The time implementation to use
*/
@threadsafe
class Throttler(desiredRatePerSec: Double,
class Throttler(@volatile var desiredRatePerSec: Double,
checkIntervalMs: Long = 100L,
throttleDown: Boolean = true,
metricName: String = "throttler",
Expand All @@ -52,6 +52,7 @@ class Throttler(desiredRatePerSec: Double,
def maybeThrottle(observed: Double): Unit = {
val msPerSec = TimeUnit.SECONDS.toMillis(1)
val nsPerSec = TimeUnit.SECONDS.toNanos(1)
val currentDesiredRatePerSec = desiredRatePerSec;

meter.mark(observed.toLong)
lock synchronized {
Expand All @@ -62,14 +63,14 @@ class Throttler(desiredRatePerSec: Double,
// we should take a little nap
if (elapsedNs > checkIntervalNs && observedSoFar > 0) {
val rateInSecs = (observedSoFar * nsPerSec) / elapsedNs
val needAdjustment = !(throttleDown ^ (rateInSecs > desiredRatePerSec))
val needAdjustment = !(throttleDown ^ (rateInSecs > currentDesiredRatePerSec))
if (needAdjustment) {
// solve for the amount of time to sleep to make us hit the desired rate
val desiredRateMs = desiredRatePerSec / msPerSec.toDouble
val desiredRateMs = currentDesiredRatePerSec / msPerSec.toDouble
val elapsedMs = TimeUnit.NANOSECONDS.toMillis(elapsedNs)
val sleepTime = round(observedSoFar / desiredRateMs - elapsedMs)
if (sleepTime > 0) {
trace("Natural rate is %f per second but desired rate is %f, sleeping for %d ms to compensate.".format(rateInSecs, desiredRatePerSec, sleepTime))
trace("Natural rate is %f per second but desired rate is %f, sleeping for %d ms to compensate.".format(rateInSecs, currentDesiredRatePerSec, sleepTime))
time.sleep(sleepTime)
}
}
Expand All @@ -79,6 +80,9 @@ class Throttler(desiredRatePerSec: Double,
}
}

def updateDesiredRatePerSec(updatedDesiredRatePerSec: Double): Unit = {
desiredRatePerSec = updatedDesiredRatePerSec;
}
}

object Throttler {
Expand Down
32 changes: 30 additions & 2 deletions core/src/test/scala/unit/kafka/log/LogCleanerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ import java.nio.charset.StandardCharsets
import java.nio.file.Paths
import java.util.Properties
import java.util.concurrent.{CountDownLatch, TimeUnit}

import kafka.common._
import kafka.server.{BrokerTopicStats, LogDirFailureChannel}
import kafka.server.{BrokerTopicStats, KafkaConfig, LogDirFailureChannel}
import kafka.utils._
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.errors.CorruptRecordException
Expand Down Expand Up @@ -1854,6 +1853,35 @@ class LogCleanerTest {
} finally logCleaner.shutdown()
}

@Test
def testReconfigureLogCleanerIoMaxBytesPerSecond(): Unit = {
val oldKafkaProps = TestUtils.createBrokerConfig(1, "localhost:2181")
oldKafkaProps.put(KafkaConfig.LogCleanerIoMaxBytesPerSecondProp, 10000000: java.lang.Double)

@showuon showuon Jul 7, 2022

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.

nit: I think we can just put 10000000L here for long value.

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.

Thank you for noticing.
After checking other tests, I replaced Properties.put(Object, Object) to Properteis.setProperty(String, String), and used string, not number. Because it is appropriate to use string as key and value for Properties, and string values are also used when actually creating KafkaConfig. And I also confirmed that the previous compilation error reappeared when using 10000000L: "the result type of an implicit conversion must be more specific than Object"
ba640f5

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.

Thanks for the update!


val logCleaner = new LogCleaner(LogCleaner.cleanerConfig(new KafkaConfig(oldKafkaProps)),
logDirs = Array(TestUtils.tempDir()),
logs = new Pool[TopicPartition, UnifiedLog](),
logDirFailureChannel = new LogDirFailureChannel(1),
time = time) {
// shutdown() and startup() are called in LogCleaner.reconfigure().
// Empty startup() and shutdown() to ensure that no unnecessary log cleaner threads remain after this test.
override def startup(): Unit = {}
override def shutdown(): Unit = {}
}

try {
assertEquals(10000000, logCleaner.throttler.desiredRatePerSec, s"Throttler.desiredRatePerSec should be initialized from initial `${KafkaConfig.LogCleanerIoMaxBytesPerSecondProp}` config.")

val newKafkaProps = TestUtils.createBrokerConfig(1, "localhost:2181")
newKafkaProps.put(KafkaConfig.LogCleanerIoMaxBytesPerSecondProp, 20000000: java.lang.Double)

logCleaner.reconfigure(new KafkaConfig(oldKafkaProps), new KafkaConfig(newKafkaProps))

assertEquals(20000000, logCleaner.throttler.desiredRatePerSec, s"Throttler.desiredRatePerSec should be updated with new `${KafkaConfig.LogCleanerIoMaxBytesPerSecondProp}` config.")
} finally {
logCleaner.shutdown()
}
}

private def writeToLog(log: UnifiedLog, keysAndValues: Iterable[(Int, Int)], offsetSeq: Iterable[Long]): Iterable[Long] = {
for(((key, value), offset) <- keysAndValues.zip(offsetSeq))
Expand Down
46 changes: 46 additions & 0 deletions core/src/test/scala/unit/kafka/utils/ThrottlerTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,50 @@ class ThrottlerTest {
val actualCountPerSec = 4 * desiredCountPerInterval * 1000 / elapsedTimeMs
assertTrue(actualCountPerSec <= desiredCountPerSec)
}

@Test
def testUpdateThrottleDesiredRate(): Unit = {
val throttleCheckIntervalMs = 100
val desiredCountPerSec = 1000.0
val desiredCountPerInterval = desiredCountPerSec * throttleCheckIntervalMs / 1000.0
val updatedDesiredCountPerSec = 1500.0;
val updatedDesiredCountPerInterval = updatedDesiredCountPerSec * throttleCheckIntervalMs / 1000.0

val mockTime = new MockTime()
val throttler = new Throttler(desiredRatePerSec = desiredCountPerSec,
checkIntervalMs = throttleCheckIntervalMs,
time = mockTime)

// Observe desiredCountPerInterval at t1
val t1 = mockTime.milliseconds()
throttler.maybeThrottle(desiredCountPerInterval)
assertEquals(t1, mockTime.milliseconds())

// Observe desiredCountPerInterval at t1 + throttleCheckIntervalMs + 1,
mockTime.sleep(throttleCheckIntervalMs + 1)
throttler.maybeThrottle(desiredCountPerInterval)
val t2 = mockTime.milliseconds()
assertTrue(t2 >= t1 + 2 * throttleCheckIntervalMs)

val elapsedTimeMs = t2 - t1
val actualCountPerSec = 2 * desiredCountPerInterval * 1000 / elapsedTimeMs
assertTrue(actualCountPerSec <= desiredCountPerSec)

// Update ThrottleDesiredRate
throttler.updateDesiredRatePerSec(updatedDesiredCountPerSec);

// Observe updatedDesiredCountPerInterval at t2
throttler.maybeThrottle(updatedDesiredCountPerInterval)
Comment thread
showuon marked this conversation as resolved.
assertEquals(t2, mockTime.milliseconds())

// Observe updatedDesiredCountPerInterval at t2 + throttleCheckIntervalMs + 1
mockTime.sleep(throttleCheckIntervalMs + 1)
throttler.maybeThrottle(updatedDesiredCountPerInterval)
val t3 = mockTime.milliseconds()
assertTrue(t3 >= t2 + 2 * throttleCheckIntervalMs)

val updatedElapsedTimeMs = t3 - t2
val updatedActualCountPerSec = 2 * updatedDesiredCountPerInterval * 1000 / updatedElapsedTimeMs
assertTrue(updatedActualCountPerSec <= updatedDesiredCountPerSec)
}
}