Repository navigation
perf: improve performance of metricsAggregator path by reducing memory allocations - #20724
Conversation
Signed-off-by: Callum Styan <[email protected]>
slice growth + unnecsesary repeated metric description creation. Signed-off-by: Callum Styan <[email protected]>
Signed-off-by: Callum Styan <[email protected]>
| } | ||
| } | ||
|
|
||
| var sinkMetric prometheus.Metric |
There was a problem hiding this comment.
Does this need to be a package global?
| } | ||
|
|
||
| // asPrometheus returns the annotatedMetric as a prometheus.Metric, it preallocates/fills by index, uses the aggregators | ||
| // |
| b.WriteString(ln) | ||
| } | ||
| for _, l := range extraLabels { | ||
| b.WriteByte("|"[0]) |
There was a problem hiding this comment.
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.
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.
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 <[email protected]>
Signed-off-by: Callum Styan <[email protected]>
| } | ||
| key := cacheKeyForDesc(name, baseLabelNames, extraLabels) | ||
| if d, ok := ma.descCache[key]; ok { | ||
| d.lastUsed = time.Now() |
There was a problem hiding this comment.
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.
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.
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.
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 <[email protected]>
Signed-off-by: Callum Styan <[email protected]>
Emyrk
left a comment
There was a problem hiding this comment.
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: