KAFKA-8578: Add basic functionality to expose RocksDB metrics#6979
Conversation
- Adds `RocksDBMetrics` class that provides methods to get sensors from the Kafka metrics registry and to setup the sensors to record RocksDB metrics - Extends `StreamsMetricsImpl` with functionality to add the required metrics to the sensors.
|
Call for review @pkleindl @ableegoldman @bbejeck @vvcephei @guozhangwang @mjsax @abbccdda |
| return tagMap; | ||
| } | ||
|
|
||
| public Map<String, String> storeLevelTagMap(final String taskName, final String storeType, final String storeName) { |
There was a problem hiding this comment.
nit: line to long (break each parameter in it's own line)
There was a problem hiding this comment.
The line is exactly 120 characters long as allowed by the coding guidelines. ;-)
There was a problem hiding this comment.
Can we just add https://checkstyle.sourceforge.io/config_sizes.html#LineLength so we never have to have this discussion again? :)
| } | ||
|
|
||
| public Map<String, String> taskLevelTagMap(final String taskName) { | ||
| final Map<String, String> tagMap = threadLevelTagMap(); |
There was a problem hiding this comment.
In threadLevelTagMap(String...) there is a check:
if (tags != null) {
if ((tags.length % 2) != 0) {
throw new IllegalArgumentException("Tags needs to be specified in key-value pairs");
}
Should we do the same here
There was a problem hiding this comment.
I am not sure I can follow. We do not have a parameter String... tags here. What should we check?
There was a problem hiding this comment.
Actually, I'm not sure what's going on with threadLevelTagMap(String...).
It has exactly one usage, which is streamsMetrics.threadLevelTagMap(TASK_ID_TAG, ALL_TASKS) which seems to be doing exactly the same thing this method is doing.
Maybe we can just get rid of threadLevelTagMap(String...) and migrate the usage to this method?
There was a problem hiding this comment.
streamsMetrics.threadLevelTagMap(TASK_ID_TAG, ALL_TASKS) is used for a parent sensor that will be removed as part of KIP-444. When the that sensor will be removed also streamsMetrics.threadLevelTagMap(TASK_ID_TAG, ALL_TASKS) and the corresponding method will disappear. I would like to leave it for the future PR, since that code is not touched in this PR.
| } | ||
|
|
||
| public Map<String, String> storeLevelTagMap(final String taskName, final String storeType, final String storeName) { | ||
| final Map<String, String> tagMap = taskLevelTagMap(taskName); |
| final String storeName) { | ||
| final Sensor sensor = streamsMetrics | ||
| .storeLevelSensor(taskName, storeName, BYTES_WRITTEN_TO_DB, RecordingLevel.DEBUG); | ||
| addAmountRateAndTotalMetricsToSensor(sensor, |
There was a problem hiding this comment.
nit: for method calls, we usually format like this:
addAmountRateAndTotalMetricsToSensor(
sensor,
...);
Breaking the first parameter already reduced line length and seems preferable.
| streamsMetrics.storeLevelTagMap(taskName, | ||
| STORE_TYPE_PREFIX + storeType, storeName), | ||
| BYTES_WRITTEN_TO_DB, | ||
| "Average per-second number of bytes written to the RocksDB state store", |
There was a problem hiding this comment.
Nit: Average number of bytes written per-second to the RocksDB state store ?
| final String storeName) { | ||
| final Sensor sensor = streamsMetrics | ||
| .storeLevelSensor(taskName, storeName, BYTES_READ_FROM_DB, RecordingLevel.DEBUG); | ||
| addAmountRateAndTotalMetricsToSensor(sensor, |
| .storeLevelSensor(taskName, storeName, BYTES_WRITTEN_TO_DB, RecordingLevel.DEBUG); | ||
| addAmountRateAndTotalMetricsToSensor(sensor, | ||
| STATE_LEVEL_GROUP, | ||
| streamsMetrics.storeLevelTagMap(taskName, |
| streamsMetrics.storeLevelTagMap(taskName, | ||
| STORE_TYPE_PREFIX + storeType, storeName), | ||
| BYTES_READ_FROM_DB, | ||
| "Average per-second number of bytes read from the RocksDB state store", |
| public void shouldAddAmountRateAndSum() { | ||
| final MockTime time = new MockTime(0); | ||
|
|
||
| addAmountRateAndTotalMetricsToSensor(sensor, group, tags, metricNamePrefix, description1, description2); |
There was a problem hiding this comment.
Nit: I think it is confusing to import methods under test (makes it unclear if this is a "setup" method -- might be better to not use static imports) ?
| final double value2 = 18.0; | ||
| sensor.record(value2, time.milliseconds()); | ||
| assertThat(metric.measurable().measure(new MetricConfig(), time.milliseconds()), | ||
| equalTo(initialValue + value1 + value2)); |
There was a problem hiding this comment.
Where does initialValue come from? For example: shouldAddAvgAndTotalMetricsToSensor add the sensor and calls verifySumMetric -- why is the initial sum not zero?
There was a problem hiding this comment.
The initial value is not zero, because in shouldAddAvgAndTotalMetricsToSensor two verifications are called, one verifies the Avg metric and the other the Sum metric. The first verification records two values and the second verification also records two values. Inside the metric the values are computed over samples. All four values added in the verifications are added to the samples over which the metric is computed. That means that the two values added for the first verification are already in the metric sample when the second verification is called. Hence, the initial value in the second verification is the sum of the two values added to the metric in the first verification.
I changed the test to get rid of the initial value. Now, I advance mock time as long as it needs to get empty samples.
|
|
||
| public static final String THREAD_ID_TAG = "client-id"; | ||
| public static final String TASK_ID_TAG = "task-id"; | ||
| public static final String STORE_ID_TAG = "state-id"; |
There was a problem hiding this comment.
This is to be consistent with existing state store-level ids.
| return sensor; | ||
| } | ||
|
|
||
| public static Sensor memtableBytesFlushedSensor(final StreamsMetricsImpl streamsMetrics, |
There was a problem hiding this comment.
One thing I would suggest we do is to create an intermediate struct to store all the parameters, in case later we need to add more fields for sensor creation.
| private static final String MEMTABLE_BYTES_FLUSHED = "memtable-bytes-flushed"; | ||
| private static final String MEMTABLE_HIT_RATIO = "memtable-hit" + RATIO_SUFFIX; | ||
| private static final String MEMTABLE_FLUSH_TIME = "memtable-flush-time"; | ||
| private static final String WRITE_STALL_DURATION = "write-stall-duration"; |
There was a problem hiding this comment.
I would suggest we co-locate the description with metrics and refactor out the sensor creation part, this may help reduce code redundancy.
There was a problem hiding this comment.
I am afraid, I cannot follow. Could you elaborate?
There was a problem hiding this comment.
Maybe @abbccdda was saying to store the description strings in static final fields as well? I'd agree with this, just to be sure that we don't waste memory on duplicate description strings.
There was a problem hiding this comment.
Does this make any difference since all description strings are literals in static methods?
There was a problem hiding this comment.
Yes, each time the method is invoked (I.e., once per store), a separate copy of the string is placed on the heap. I previously didn't think this would be a big factor, but someone in the community profiled the memory usage of a long-running topology and found that these strings tend to accumulate over time.
There's no need to worry about this for short-scoped strings like exception messages, but metrics are long-lived objects, and we benefit from making them static constants.
|
|
||
| @Test | ||
| public void shouldAddAmountRateAndSum() { | ||
| final MockTime time = new MockTime(0); |
There was a problem hiding this comment.
How about making time a private field and do a setUp call to reinitialize it every time?
| assertThat(metrics.metrics().size(), equalTo(2 + 1)); // one metric is added automatically in the constructor of Metrics | ||
| } | ||
|
|
||
| private void verifySumMetric(final Metrics metrics, |
There was a problem hiding this comment.
It's more reusable if we make value1 and value2 both parameters. Maybe we could pass a List along with initialValue in this case?
There was a problem hiding this comment.
I would like to leave this method as it is because it suffices to use two values to verify that the statistical function set in the metric is a sum. IMO a test should be as simple as possible to avoid errors. Additionally, the expected sum would not only depend on the list of values but also on the instance of MetricConfig. This dependency would unnecessarily complicate the logic of this verification.
| sensor.record(value1, time.milliseconds()); | ||
| final double value2 = 18.0; | ||
| sensor.record(value2, time.milliseconds()); | ||
| time.sleep(30000); |
There was a problem hiding this comment.
This sleep is a bit too long for a unit test, could we consider avoiding it?
There was a problem hiding this comment.
This is NOT an actual sleep. This call advances the mocked time by 30 seconds. It does not wait at all.
| replayAndVerify(sensorCreator); | ||
| } | ||
|
|
||
| private void verifyRateSensor(final String metricNamePrefix, |
There was a problem hiding this comment.
nit: I slightly doubt whether these exact match tests are necessary
There was a problem hiding this comment.
Could you elaborate on your doubts? How should I otherwise test, if the methods in RocksDBMetrics create the sensors with the correct name, tags and metrics? Please let me know, if you have a better idea.
| sensor.record(value1, time.milliseconds()); | ||
| final double value2 = 18.0; | ||
| sensor.record(value2, time.milliseconds()); | ||
| time.sleep(30000); |
There was a problem hiding this comment.
here it might work better to use TestUtils.waitForCondition instead of an arbitrary sleep call
There was a problem hiding this comment.
This is NOT an actual sleep. This call advances the mocked time by 30 seconds. It does not wait at all.
|
Java 8 and Java 11 failed, test results already cleaned up. Retest this please |
|
|
||
| public static final String THREAD_ID_TAG = "client-id"; | ||
| public static final String TASK_ID_TAG = "task-id"; | ||
| public static final String STORE_ID_TAG = "state-id"; |
There was a problem hiding this comment.
This is to be consistent with existing state store-level ids.
| "The average per-second number of " + operation); | ||
| } | ||
|
|
||
| public static void addAmountRateAndTotalMetricsToSensor(final Sensor sensor, |
There was a problem hiding this comment.
This is not part of the PR but I realized that CumulativeCount is in streams.processor.internals.metrics but not in common.metrics.stats. Is this intentional? @vvcephei
There was a problem hiding this comment.
IIRC, it was done to avoid a KIP.
Of course, that may or may not have been the right thing to do, but I believe it was the idea... Do you think we should move it?
There was a problem hiding this comment.
Ok, I took a closer look and had a bit of a flash-back to how confusing these metrics are... Maybe #7057 will help.
|
|
||
| public Map<String, String> storeLevelTagMap(final String taskName, final String storeType, final String storeName) { | ||
| final Map<String, String> tagMap = taskLevelTagMap(taskName); | ||
| tagMap.put(storeType + STORE_ID_TAG, storeName); |
There was a problem hiding this comment.
Are the storeType supposed to have the dash already? If not this should be storeType + "-" + STORE_ID_TAG.
There was a problem hiding this comment.
Good point! I think I am going to change this to be consistent with the other labels.
| final String storeName) { | ||
| final Sensor sensor = streamsMetrics | ||
| .storeLevelSensor(taskName, storeName, COMPACTION_TIME + MIN_SUFFIX, RecordingLevel.DEBUG); | ||
| addValueMetricToSensor(sensor, |
There was a problem hiding this comment.
I'm a bit confused here for max / min we are creating a sensor with type Value still, but since I did not see how the record() is going to be invoked maybe I'm just wrong.. otherwise, could we just use a similar function like addAvgAndMax?
There was a problem hiding this comment.
For the RocksDB statistics that come in the form of histograms, we already get the min, max, and average values from RocksDB. We still need to decide if we should reset the histogram after each recording to have
kind of a moving window semantic or if we are fine with the histograms from the start of the application. WDYT?
There was a problem hiding this comment.
Just chiming in, I think we're just defining the metrics in this PR and then actually recording them later. We might change the actual metric type later on when we look at how it's going to actually get recorded, so maybe we shouldn't worry too much about the selected metric implementation right now.
| } | ||
|
|
||
| public Map<String, String> taskLevelTagMap(final String taskName) { | ||
| final Map<String, String> tagMap = threadLevelTagMap(); |
There was a problem hiding this comment.
Actually, I'm not sure what's going on with threadLevelTagMap(String...).
It has exactly one usage, which is streamsMetrics.threadLevelTagMap(TASK_ID_TAG, ALL_TASKS) which seems to be doing exactly the same thing this method is doing.
Maybe we can just get rid of threadLevelTagMap(String...) and migrate the usage to this method?
| return tagMap; | ||
| } | ||
|
|
||
| public Map<String, String> storeLevelTagMap(final String taskName, final String storeType, final String storeName) { |
There was a problem hiding this comment.
Can we just add https://checkstyle.sourceforge.io/config_sizes.html#LineLength so we never have to have this discussion again? :)
| private static final String MEMTABLE_BYTES_FLUSHED = "memtable-bytes-flushed"; | ||
| private static final String MEMTABLE_HIT_RATIO = "memtable-hit" + RATIO_SUFFIX; | ||
| private static final String MEMTABLE_FLUSH_TIME = "memtable-flush-time"; | ||
| private static final String WRITE_STALL_DURATION = "write-stall-duration"; |
There was a problem hiding this comment.
Maybe @abbccdda was saying to store the description strings in static final fields as well? I'd agree with this, just to be sure that we don't waste memory on duplicate description strings.
| "The average per-second number of " + operation); | ||
| } | ||
|
|
||
| public static void addAmountRateAndTotalMetricsToSensor(final Sensor sensor, |
There was a problem hiding this comment.
Ok, I took a closer look and had a bit of a flash-back to how confusing these metrics are... Maybe #7057 will help.
| sensor.add(new MetricName(name, group, description, tags), new Value()); | ||
| } | ||
|
|
||
| public static void addAvgAndTotalMetricsToSensor(final Sensor sensor, |
There was a problem hiding this comment.
| public static void addAvgAndTotalMetricsToSensor(final Sensor sensor, | |
| public static void addAvgAndSumMetricsToSensor(final Sensor sensor, |
| final String storeName) { | ||
| final Sensor sensor = streamsMetrics | ||
| .storeLevelSensor(taskName, storeName, COMPACTION_TIME + MIN_SUFFIX, RecordingLevel.DEBUG); | ||
| addValueMetricToSensor(sensor, |
There was a problem hiding this comment.
Just chiming in, I think we're just defining the metrics in this PR and then actually recording them later. We might change the actual metric type later on when we look at how it's going to actually get recorded, so maybe we shouldn't worry too much about the selected metric implementation right now.
| verifyAll(); | ||
| verify(StreamsMetricsImpl.class); | ||
|
|
||
| assertThat(sensor, is(this.sensor)); |
There was a problem hiding this comment.
Hey, I'm sorry, but can you explain what's going on here? Sensor doesn't override equals, so I'm not seeing how this assertion works.
There was a problem hiding this comment.
Here I am just verifying that the methods in RocksDBMetrics pass through the sensor they get from the methods in the StreamsMetricImpl mock. It is an equality check on the reference. Before verifying the sensor, I also verify through the mock whether the metrics with the correct names and tags are added to the sensor.
There was a problem hiding this comment.
Oh, right, I forgot that the metrics registry returns the same copy of the sensor when all the name, description, and tags are the same... Thanks.
|
Failures unrelated |
|
Retest this, please |
| "The average per-second number of " + operation); | ||
| } | ||
|
|
||
| public static void addAmountRateAndTotalMetricsToSensor(final Sensor sensor, |
| addTotalMetricToSensor(sensor, group, tags, operation, descriptionOfTotal); | ||
| } | ||
|
|
||
| public static void addAmountRateMetricToSensor(final Sensor sensor, |
There was a problem hiding this comment.
This is not for this PR, but: since in #7057 we are cleaning up the confusing names shall we do the same for function names as well as a follow-up? Also the existing function names do not have ..ToSensor suffix, could we make it consistent (I'm fine with either way, just want to do the same for new and existing functions)?
There was a problem hiding this comment.
To make the function name consistent with the Stat name, should this be addRateOfSum?
There was a problem hiding this comment.
This is not for this PR, but: since in #7057 we are cleaning up the confusing names shall we do the same for function names as well as a follow-up? Also the existing function names do not have
..ToSensorsuffix, could we make it consistent (I'm fine with either way, just want to do the same for new and existing functions)?
I agree with you that we should be consistent. I just did not want to make this PR bigger than needed since it is already quite big. I will open a refactoring PR for this.
There was a problem hiding this comment.
To make the function name consistent with the Stat name, should this be
addRateOfSum?
Done
| new Rate(TimeUnit.SECONDS, new Sum())); | ||
| } | ||
|
|
||
| public static void addTotalMetricToSensor(final Sensor sensor, |
| } | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
This is a meta comment: for SampledStat their recorded values can vary based on the windowing itself, so in unit test it may not always has the exact value, i.e. asserting equalTo(value) may make this flaky, whereas just testing it is non-zero is sufficient.
There was a problem hiding this comment.
IIUC, since SampledStat metrics do not maintain any clock internally and I use MockTime in the unit tests, the values recorded in SampledStat should be deterministic. Please, correct me if I am missing anything.
There was a problem hiding this comment.
If we are using MockTime then it should be fine.
| "Total number of bytes read from the RocksDB state store"; | ||
| private static final String MEMTABLE_BYTES_FLUSHED_RATE_DESCRIPTION = | ||
| "Average number of bytes flushed per second from the memtable to disk"; | ||
| private static final String MEMTABLE_BYTES_FLUSHED_AVG_DESCRIPTION = |
There was a problem hiding this comment.
The description Total number ... seems incorrect for AVG?
There was a problem hiding this comment.
Oh... Thank you! Actually, it is the other way around. The description is correct, but the variable name is wrong.
| "Minimum time spent on flushing the memtable to disk in ms"; | ||
| private static final String MEMTABLE_FLUSH_TIME_MAX_DESCRIPTION = | ||
| "Maximum time spent on flushing the memtable to disk in ms"; | ||
| private static final String WRITE_STALL_DURATION_AVG_DESCRIPTION = "Moving average duration of write stalls in ms"; |
There was a problem hiding this comment.
Moving average duration may be a bit confusing to readers, maybe just Average duration of ...
There was a problem hiding this comment.
I will change it to Average duration to be consistent with the other average metrics. However, I think we risk wrong expectations on user side if we do not improve the descriptions of the metrics. Especially, with having compaction-time-avg which is not a moving average, but it has the following description Average time spent on compaction in ms.
|
Retest this, please |
1 similar comment
|
Retest this, please |
|
Merged to trunk, thanks @cadonna !! |
RocksDBMetricsclass that provides methods to getsensors from the Kafka metrics registry and to setup the
sensors to record RocksDB metrics
StreamsMetricsImplwith functionality to add therequired metrics to the sensors.
Committer Checklist (excluded from commit message)