streams mode: purge deleted stream metric series from exporters - #487
streams mode: purge deleted stream metric series from exporters#487squiidz wants to merge 1 commit into
Conversation
|
Commits
Review Reviewed the added optional series-deletion plumbing: The forwarding chain checks out end to end: LGTM |
Deleting a stream via DELETE /streams/{id} (or replacing it via an
update) stopped the stream and removed it from the API, but its metric
series (labeled stream="<id>") remained registered in the shared
metrics exporter with frozen values until the process restarted. Pull
based exporters such as prometheus therefore accumulated series for
every stream ever deleted, growing cardinality without bound for
workloads that churn streams.
Per-stream metrics are label children of process-wide shared vecs
(stats.WithLabels("stream", id)), so no per-stream object exists whose
lifetime could clean them up, and no deletion concept existed anywhere
in the metrics plumbing.
This adds one:
- service.MetricsExporterSeriesDeleter, an optional interface for
metrics exporter plugins that can delete all series matching a set of
label values.
- metrics.LabelPurger, the internal equivalent, implemented by
Namespaced (forwarding to its child when supported) and by the
air-gapped plugin wrapper (forwarding to the exporter when it
implements MetricsExporterSeriesDeleter).
- The stream manager now purges series matching {stream: <id>} after a
stream is stopped and removed by Delete.
Exporters that do not implement the optional interface are unaffected.
Note that recreating a stream with a previously deleted id restarts its
counters from zero, which scrapers treat as an ordinary counter reset.
4a10abe to
edac8b8
Compare
| // Purge the deleted stream's metric series from exporters that support | ||
| // deletion, otherwise they accumulate (and are exposed by pull based | ||
| // exporters) for the lifetime of the process. | ||
| if purger, ok := m.manager.Metrics().(metrics.LabelPurger); ok { | ||
| purger.DeleteSeriesPartialMatch(map[string]string{"stream": id}) | ||
| } |
There was a problem hiding this comment.
The purge runs after m.lock has been released, which opens a window where it can wipe the series of a newly created stream that reuses the same id.
Interleaving:
Delete("foo")removesfoofromm.streamsand releasesm.lock.- A concurrent
Create("foo", ...)acquiresm.lock, builds the stream and callsstrm.TriggerStartConsuming()— the new stream begins emitting series labelledstream="foo"— and returns. Delete'sDeleteSeriesPartialMatch({stream: "foo"})then runs and deletes those series.
This is worse than the "straggling write re-creates a series" case noted in the PR description: for an exporter like Prometheus, DeletePartialMatch unregisters the children, but the running components already hold references to the metric objects they were handed, so their increments no longer surface in the registry. The live stream silently loses its metrics for its whole lifetime rather than just resetting.
Since Create holds m.lock across the entire construct-and-start sequence, performing the purge inside the same critical section that does delete(m.streams, id) closes the window — a concurrent Create for that id then cannot have started emitting before the purge completes.
The sequential Update path (Delete then Create) is unaffected; this only shows up with concurrent API requests for the same stream id.
|
Commits Review One issue:
|
Ref: CON-555
Problem
In streams mode,
DELETE /streams/{id}stops the stream and removes it from the API, but its metric series (labeledstream="<id>") remain registered in the shared metrics exporter forever. Pull-based exporters such as prometheus keep exposing every deleted stream's counters/gauges/timers with frozen values until the process restarts, growing cardinality without bound for workloads that churn streams:Per-stream metrics are label children of process-wide shared vecs (
stats.WithLabels("stream", id)), so there is no per-stream object whose lifetime could clean them up, and no deletion concept existed anywhere in the metrics plumbing.Fix
service.MetricsExporterSeriesDeleter, an optional interface for metrics exporter plugins that can delete all series matching a set of label values (DeleteSeriesPartialMatch(labels map[string]string)).metrics.LabelPurger, the internal equivalent, implemented byNamespaced(forwarding to its child when supported) and by the air-gapped plugin wrapper (forwarding to the exporter when it implements the public interface).{stream: <id>}after a stream is stopped and removed byDelete.Exporters that do not implement the optional interface are unaffected (the purge is a no-op).
The prometheus exporter in redpanda-connect implements the new interface via
DeletePartialMatchin a follow-up PR; with both in place, the reproduction above reports 21 series before delete and 0 after.Notes
Updateis implemented asDelete+Create, so updating a stream now also resets that stream's series. For prometheus this is an ordinary counter reset, which scrapers handle; it also matches the semantics of the stream restarting.Testing
Namespacedforwarding (supported and unsupported child), the air-gap forwarding (supported and unsupported exporter), and the stream manager purging exactly{stream: <id>}on delete and not purging on failed deletes.