-
Notifications
You must be signed in to change notification settings - Fork 1k
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
feat: add storage utilization metrics #7868
Conversation
final long totalSpace = f.getTotalSpace(); | ||
final double percFree = percentage(freeSpace, (double) totalSpace); | ||
dataPoints.add(new MetricsReporter.DataPoint(sampleTime,"storage-usage", freeSpace)); | ||
dataPoints.add(new MetricsReporter.DataPoint(sampleTime,"storage-usage-perc", percFree)); |
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.
I found that looking at the raw free space number didn't give me a sense of how full my disk was, but maybe other people automatically know how much space there is total? For task level we don't have total file size so maybe we don't want it here either for consistency
ksqldb-engine/src/main/java/io/confluent/ksql/internal/MetricsReporter.java
Outdated
Show resolved
Hide resolved
ksqldb-engine/src/main/java/io/confluent/ksql/internal/UtilizationMetricsListener.java
Outdated
Show resolved
Hide resolved
final long totalSpace = f.getTotalSpace(); | ||
final long usedSpace = totalSpace - f.getFreeSpace(); | ||
final double percFree = percentage((double) usedSpace, (double) totalSpace); | ||
dataPoints.add(new MetricsReporter.DataPoint(sampleTime,"storage-usage", (double) usedSpace)); |
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.
nit: space after comma.
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.
Also: seems we are only keep three data points here since the next file's usage would simply overwrite the previous ones, is that intentional??
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.
Hmm yeah good point @guozhangwang. Looking back at this I'm really reporting storage usage by query which I do later. I switched it to aggregate all of the files and return the final result as the node reporting. I saw your comment back on the one-pager about if getting the query storage usage here with f.getFreeSpace
will be more accurate than aggregating sst-file-size
, I'll leave log lines in and see if I can get a read on that during benchmarks
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.
Makes sense, thanks.
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.
Made a second pass; but it seems this PR is not complete and ready for review yet? @lct45 please feel free to ping me when it's done.
); | ||
metrics.put(queryId, new ArrayList<>()); | ||
} | ||
// We've seen metric for this query before |
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.
nit: for this query's task
private final Collection<TaskStorageMetric> registeredMetrics; | ||
|
||
public UtilizationMetricsListener(final KsqlConfig config, final Metrics metricRegistry) { | ||
this.queryStorageMetrics = new ConcurrentHashMap<>(); |
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.
Could you elaborate a bit more about the differences of these three collections: the metrics
map, the queryStorageMetrics
map, and the registeredMetrics
set. From what I can see it seems:
queryStorageMetrics
is used for the per-query metrics, but it was not populated and updated anywhere.registeredMetrics
is used for per-query-task metricsmetrics
is also used for storing per-query metrics?
The relationships of these and how they are leveraged / updated are all not very clear to me.
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.
I think we can simplify things here by doing a few things:
First, repurpose TaskStorageMetric
as a general purpose gauge that you can use to implement measure by adding up other metrics. It already is this, you just need to drop the notion of a task ID from it. You can rename to AggregatedMetric
(or maybe even refactor RocksDBMetricCollector
to pull out that inner class and use it).
Then, maintain 2 maps:
Map<String, AggregatedMetric> taskStorageUsage
- this maps from QueryID to an aggregated metric that measures usage for the query. It's aggregated metric will contain all the state store metrics for that query.
Map<TaskID, AggregatedMetric> queryStorageUsage
- this maps from Task ID to an aggregated metric that measures usage for the task. It's aggregated metric will contain all the state store metrics for that task. You'll probably have to add a TaskID type here that includes the task ID and query ID.
Then, whenever you get an event for a new metric, first compute the QueryID and TaskID. If the task is new, then add a new task-level metric to the metric registry (metricsRegistry
) and add a new AggregatedMetric
to taskStorageUsage
. If the query is new, then add a new query-level metric to the metric registry and add a new AggregatedMetric
to queryStorageUSage
. Then, add the metric you're being notified on to the AggregatedMetric
for the task and query.
Similarly, on a notification that a state store metric is removed, remove the state store metric from the corresponding AggregatedMetric
instances. If the AggregatedMetric
instance is now empty, then unregister it from the metrics registry.
if (metrics.get(queryId).contains(taskId)) { | ||
// we have this task metric already, just need to update it | ||
resetMetric(metric); | ||
for (TaskStorageMetric storageMetric : registeredMetrics) { |
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.
Why not put this for loop into the resetMetric
as well??
ksqldb-engine/src/main/java/io/confluent/ksql/internal/UtilizationMetricsListener.java
Outdated
Show resolved
Hide resolved
} | ||
final String taskId = metric.metricName().tags().getOrDefault("task-id", ""); | ||
|
||
final String queryId = ""; |
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.
we should be able to get this from the task id or the thread id, but it depends on whether we're using the new consolidated runtime stuff or not.
currently the thread-id tag will have the form _confluent-ksql-<server id>query_<query ID>-<uuid>-StreamThread-0
for persistent queries, and the form _confluent-ksql-<server id>transient_<query ID>-<uuid>-StreamThread-0
for transient queries.
once the new runtime changes go in, you would need to look in the task ID. @ableegoldman can you advise on how @lct45 would get the query ID from the task ID for the new runtime?
private final Collection<TaskStorageMetric> registeredMetrics; | ||
|
||
public UtilizationMetricsListener(final KsqlConfig config, final Metrics metricRegistry) { | ||
this.queryStorageMetrics = new ConcurrentHashMap<>(); |
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.
I think we can simplify things here by doing a few things:
First, repurpose TaskStorageMetric
as a general purpose gauge that you can use to implement measure by adding up other metrics. It already is this, you just need to drop the notion of a task ID from it. You can rename to AggregatedMetric
(or maybe even refactor RocksDBMetricCollector
to pull out that inner class and use it).
Then, maintain 2 maps:
Map<String, AggregatedMetric> taskStorageUsage
- this maps from QueryID to an aggregated metric that measures usage for the query. It's aggregated metric will contain all the state store metrics for that query.
Map<TaskID, AggregatedMetric> queryStorageUsage
- this maps from Task ID to an aggregated metric that measures usage for the task. It's aggregated metric will contain all the state store metrics for that task. You'll probably have to add a TaskID type here that includes the task ID and query ID.
Then, whenever you get an event for a new metric, first compute the QueryID and TaskID. If the task is new, then add a new task-level metric to the metric registry (metricsRegistry
) and add a new AggregatedMetric
to taskStorageUsage
. If the query is new, then add a new query-level metric to the metric registry and add a new AggregatedMetric
to queryStorageUSage
. Then, add the metric you're being notified on to the AggregatedMetric
for the task and query.
Similarly, on a notification that a state store metric is removed, remove the state store metric from the corresponding AggregatedMetric
instances. If the AggregatedMetric
instance is now empty, then unregister it from the metrics registry.
); | ||
metricsSeen.put(queryId, new ArrayList<>()); | ||
} | ||
final TaskStorageMetric newMetric = new TaskStorageMetric( |
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.
I think you can simplify this bit with a few changes:
- make
metricsSeen
a map from query id to a map from task id toTaskStorageMetric
(e.g.Map<String, Map<String, TaskStorageMetric>
) - there's no need for
resetAndUpdateMetric
- you just need to calladd
onTaskStorageMetric
and the value will get replaced in the map. Also technically that case should never actually happen - consider adding a warning log
Then this bit can just be:
final TaskStorageMetric taskMetric;
if (!metricsSeen.get(queryId).contains(taskId)) {
taskMetric = new TaskStorageMetric(...);
metricsSeen.get(queryId).add(taskId, new taskMetric);
metricRegistry.addMetric(...)
} else {
taskmetric = metricsSeen.get(queryId).get(taskId);
}
taskMetric.add(metric);
This should simplify removal too since you don't have to iterate the whole list.
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.
What case should never happen, calling add on an existing TaskStorageMetric
? The rest of this makes sense and makes it cleaner
Matcher matcher = pattern.matcher(queryIdTag); | ||
final String queryId = matcher.find() ? matcher.group(1) : ""; | ||
// if we haven't seen a task for this query yet | ||
if (!metricsSeen.containsKey(queryId)) { |
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.
everything from this line down to the end of this method needs to be in a synchronized block. I wouldn't synchronize this whole method though because most of the time it's called it doesn't need to do anything (since most metrics will fail the initial check).
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.
Synchronized on metricsSeen
, not metricsRegistry
, right?
this.metricName = metricName; | ||
} | ||
|
||
private void add(final KafkaMetric metric) { |
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.
add, remove, and getValue need to be synchronized - streams threads may be creating/removing state stores while telemetry is reading the value
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.
never mind - noticed this is a ConcurrentHashMap
} | ||
} | ||
|
||
final BigInteger computeQueryMetric(final String queryId) { |
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 needs to be synchronized with metric addition and removal.
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.
Also, to be extra careful I would call metricValue
outside of the lock. metricValue
is going to call into Streams, which could in theory (though very unlikely) try to take a lock that another stream thread holds while waiting to get our lock (because that thread is trying to register a metric).
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.
Just to clarify, is this still true since the metric map is a concurrent hash map?
ksqldb-engine/src/main/java/io/confluent/ksql/internal/UtilizationMetricsListener.java
Outdated
Show resolved
Hide resolved
final MetricName nodeTotal = | ||
metricRegistry.metricName("node-storage-total", METRIC_GROUP); | ||
final MetricName nodeUsed = | ||
metricRegistry.metricName("node-storage-used", METRIC_GROUP); |
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.
@rodesai I can't remember what we landed on for node metrics - IIRC calculated storage used by doing (f.getTotalSpace - f.getFreeSpace) isn't actually accurate for what we want. Given that, do we just want to report f.getFreeSpace? Or f.getFreeSpace and f.getTotalSpace?
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.
Additionally for naming these + the other storage metrics, do we want to add a unit suffix? eg _bytes
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.
IIRC calculated storage used by doing (f.getTotalSpace - f.getFreeSpace) isn't actually accurate for what we want. Given that, do we just want to report f.getFreeSpace? Or f.getFreeSpace and f.getTotalSpace?
I thought we determined it was probably accurate, and the disparity we saw was very small (18MB) and probably explained by the workload continuing to run between the time you printed the metrics and the time you looked at df
. So we decided to get a PR out and we can test on a cloud instance rather than on our macbooks.
And yeah a _bytes
suffix makes sense - good call
final MetricName nodeTotal = | ||
metricRegistry.metricName("node-storage-total", METRIC_GROUP); | ||
final MetricName nodeUsed = | ||
metricRegistry.metricName("node-storage-used", METRIC_GROUP); |
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.
IIRC calculated storage used by doing (f.getTotalSpace - f.getFreeSpace) isn't actually accurate for what we want. Given that, do we just want to report f.getFreeSpace? Or f.getFreeSpace and f.getTotalSpace?
I thought we determined it was probably accurate, and the disparity we saw was very small (18MB) and probably explained by the workload continuing to run between the time you printed the metrics and the time you looked at df
. So we decided to get a PR out and we can test on a cloud instance rather than on our macbooks.
And yeah a _bytes
suffix makes sense - good call
ksqldb-engine/src/main/java/io/confluent/ksql/internal/StorageUtilizationMetrics.java
Outdated
Show resolved
Hide resolved
final String queryId = getQueryId(metric); | ||
|
||
// if we haven't seen a task for this query yet | ||
synchronized (metricsSeen) { |
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.
can you move this into another method like private synchronized void handleNewSstFilesSizeMetric(final KafkaMetric metric, final String taskId, final String queryId) {...
?
ditto for the other side of this in metricRemoval
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.
And we have to do this because the hashmap is a concurrent hashmap, right? So when we access it we need to do it in a synchronized fashion? Is there something different about using a synchronized function rather than a synchronized block or is the synchronized function easier to use
|
||
final String queryId = getQueryId(metric); | ||
final String taskId = metric.metricName().tags().getOrDefault("task-id", ""); | ||
final TaskStorageMetric taskMetric = metricsSeen.get(queryId).get(taskId); |
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.
move everything below this into another method like:
private synchronized void handleRemovedSstFileSizeMetric(...
|
||
private BigInteger computeQueryMetric(final String queryId) { | ||
BigInteger queryMetricSum = BigInteger.ZERO; | ||
for (Map.Entry<String, TaskStorageMetric> entry : metricsSeen.get(queryId).entrySet()) { |
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.
we need to synchronize the bit here that iterates over the map. However we don't want to synchronize the part that gets the metric (to eliminate the risk of a deadlock). So you can write this like:
private BigInteger computeQueryMetric(final String queryId) {
BigInteger queryMetricSum = BigInteger.ZERO;
for (final Supplier<BigInteger> gauge : getGaugesForQuery(queryId)) {
queryMetricSum = queryMetricSum.add(gauge.get());
}
return queryMetricSum;
}
private synchronized Collection<Supplier<BigInteger>> getGaugesForQuery(final String queryId) {
return metricsSeen.get(queryId).values().stream()
.map(v -> () -> v.getValue())
.collect(Collectors.toList());
}
import org.apache.kafka.common.metrics.MetricsReporter; | ||
import org.apache.kafka.streams.StreamsConfig; | ||
|
||
public class StorageUtilizationMetrics implements MetricsReporter { |
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.
nit: StorageUtilizationMetricsReporter
.
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.
Also I'm wondering when would we implement org.apache.kafka.common.metrics.MetricsReporter
and when we implement io.confluent.ksql.internal.MetricsReporter
, they seem to be serving the same purpose.
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.
final String queryIdTag = metric.metricName().tags().getOrDefault("thread-id", ""); | ||
final Pattern pattern = Pattern.compile("(?<=query_|transient_)(.*?)(?=-)"); | ||
final Matcher matcher = pattern.matcher(queryIdTag); | ||
return matcher.find() ? matcher.group(1) : ""; |
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.
Would we ever not find the match? When that happens we would not get the queryId we would put "" into metricsSeen
silently. Should we treat it as a bug instead?
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.
We should always find a match for queries, although my regex skills aren't incredible so it depends on if anyone else sees any issues on L 233 😉. But I think you're right - we should treat it as a bug. I'm hesitant to crash the app if the queryId isn't showing up for a metric, on the other hand, if we only log an error we likely won't know that we're missing metrics... We also don't want to continue reporting any of these metrics if we don't have a queryID imo, so maybe throwing is the right way to go. LMK what you think
|
||
private static class TaskStorageMetric { | ||
final MetricName metricName; | ||
private final Map<MetricName, KafkaMetric> metrics = new ConcurrentHashMap<>(); |
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.
Hmm, are we having more than one metric here? Since we are filtering on metric.metricName().name().equals("total-sst-files-size")
at the first place, we would end up with only that metric name right?
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.
From what I understand, total-sst-files-size
is broken down by store, so we may have multiple stores per task. The storeName
is included in the tag map so the metric names would be different if you had multiple stores under one task
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.
Ah yes, you're right :)
ksqldb-engine/src/main/java/io/confluent/ksql/internal/StorageUtilizationMetrics.java
Outdated
Show resolved
Hide resolved
final String taskId | ||
) { | ||
// remove storage metric for this task | ||
taskMetric.remove(metric); |
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.
See my other comment: if we would only ever have one metric under that task, then we can simplify this a bit.
c1e929a
to
b231ae0
Compare
nodeTotal, | ||
(Gauge<Long>) (config, now) -> baseDir.getTotalSpace() | ||
); | ||
metricRegistry.addMetric( |
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.
can you add a storage_utilization
metric that returns ((baseDir.getTotalSpace - baseDir.getFreeSpace) / baseDir.getTotalSpace)
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.
There should be no risk of division by 0 here, right? Since our baseDir
should always exist
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.
And do you want this all the way up to the metrics api + exposed to users?
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.
There should be no risk of division by 0 here, right? Since our baseDir should always exist
yeah that should never happen
And do you want this all the way up to the metrics api + exposed to users?
yeah, the max value of this metric (over all the nodes) is what we want to show users in cloud
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.
LGTM
a72c538
to
a6c9d24
Compare
LGTM. |
c95154a
to
ed287ab
Compare
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.
LGTM
import org.apache.kafka.common.metrics.MetricsReporter; | ||
import org.apache.kafka.streams.StreamsConfig; | ||
|
||
public class StorageUtilizationMetricsReporter implements MetricsReporter { |
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.
I don't understand why you are implementing a new MetricsReporter
here.
A MetricsReporter
is for reporting metrics via different mechanisms (JMX, etc.). For defining new metrics, you typically just create a new KafkaMetric
and call Metrics.addMetric().
Description
As part of the observability metrics we're adding in Q3, we'd like to see storage metrics. This adds storage metrics by node, task, and query.
Testing done
Tested locally that the metrics picked up, added unit tests, did benchmarking
Reviewer checklist