perf(shards): stream /_cat/shards decoding to cut peak memory - #1187
Open
pincher95 wants to merge 1 commit into
Open
perf(shards): stream /_cat/shards decoding to cut peak memory#1187pincher95 wants to merge 1 commit into
pincher95 wants to merge 1 commit into
Conversation
The Shards collector decoded the entire /_cat/shards?format=json array into a []ShardResponse before aggregating started-shard counts per node. On clusters with many thousands of shards this slice is held live for the whole aggregation, and under concurrent /probe scrapes it is multiplied per request. Decode the array one shard at a time with encoding/json's streaming token API and aggregate directly into the per-node map, so retained heap is proportional to the node count rather than the full shard list — the same approach used for /_all/_stats in prometheus-community#1159. Decoded output is unchanged. Empirical suite added in collector/shards_stream_test.go (mirrors prometheus-community#1159): - TestStreamShardsEquivalence: streaming decode yields byte-for-byte the same shards and the same per-node STARTED aggregation as the previous whole-array json decode, over the real fixture and a synthetic payload. - BenchmarkShardsDecode / BenchmarkShardsDecodeHTTP: ~85% fewer bytes/op across 139 KB–7 MB payloads, including over a real httptest *http.Response.Body; throughput unchanged (allocation count rises ~33% from more, smaller short-lived per-element decodes). - BenchmarkShardsRetainedHeap: live heap retained while the collector works drops from ~5.4 MB (whole slice) to ~34 KB (per-node map only) at 50k shards. Signed-off-by: pincher95 <yuri.tsuprun@logz.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Streams
/_cat/shardsdecoding one shard at a time so the Shards collector's retained heap is proportional to the cluster's node count instead of the full shard list. Same approach #1159 applied to/_all/_stats.Motivation
Shards.Collectdecoded the entire/_cat/shards?format=jsonresponse into a[]ShardResponseand then aggregated STARTED shards per node. On a cluster with many thousands of shards that slice (a struct with four retained strings per shard) stays live for the whole aggregation, and in multi-target/probemode it is allocated per concurrent request. Only the small per-node count map is actually needed.Change
streamShardsdecodes the JSON array element-by-element viaencoding/json's token API;streamAndAggregateShardsfeeds each shard straight into the per-node map through the sharedfetchURLhelper.Collectnever materializes[]ShardResponse. Decoded output is unchanged.Benchmarks
darwin/arm64, go1.26,go test ./collector/ -bench BenchmarkShards -benchmem. Synthetic/_cat/shardspayloads modeled on the real fixture (all 8 API fields per object, ~1/20 UNASSIGNED with nulls, node cardinality scaled with shard count).Allocation / throughput (
BenchmarkShardsDecode,BenchmarkShardsDecodeHTTP):Allocation count rises ~33% (more, smaller, short-lived per-element decodes); total bytes — the driver of GC pressure — drop ~85%. Throughput is unchanged.
Retained live heap (
BenchmarkShardsRetainedHeap, 50,000 shards / 7.1 MB / ~500 nodes) — heap still live after decode+aggregate, i.e. what the collector holds during the emit loop:[]ShardResponse+ map)The buffered slice is eliminated; only the per-node map (which scales with node count, not shard count) is retained — ~160× lower for this case. This benchmark measures post-GC retained heap, a conservative lower bound on the buffered path's transient peak (which also holds the read buffer and decode garbage); sub-baseline GC jitter is clamped to zero, so the streaming figure is a small near-floor number.
raw benchmark output
Testing
TestStreamShardsEquivalence: streaming decode produces byte-for-byte the same shards and the same per-node STARTED aggregation as the previous whole-arrayjson.Unmarshal, over the real fixture (fixtures/shards/7.15.0.json) and a synthetic payload.TestShardsunchanged and passing.go test -race ./...,go vet ./..., andgolangci-lint runare clean.Notes
Follows #1159, which streamed
/_all/_stats. The Shards collector remains a direct-registrationprometheus.Collector; no metric names or labels change.Fixes #1188