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. This updates desiredRatePerSec in Throttler with logCleanerIoMaxBytesPerSecond and stops current log cleaners and creates new ones.

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.

There are 2 and in the sentence now. Maybe we can put it like this to make it clear:

It will (1) updates desiredRatePerSec in Throttler with logCleanerIoMaxBytesPerSecond if necessary (2) stops current log cleaners and creates new ones.

WDYT?

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. I’ve update the comment.
f9d27f6

* 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
23 changes: 21 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,26 @@ class LogCleanerTest {
} finally logCleaner.shutdown()
}

@Test
def testReconfigureLogCleanerIoMaxBytesPerSecond(): Unit = {
val oldKafkaProps = TestUtils.createBrokerConfig(1, "localhost:2181")
oldKafkaProps.put(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)

assertEquals(logCleaner.throttler.desiredRatePerSec, 10000000, "Throttler.desiredRatePerSec should be initialized with KafkaConfig.LogCleanerIoMaxBytesPerSecondProp")

@showuon showuon Jul 5, 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: the 1st parameter of assertEquals should be expected value, and 2nd one is actual value. Same comment applied to below assertion. That is:
assertEquals(1000000, logCleaner.throttler.desiredRatePerSec)

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.

Also, the error message might be able to update to:

Throttler.desiredRatePerSec should be initialized from initial `cleaner.io.max.bytes.per.second` config.

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. I’ve updated the assert method parameters and the error message.
e05707d


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

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

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.

I think the logCleaner should not call shutdown in the end since we never startup it, am I correct?

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.

Your concern is correct. Thank you.
LogCleaner.shutdown() should be called at the end of the test because kafka-log-cleaner-thread-x threads are created in LogCleaner.startup() at the end of LogCleaner.reconfigure(), and the threads continue to remain.
I appended LogCleaner.shutdown() to the end of the test and also used LogCleaner with empty startup() and shutdown() implementations.
The test is somewhat more white-box like according to the LogCleaner.reconfigure() implementation, but I couldn't think of any other way. Please let me know if you have any.
e05707d


assertEquals(logCleaner.throttler.desiredRatePerSec, 20000000, "Throttler.desiredRatePerSec should be updated with new KafkaConfig.LogCleanerIoMaxBytesPerSecondProp")

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.

The error message might be able to update to:

Throttler.desiredRatePerSec should be updated with new `cleaner.io.max.bytes.per.second` config.

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. I’ve update the error message.
e05707d

}

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