-
Notifications
You must be signed in to change notification settings - Fork 1.1k
perf: improve performance of metricsAggregator path by reducing memory allocations #20724
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
Conversation
Signed-off-by: Callum Styan <callumstyan@gmail.com>
slice growth + unnecsesary repeated metric description creation. Signed-off-by: Callum Styan <callumstyan@gmail.com>
| } | ||
| } | ||
|
|
||
| var sinkMetric prometheus.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.
Does this need to be a package global?
| } | ||
|
|
||
| // asPrometheus returns the annotatedMetric as a prometheus.Metric, it preallocates/fills by index, uses the aggregators | ||
| // |
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.
Spurious newline?
| b.WriteString(ln) | ||
| } | ||
| for _, l := range extraLabels { | ||
| b.WriteByte("|"[0]) |
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 use a character literal here and line 385
| b.WriteByte("|"[0]) | |
| b.WriteByte('|') |
| cleanupHistogram prometheus.Histogram | ||
| aggregateByLabels []string | ||
| // per-aggregator cache of descriptors | ||
| descCache map[string]*prometheus.Desc |
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'm just now learning how metrics are collected. To your question about cache strategy, I see there's already some timer based cleanup done by MetricAggregator. Might be easy to piggy-back on that for TTL.
I wonder if we can simplify/eliminate the map key and additional eviction by somehow storing a *prometheus.Desc alongside the corresponding annotatedMetric in the store map value. Then I think we could just call prometheus.NewDesc() and replace the stored descriptor when needed (which seems like it would be a fairly narrow criteria based on how hashKey() uniquely identifies an annotatedMetric).
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 obviously had some horse blinders on when writing this hey? Didn't even see that this cleanup routine for the metrics themselves existed. We definitely can piggy back on that 👍 However, we can't fully reuse the store since that is a cache of individual unique metrics (per-agent, for example) while the description caching is per-metric family (a metric name + generic labelset, but for multiple agents, we're caching the description/looking it up based on metric label names but not values).
cache cleanup Signed-off-by: Callum Styan <callumstyan@gmail.com>
Signed-off-by: Callum Styan <callumstyan@gmail.com>
| } | ||
| key := cacheKeyForDesc(name, baseLabelNames, extraLabels) | ||
| if d, ok := ma.descCache[key]; ok { | ||
| d.lastUsed = time.Now() |
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.
Does this update the timestamp in the map value? Since d is not a pointer I think this updates the timestamp in a copied entry.
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.
yep, you're right 👍 added a test to ensure the timestamp value is actually written and changed the line(s) just after this to reassign the new d to the map key
|
|
||
| // then | ||
| require.Eventually(t, func() bool { | ||
| return len(ma.descCache) == 0 |
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'm pretty sure there's a data race here. We may be reading len(ma.descCache) here while the loop in MetricsAggregator.Run() is writing to the map.
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.
it really would only have been updating the once when we first write the metric, but yes the race detector was complaining here
I've refactored the test to not need to call run so we can avoid this
Signed-off-by: Callum Styan <callumstyan@gmail.com>
Signed-off-by: Callum Styan <callumstyan@gmail.com>
Emyrk
left a comment
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.
Smart 👍
I am not aware of the frequency of these allocations, but I do imagine the quantity per metric collect is high since the number of agent metrics is probably growing.
If I am understanding this right, we hold those metric descriptions in memory. And this is across all agents, so the cache does not grow if more agents connect to the coderd.
The cache deletes metrics not seen in the last 2 minutes. In practice though, unless agent versions change, all metrics should be appearing every 2minutes. So this cache should realistically not purge anything right?
Keep the cache cleaning, just checking my understanding
For each coderd instance, it receives a stats/metrics update on
Correct, the existing
Yeah, |
NOTE: this is still WIP as it introduces in-memory caching of some data, and there is currently no cleanup of that cache. To be decided; do we want to keep the cache or not based on benchmark results (see below), and if so do we want to introduce LRU caching or TTL caching. I would lean towards basic TTL caching with cleanup via a goroutine unless there's an argument for more strict bounding of our memory usage here. We're likely already over allocating here at the moment, with this PR memory usage should be more stable for a given set of live Agents.
Again, poking around in our profiling data this seemed like a quick win. This will be more impactful in larger organizations/with larger #'s of agents per workspace.
The
MetricsAggregatorRunfunction and theasPrometheusfunction, which previously was attached to theannotatedMetrictype, are the source of ~11% of our memory allocations and ~7.5% of total memory allocated (24h period). This path itself is only ~1.6% of our CPU time but it should also have a knock on affect on garbage collection which is about ~7.6% of CPU time.The optimizations are relatively straightforward:
asPrometheusfrom theannotatedMetrictoMetricsAggregatorso we can cache metrics description structs for metrics (should be a big win, agents likely push the same metrics repeatedly over time)Benchmark Results: