Skip to content

feat(ingester): Add owned series tracking to prevent false throttling… - #7758

Open
anant10 wants to merge 1 commit into
cortexproject:masterfrom
anant10:feat/owned-series-tracking
Open

feat(ingester): Add owned series tracking to prevent false throttling…#7758
anant10 wants to merge 1 commit into
cortexproject:masterfrom
anant10:feat/owned-series-tracking

Conversation

@anant10

@anant10 anant10 commented Aug 11, 2026

Copy link
Copy Markdown

What this PR does:

Adds per-ingester series ownership tracking to prevent false throttling during ingester scale-up and ring resharding.

The problem: When ingesters scale up, the per-ingester local series limit drops immediately (recalculated based on new ingester count), but stale series data remains in the TSDB head for up to 2 hours until head compaction. PreCreation() uses Head().NumSeries() for limit checks, so it incorrectly rejects new writes during this window — the ingester appears over its new lower limit, but many of those series have been resharded to other ingesters and will be cleaned up at next compaction.

The solution: Track which series each ingester actually owns according to the ring, and use that count for limit enforcement. The owned count drops immediately when the ring changes (within 1 minute), eliminating the 2-hour dependency on head compaction.

How it works:

  1. On each push, compute the series' ring token (same hash the distributor uses for routing) and store it in ActiveSeries
  2. Every ~1 min (updateActiveSeries cycle), if the ring changed, re-scan all entries and remove series whose token no longer maps to this ingester
  3. PreCreation() uses activeSeries.Owned() instead of Head().NumSeries() for the limit check

Design decisions:

  • Two feature flags for safe progressive rollout:
    • -ingester.owned-series-metrics-enabled: enables cortex_ingester_owned_series metric emission only (no enforcement change)
    • -ingester.owned-series-limit-enforcement-enabled: switches limit enforcement to use owned count (requires first flag)
  • Zone-local ownership check (3 lines): SearchToken(zoneTokens, key) → is responsible token in this instance's set?
  • Lock-free reads on push path: ring state stored behind atomic.Pointer[ringState] — zero lock contention
  • Instance-level max_series: instanceOwnedCount recalculated every ~1 min (not incremental, avoids drift). Startup fallback to instanceSeriesCount when count is 0
  • Code consolidation: FNV hash → pkg/util/fnv.go, sharding functions → pkg/ring/token.go (eliminates duplication between distributor and ring)

Validation:

Tested in a multi-zone deployment under sustained write load. After scale-up:

  • memory_series on old ingesters remained high (stale data in head)
  • owned_series on old ingesters dropped proportionally to ring redistribution
  • Zero throttle errors during the scale-up window that would have previously caused false rejections

New configuration flags (experimental):

yaml

ingester:
 # Emit cortex_ingester_owned_series metric (no enforcement change)
 # CLI flag: -ingester.owned-series-metrics-enabled
 [owned_series_metrics_enabled: <bool> | default = false]

 # Use owned count for limit enforcement (requires above flag)
 # CLI flag: -ingester.owned-series-limit-enforcement-enabled
 [owned_series_limit_enforcement_enabled: <bool> | default = false]

Which issue(s) this PR fixes: Fixes #7509

… during ring changes

When ingesters scale up, the per-ingester local series limit drops immediately
but stale series data remains in TSDB head for up to 2 hours. This causes
PreCreation() to incorrectly reject new writes.

This PR introduces owned series tracking in ActiveSeries:
- Each series stores its ring token (computed via TokenForLabels)
- Ownership is evaluated against current ring state on each update cycle
- When ring changes, unowned series are excluded from limit enforcement

Two feature flags for safe rollout:
- owned_series_metrics_enabled: enables cortex_ingester_owned_series metric
- owned_series_limit_enforcement_enabled: switches PreCreation to use owned
  count for both per-user and instance-level max_series limits

Key design decisions:
- Zone-local ownership check via SearchToken + instance token map lookup
- Ring state stored behind atomic.Pointer[ringState] for lock-free reads
  on the hot push path
- instanceOwnedCount recalculated every ~1 min (not incremental) to avoid
  drift from edge cases
- Startup fallback: when instanceOwnedCount==0, uses instanceSeriesCount

Code consolidation:
- FNV hash functions consolidated into pkg/util/fnv.go (single source)
- Sharding functions moved to pkg/ring/token.go (eliminates duplication
  between distributor and ring packages)

Production validation: tested with 5M active series, scale-up 9->18
ingesters showed owned_series=984K vs memory_series=1.8M (813K stale
series correctly excluded). Zero throttle errors.

Fixes cortexproject#7509

Signed-off-by: Anant Shanbhag <anantvas@amazon.com>
@anant10
anant10 force-pushed the feat/owned-series-tracking branch from 97d8157 to 305bc32 Compare August 11, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ingester: Add owned series tracking to exclude stale data from per-ingester limit calculations

1 participant