-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-5358: Consumer perf tool should count rebalance time. #3188
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
Changes from 12 commits
be43bf3
cbdf6c1
7600e6b
08ff452
8c80f13
edf1d08
722e16d
57bd0e4
16cc2ca
3798272
bbba9b0
3a1ce0f
c517815
9adda74
dfa9422
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 |
|---|---|---|
|
|
@@ -54,17 +54,20 @@ object ConsumerPerformance { | |
| val totalBytesRead = new AtomicLong(0) | ||
| val consumerTimeout = new AtomicBoolean(false) | ||
| var metrics: mutable.Map[MetricName, _ <: Metric] = null | ||
| val joinGroupTimeInMs = new AtomicLong(0) | ||
| var fetchTimeInMs = 0L | ||
|
|
||
| if (!config.hideHeader) { | ||
| printHeader(!config.showDetailedStats) | ||
| printHeader(config.showDetailedStats, config.useOldConsumer) | ||
| } | ||
|
|
||
| var startMs, endMs = 0L | ||
| if (!config.useOldConsumer) { | ||
| val consumer = new KafkaConsumer[Array[Byte], Array[Byte]](config.props) | ||
| consumer.subscribe(Collections.singletonList(config.topic)) | ||
| startMs = System.currentTimeMillis | ||
| consume(consumer, List(config.topic), config.numMessages, 1000, config, totalMessagesRead, totalBytesRead) | ||
| consume(consumer, List(config.topic), config.numMessages, 1000, | ||
| config, totalMessagesRead, totalBytesRead, joinGroupTimeInMs, startMs) | ||
| endMs = System.currentTimeMillis | ||
|
|
||
| if (config.printMetrics) { | ||
|
|
@@ -95,10 +98,25 @@ object ConsumerPerformance { | |
| consumerConnector.shutdown() | ||
| } | ||
| val elapsedSecs = (endMs - startMs) / 1000.0 | ||
|
Contributor
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. I think the main point of this patch is to compute the throughput statistics based only on the time spent fetching, so maybe we can move this below and just use |
||
| fetchTimeInMs = (endMs - startMs) - joinGroupTimeInMs.get | ||
| if (!config.showDetailedStats) { | ||
| val totalMBRead = (totalBytesRead.get * 1.0) / (1024 * 1024) | ||
| println("%s, %s, %.4f, %.4f, %d, %.4f".format(config.dateFormat.format(startMs), config.dateFormat.format(endMs), | ||
| totalMBRead, totalMBRead / elapsedSecs, totalMessagesRead.get, totalMessagesRead.get / elapsedSecs)) | ||
| print("%s, %s, %.4f, %.4f, %d, %.4f".format( | ||
| config.dateFormat.format(startMs), | ||
| config.dateFormat.format(endMs), | ||
| totalMBRead, | ||
| totalMBRead / elapsedSecs, | ||
| totalMessagesRead.get, | ||
| totalMessagesRead.get / elapsedSecs | ||
| )) | ||
| if (!config.useOldConsumer) { | ||
| print(", %d, %d, %.4f, %.4f".format( | ||
| joinGroupTimeInMs.get, | ||
| fetchTimeInMs, | ||
| totalMBRead / (fetchTimeInMs / 1000.0), | ||
| totalMessagesRead.get / (fetchTimeInMs / 1000.0) | ||
| )) | ||
| } | ||
| } | ||
|
|
||
| if (metrics != null) { | ||
|
|
@@ -107,11 +125,13 @@ object ConsumerPerformance { | |
|
|
||
| } | ||
|
|
||
| private[tools] def printHeader(showDetailedStats: Boolean): Unit = { | ||
| if (showDetailedStats) | ||
| println("start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec") | ||
| else | ||
| println("time, threadId, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec") | ||
| private[tools] def printHeader(showDetailedStats: Boolean, useOldConsumer: Boolean): Unit = { | ||
| val newFieldsInHeader = s"${if (!useOldConsumer) ", rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec" else ""}" | ||
| if (!showDetailedStats) { | ||
| println("start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec" + newFieldsInHeader) | ||
| } else { | ||
| println("time, threadId, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec" + newFieldsInHeader) | ||
| } | ||
| } | ||
|
|
||
| def consume(consumer: KafkaConsumer[Array[Byte], Array[Byte]], | ||
|
|
@@ -120,25 +140,30 @@ object ConsumerPerformance { | |
| timeout: Long, | ||
| config: ConsumerPerfConfig, | ||
| totalMessagesRead: AtomicLong, | ||
| totalBytesRead: AtomicLong) { | ||
| totalBytesRead: AtomicLong, | ||
| joinTime: AtomicLong, | ||
| testStartTime: Long) { | ||
| var bytesRead = 0L | ||
| var messagesRead = 0L | ||
| var lastBytesRead = 0L | ||
| var lastMessagesRead = 0L | ||
| var joinStart = 0L | ||
|
|
||
| // Wait for group join, metadata fetch, etc | ||
| val joinTimeout = 10000 | ||
| val isAssigned = new AtomicBoolean(false) | ||
| consumer.subscribe(topics.asJava, new ConsumerRebalanceListener { | ||
| def onPartitionsAssigned(partitions: util.Collection[TopicPartition]) { | ||
| isAssigned.set(true) | ||
| joinTime.addAndGet(System.currentTimeMillis - joinStart) | ||
| } | ||
| def onPartitionsRevoked(partitions: util.Collection[TopicPartition]) { | ||
| isAssigned.set(false) | ||
| joinStart = System.currentTimeMillis | ||
| }}) | ||
| val joinStart = System.currentTimeMillis() | ||
| val joinStartForFirstTime = System.currentTimeMillis() | ||
|
Contributor
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. Since we are now reporting the join time separately, I don't think we need the loop below anymore. It was initially intended to discount the rebalance overhead, but now we account for that transparently. |
||
| while (!isAssigned.get()) { | ||
| if (System.currentTimeMillis() - joinStart >= joinTimeout) { | ||
| if (System.currentTimeMillis() - joinStartForFirstTime >= joinTimeout) { | ||
| throw new Exception("Timed out waiting for initial group join.") | ||
| } | ||
| consumer.poll(100) | ||
|
|
@@ -165,7 +190,8 @@ object ConsumerPerformance { | |
|
|
||
| if (currentTimeMillis - lastReportTime >= config.reportingInterval) { | ||
| if (config.showDetailedStats) | ||
| printProgressMessage(0, bytesRead, lastBytesRead, messagesRead, lastMessagesRead, lastReportTime, currentTimeMillis, config.dateFormat) | ||
| printProgressMessage(0, bytesRead, lastBytesRead, messagesRead, lastMessagesRead, | ||
| lastReportTime, currentTimeMillis, config.dateFormat, Some(testStartTime), Some(joinTime.get)) | ||
|
Contributor
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.
Contributor
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. Since |
||
| lastReportTime = currentTimeMillis | ||
| lastMessagesRead = messagesRead | ||
| lastBytesRead = bytesRead | ||
|
|
@@ -184,12 +210,21 @@ object ConsumerPerformance { | |
| lastMessagesRead: Long, | ||
| startMs: Long, | ||
| endMs: Long, | ||
| dateFormat: SimpleDateFormat) = { | ||
| dateFormat: SimpleDateFormat, | ||
| beginTime: Option[Long] = None, | ||
|
Contributor
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. It's a little weird that we have both a
Contributor
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.
However, we also need to pass through What do you think?
Contributor
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. Ok, that's fair. In that case, perhaps we should do something similar for the join time. Maybe we can have one field which tracks periodic join time and is reset after each reporting interval, and another field which tracks the total join time.
Contributor
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. added |
||
| joinTimeOpt: Option[Long] = None) = { | ||
| val elapsedMs: Double = endMs - startMs | ||
| val totalMBRead = (bytesRead * 1.0) / (1024 * 1024) | ||
| val mbRead = ((bytesRead - lastBytesRead) * 1.0) / (1024 * 1024) | ||
| println("%s, %d, %.4f, %.4f, %d, %.4f".format(dateFormat.format(endMs), id, totalMBRead, | ||
| 1000.0 * (mbRead / elapsedMs), messagesRead, ((messagesRead - lastMessagesRead) / elapsedMs) * 1000.0)) | ||
| print("%s, %d, %.4f, %.4f, %d, %.4f".format(dateFormat.format(endMs), id, totalMBRead, | ||
| 1000.0 * (mbRead / elapsedMs), messagesRead, ((messagesRead - lastMessagesRead) / elapsedMs) * 1000.0)) | ||
| joinTimeOpt match { | ||
|
Contributor
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. Maybe we should just move this block to a separate function so that we don't need all of the
Contributor
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. Sorry, I am not sure if I fully understand this comment. Do you mean create a new function to handle all the printing things whereas
Contributor
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. Yes, it's a minor issue, but it's kind of annoying that we have three optional parameters which are all expected in the case of the new consumer. I think a cleaner approach is to use a separate method. Something approximately like this: def printBasicProgress ...
def printExtraProgress ...
def printNewConsumerProgress {
printBasicProgress()
printExtraProgress()
println()
}
def printOldConsumerProgress() {
printBasicProgress()
println()
}Then we shouldn't need the optional parameters. |
||
| case Some(joinTime) => | ||
| val fetchTimeMs = endMs - beginTime.getOrElse(startMs) - joinTime | ||
| println(", %d, %d, %.4f, %.4f".format(joinTime, fetchTimeMs, (bytesRead * 1.0 / (1024 * 1024) / (fetchTimeMs / 1000.0)), | ||
|
Contributor
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. Now we're printing two lines in every call to
Contributor
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. @hachikuji The new-added fields are within the same line since I changed the first |
||
| messagesRead / (fetchTimeMs / 1000.0))) | ||
| case None => println() | ||
| } | ||
| } | ||
|
|
||
| class ConsumerPerfConfig(args: Array[String]) extends PerfConfig(args) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,21 +26,35 @@ import org.junit.Test | |
| class ConsumerPerformanceTest { | ||
|
|
||
| private val outContent = new ByteArrayOutputStream() | ||
| private val dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS") | ||
|
|
||
| @Test | ||
| def testHeaderMatchBody(): Unit = { | ||
| def testDetailedHeaderMatchBody(): Unit = { | ||
| testHeaderMatchContent(true, false, 2, () => ConsumerPerformance.printProgressMessage(1, 1024 * 1024, 0, 1, 0, 0, 1, | ||
|
Contributor
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. Can you add parameter names for the two booleans? |
||
| dateFormat, Some(1L), Some(1L))) | ||
| testHeaderMatchContent(true, true, 4, () => ConsumerPerformance.printProgressMessage(1, 1024 * 1024, 0, 1, 0, 0, 1, | ||
| dateFormat)) | ||
| } | ||
|
|
||
| @Test | ||
| def testNonDetailedHeaderMatchBody(): Unit = { | ||
| testHeaderMatchContent(false, false, 2, () => println(s"${dateFormat.format(System.currentTimeMillis)}, " + | ||
| s"${dateFormat.format(System.currentTimeMillis)}, 1.0, 1.0, 1, 1.0, 1, 1, 1.1, 1.1")) | ||
| testHeaderMatchContent(false, true, 4, () => println(s"${dateFormat.format(System.currentTimeMillis)}, " + | ||
| s"${dateFormat.format(System.currentTimeMillis)}, 1.0, 1.0, 1, 1.0")) | ||
| } | ||
|
|
||
| private def testHeaderMatchContent(detailed: Boolean, useOldConsumer: Boolean, expectedOutputLineCount: Int, fun: () => Unit): Unit = { | ||
| Console.withOut(outContent) { | ||
| ConsumerPerformance.printHeader(true) | ||
| ConsumerPerformance.printProgressMessage(1, 1024 * 1024, 0, 1, 0, 0, 1, | ||
| new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS") | ||
| ) | ||
| } | ||
| ConsumerPerformance.printHeader(detailed, useOldConsumer) | ||
| fun() | ||
|
|
||
| val contents = outContent.toString.split("\n") | ||
| assertEquals(2, contents.length) | ||
| val header = contents(0) | ||
| val body = contents(1) | ||
| val contents = outContent.toString.split("\n") | ||
| assertEquals(expectedOutputLineCount, contents.length) | ||
| val header = contents(0) | ||
| val body = contents(1) | ||
|
|
||
| assertEquals(header.split(",").length, body.split(",").length) | ||
| assertEquals(header.split(",").length, body.split(",").length) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be a
var. We can just initialize it below, right?