Skip to content

Remove the two deprecated Summary metrics - #599

Open
FZambia wants to merge 2 commits into
masterfrom
perf/drop-deprecated-summaries
Open

Remove the two deprecated Summary metrics#599
FZambia wants to merge 2 commits into
masterfrom
perf/drop-deprecated-summaries

Conversation

@FZambia

@FZambia FZambia commented Jul 29, 2026

Copy link
Copy Markdown
Member

Removes client_command_duration_seconds and node_survey_duration_seconds, the two Prometheus Summary instruments, along with the machinery that existed only to support them (dualObserver, noopObserverVec, noopObserver).

Both were already marked DEPRECATED in their own Help text, both already had a _histogram companion recording the same observations unconditionally, and both were suppressed entirely when EnableNativeHistograms was set.

Why now: it is a node-wide lock on the command path

This started as an unrelated investigation into broadcast lock contention, where this turned up as by far the largest contention point in the whole system.

A Prometheus Summary with objectives (here {0.5, 0.99, 0.999}) maintains a streaming quantile estimator behind a mutex, so every Observe takes a lock. Command duration is observed once per command, for every connection on the node — so all connections serialize on one lock per label set.

Mutex profile, 16 connections issuing commands concurrently:

prometheus.(*summary).Observe   142353704us  92.55%   of all mutex delay

92.5% of all contention in that workload was this one metric.

Measured effect

Probe issuing presence commands from 16 concurrent connections, benchstat n=10, default config on both sides:

sec/op
before 469.3n ± 5%
after 237.3n ± 3%
-49.4% (p=0.000)

Command handling is roughly 2× faster. Because the lock is node-wide rather than per-connection, the gap should widen with connection count — this was measured at 16 connections.

Breaking change

This changes the exposed metric surface. Anyone still scraping the Summary names loses the {quantile="..."} series, and _sum / _count move to the _histogram names.

Migration is histogram_quantile() over the companions that already exist:

{ns}_client_command_duration_seconds  ->  {ns}_client_command_duration_seconds_histogram
{ns}_node_survey_duration_seconds     ->  {ns}_node_survey_duration_seconds_histogram

For example p99 becomes:

histogram_quantile(0.99, sum by (le, method) (rate({ns}_client_command_duration_seconds_histogram_bucket[5m])))

Users who had already set EnableNativeHistograms: true see no change at all — the Summaries were already suppressed for them.

Migrating dashboards and alerts

Names verified against a live registry on this branch. Default namespace is centrifuge (MetricsConfig.MetricsNamespace).

removed (Summary) replacement (Histogram) labels
centrifuge_client_command_duration_seconds centrifuge_client_command_duration_seconds_histogram method, channel_namespace (+ app_* if ClientLabels set)
centrifuge_node_survey_duration_seconds centrifuge_node_survey_duration_seconds_histogram op

_sum and _count move too. This is the easiest breakage to miss: a Summary exports _sum/_count under its base name, so centrifuge_client_command_duration_seconds_count disappears along with the quantiles. It becomes centrifuge_client_command_duration_seconds_histogram_count.

Query translations

Quantiles — the quantile label is gone, use histogram_quantile() over the _bucket series:

# before
centrifuge_client_command_duration_seconds{quantile="0.99"}

# after
histogram_quantile(0.99, sum by (le) (rate(centrifuge_client_command_duration_seconds_histogram_bucket[5m])))

# after, broken down by method (keep `le` in the grouping)
histogram_quantile(0.99, sum by (le, method) (rate(centrifuge_client_command_duration_seconds_histogram_bucket[5m])))

Average latency and throughput — just the longer metric name:

# before
rate(centrifuge_client_command_duration_seconds_sum[5m]) / rate(centrifuge_client_command_duration_seconds_count[5m])
rate(centrifuge_client_command_duration_seconds_count[5m])

# after
rate(centrifuge_client_command_duration_seconds_histogram_sum[5m]) / rate(centrifuge_client_command_duration_seconds_histogram_count[5m])
rate(centrifuge_client_command_duration_seconds_histogram_count[5m])

Surveys are the same shape with the op label:

histogram_quantile(0.99, sum by (le, op) (rate(centrifuge_node_survey_duration_seconds_histogram_bucket[5m])))

Buckets refined in this PR

histogram_quantile() interpolates linearly inside whichever bucket the quantile falls into, assuming observations are spread evenly across it. Real latency distributions decay, so a wide bucket biases the estimate high. The previous layout did not resolve the range Centrifuge runs in — commands complete in the microsecond range, a couple of hundred microseconds being normal, but the finest boundary was 100µs and there was a 5x step from 1ms to 5ms.

Both are fixed here. Simulated over 300k samples:

quantile true old buckets new buckets
typical node (median ~200µs) p50 200.8µs 212.4µs (+5.8%) 212.4µs (+5.8%)
p90 740.4µs 834.4µs (+12.7%) 834.4µs (+12.7%)
p99 2611.6µs 4660.5µs (+78.5%) 2749.2µs (+5.3%)
p99.9 35.3ms 40.3ms (+14.3%) 40.3ms (+14.3%)
fast node (median ~50µs) p50 50.3µs 62.3µs (+24.0%) 50.5µs (+0.4%)
p90 142.7µs 185.7µs (+30.1%) 185.7µs (+30.1%)
p99 392.4µs 461.7µs (+17.7%) 461.7µs (+17.7%)
p99.9 8.5ms 9.0ms (+5.5%) 9.0ms (+5.5%)

Added 25µs and 50µs at the bottom and 2ms/3ms across the gap; the survey histogram gets the same 1ms..5ms fill. The change is additive — every boundary that existed before is still present, so le= selectors in dashboards, recording rules and SLO ratios keep matching and only gain resolution. Command histogram goes 15 → 19 buckets.

command: 25µs 50µs 100µs 250µs 500µs 1ms 2ms 3ms 5ms 10ms 25ms 50ms 100ms 250ms 500ms 1s 2.5s 5s 10s
survey:                              1ms 2ms 3ms 5ms 10ms 25ms 50ms 100ms 250ms 500ms 1s 2.5s 5s 10s

Every percentile is queryable, p99.9 included — accuracy depends on which bucket the quantile lands in, not on how extreme it is.

Still approximate. Even refined, a histogram estimates where a Summary stored real observed values, so numbers will move somewhat: re-check alert thresholds rather than copying them across. And p90 on a fast node still reads ~30% high (142µs falls in the 100µs..250µs bucket) — closing that needs a boundary near 150µs, which did not seem worth further cardinality without evidence that p90 is something operators alert on. Say so if it is.

For anything precise, prefer a ratio over a quantile. Bucket counts are exact — only interpolation between them is approximate — so sum(rate(..._histogram_bucket{le="0.005"}[5m])) / sum(rate(..._histogram_count[5m])) answers "what fraction of commands completed under 5ms" with no estimation at all, at whatever percentile you care about.

The largest finite bucket is 10s. If a quantile falls into +Inf, histogram_quantile() returns 10 rather than extrapolating. Use the ratio form to detect a genuinely slow tail.

One thing that gets better

Summary quantiles could not be aggregated across instances — averaging pre-computed quantiles is statistically meaningless, so per-node graphs were the only correct option. Histogram buckets add cleanly, so sum by (le) over all pods gives a correct fleet-wide quantile. Queries that previously had to be per-instance can now be global.

If you set EnableNativeHistograms

Buckets become native/exponential and there is no _bucket series to sum — query the metric directly:

histogram_quantile(0.99, sum(rate(centrifuge_client_command_duration_seconds_histogram[5m])))

For busy dashboards

histogram_quantile over high-cardinality buckets is more expensive than reading a Summary quantile. If a dashboard felt instant before and does not now, a recording rule restores it:

- record: centrifuge:command_duration_seconds:p99
  expr: histogram_quantile(0.99, sum by (le, method) (rate(centrifuge_client_command_duration_seconds_histogram_bucket[5m])))

Open question for review

The histograms keep their _histogram suffix, which only existed to avoid colliding with the Summary base name. Renaming them to the base names would be tidier, but I deliberately did not do it here: it would break everyone who has already migrated to the _histogram names, and it would reuse a metric name with a changed instrument type. Happy to follow up separately if you want that.

Other changes

  • EnableNativeHistograms docs no longer describe suppressing Summaries — there are none left to suppress. Its remaining meaning (classic explicit buckets vs native sparse schema) is unchanged.
  • _examples/native_histograms_otel/readme.md updated for the same reason.

Testing

New TestMetrics_NoSummariesExposed pins the removal under the default config — the path that actually needed guarding, since summaries were previously only suppressed behind the flag. It asserts the Summary names are gone, the _histogram companions still carry the data, and that no Summary instrument remains anywhere in the registry. It fails on master and passes here.

Full suite and go vet ./... clean. gofmt: the struct-field block was re-aligned as a consequence of removing the longest field names; the file has exactly the same number of pre-existing gofmt deviations as on master, so no new formatting noise was introduced.

client_command_duration_seconds and node_survey_duration_seconds were
Summary instruments carrying objectives {0.5, 0.99, 0.999}. Both were
already marked DEPRECATED in their Help text, both already had a
_histogram companion recording the same observations unconditionally, and
both were suppressed entirely when EnableNativeHistograms was set. This
removes them and the machinery that existed only to support them:
dualObserver, noopObserverVec and noopObserver.

The motivation is not only surface area. A Prometheus Summary with
objectives keeps a streaming quantile estimator behind a mutex, so every
Observe takes a lock — and command duration is observed once per command
for every connection on the node. That made it a node-wide serialization
point on the command path. In a mutex profile of 16 connections issuing
commands concurrently, prometheus.(*summary).Observe accounted for 92.5%
of all contention.

Measured with a probe issuing presence commands from 16 concurrent
connections, benchstat n=10, default config on both sides:

  before  469.3n ± 5%
  after   237.3n ± 3%   -49.44% (p=0.000)

Command handling is roughly 2x faster, and because the lock is shared by
the whole node rather than per connection, the gap should widen with
connection count.

This is a breaking change for the metric surface. Anyone still scraping
the Summary names loses the {quantile="..."} series, and the _sum and
_count series move to the _histogram names. The migration is to
histogram_quantile() over the existing companions:

  {ns}_client_command_duration_seconds -> ..._seconds_histogram
  {ns}_node_survey_duration_seconds    -> ..._seconds_histogram

EnableNativeHistograms keeps its remaining meaning — classic explicit
buckets versus native sparse schema — and its documentation no longer
describes suppressing Summaries, since there are none left to suppress.
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.49%. Comparing base (3ed0458) to head (3073e25).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #599      +/-   ##
==========================================
- Coverage   85.51%   85.49%   -0.03%     
==========================================
  Files          58       58              
  Lines       15576    15532      -44     
==========================================
- Hits        13320    13279      -41     
+ Misses       1603     1598       -5     
- Partials      653      655       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ns in

Removing the Summaries leaves histogram_quantile() as the only way to get
command latency percentiles, and the existing bucket layout does not resolve
the range Centrifuge actually operates in. Commands complete in the
microsecond range - a couple of hundred microseconds is normal - but the
finest boundary was 100us, and there was a 5x step from 1ms to 5ms.

Both gaps distort exactly the quantiles people watch. histogram_quantile()
interpolates linearly inside a bucket, assuming observations are spread
evenly across it; real latency distributions decay, so a wide bucket biases
the estimate high. Simulated over 300k samples:

  typical node, median ~200us
    p99    true 2611.6us   old 4660.5us (+78.5%)   new 2749.2us (+5.3%)

  fast node, median ~50us
    p50    true   50.3us   old   62.3us (+24.0%)   new   50.5us (+0.4%)

The median being interpolated from zero on a fast node is the more
fundamental of the two: with no boundary below 100us there is nothing to
interpolate between.

Add 25us and 50us at the bottom, and 2ms and 3ms across the gap; the survey
histogram gets the same 1ms..5ms fill, since node-to-node round trips sit in
that range and it had the identical step.

The change is additive: every boundary that existed before is still present,
so le= selectors in dashboards, recording rules and SLO ratios keep matching
and only gain resolution. Command histogram goes from 15 buckets to 19, an
increase in series per method/channel_namespace combination that seemed a
fair price for the median and p99 being roughly right.

Untouched: p90 on a fast node still reads ~30% high, since 142us falls in the
100us..250us bucket. Closing that needs a boundary near 150us, which did not
seem worth a further increase in cardinality without evidence that p90 is
something operators alert on.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant