Skip to content
Merged
13 changes: 11 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,20 @@ class LogCleaner(initialConfig: CleanerConfig,
}

/**
* Reconfigure log clean config. This simply stops current log cleaners and creates new ones.
* Reconfigure log clean config. The will:
* 1. update desiredRatePerSec in Throttler with logCleanerIoMaxBytesPerSecond, if necessary
* 2. stop current log cleaners and create new ones.
* 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.setProperty(KafkaConfig.LogCleanerIoMaxBytesPerSecondProp, "10000000")

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.setProperty(KafkaConfig.LogCleanerIoMaxBytesPerSecondProp, "20000000")

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)
}
}