Skip to content

Implement Memory Limiter prototype - #2

Closed
dashpole wants to merge 72 commits into
mainfrom
memory_limiter_prototype
Closed

Implement Memory Limiter prototype#2
dashpole wants to merge 72 commits into
mainfrom
memory_limiter_prototype

Conversation

@dashpole

@dashpole dashpole commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Prometheus Memory Limiter: Implementation Plan & Design Document

This document defines the architectural specification, component design, integration points, and rollout for the Prometheus Memory Limiter (prometheus/proposals#76).


1. System Architecture Overview

The Memory Limiter acts as a centralized runtime circuit breaker. It periodically evaluates memory pressure using Go's runtime metrics (runtime/metrics) and coordinates load-shedding mitigations across Prometheus subsystems without modifying Go runtime tuning parameters (GOMEMLIMIT / GOGC are treated strictly as read-only inputs).

                               ┌───────────────────────────┐
                               │     Go Runtime Metrics    │
                               │  (/memory/classes/total,  │
                               │   /memory/classes/heap/   │
                               │    released, /gc/gomem-   │
                               │       limit:bytes)        │
                               └─────────────┬─────────────┘
                                             │ Poll (default 100ms)
                                             ▼
                               ┌───────────────────────────┐
                               │   Memory Limiter Engine   │
                               │   (pkg/memorylimiter)     │
                               │  - Pressure Calculation   │
                               │  - Transitions Tracking   │
                               │  - State Machine (OK,     │
                               │    SOFT_LIMIT, HARD_LIMIT)│
                               └─────────────┬─────────────┘
                                             │ State / Predicates
             ┌───────────────────────────────┼───────────────────────────────┐
             ▼                               ▼                               ▼
    ┌─────────────────┐             ┌─────────────────┐             ┌─────────────────┐
    │  Scrape Manager │             │   TSDB Engine   │             │   Web Handlers  │
    │  (Hard Limit)   │             │  (Soft Limit)   │             │  (Soft & Hard)  │
    │ - Skip scrapes  │             │ - Pause on-disk │             │ - 503 OTLP/RW   │
    │ - 0 WAL Appends │             │   compaction    │             │ - 503 Remote Rd │
    │ - No up=0 write │             │ - Keep Head/WAL │             │   & Federation  │
    └─────────────────┘             └─────────────────┘             └─────────────────┘
             │                                                               │
             ▼                                                               ▼
    ┌─────────────────┐                                             ┌─────────────────┐
    │  Rules Manager  │                                             │   Prometheus    │
    │  (Hard Limit)   │                                             │   Telemetry     │
    │ - Pause indep.  │                                             │ - State gauges  │
    │   rec. rules    │                                             │ - Seconds total │
    │ - Keep alerts   │                                             │ - Transitions   │
    └─────────────────┘                                             └─────────────────┘

2. Core Controller Engine (pkg/memorylimiter)

2.1 Package Interface and Data Types

package memorylimiter

import (
	"context"
	"log/slog"
	"sync"
	"time"
)

type LimiterState int

const (
	StateOK LimiterState = iota
	StateSoftLimit
	StateHardLimit
)

type MemoryLimiter interface {
	// State returns the current evaluation state.
	State() LimiterState

	// Granular mitigation predicates for zero-lock checking:
	AllowScrape() bool
	AllowOTLP() bool
	AllowRemoteWrite() bool
	AllowRemoteRead() bool
	AllowFederation() bool
	AllowBlockCompaction() bool
	AllowRecordingRules() bool

	// Dynamic config updates on SIGHUP
	ApplyConfig(cfg *Config) error
	
	// Lifecycle
	Start(ctx context.Context)
	Stop()
}

2.2 Memory Pressure Sensor & GC Pacer Accounting

The limiter queries runtime/metrics on a background ticker (default 100ms) with zero allocations.

// In-use calculation:
// Total mapped memory minus memory released back to the OS.
// in_use = /memory/classes/total:bytes - /memory/classes/heap/released:bytes
// gomemlimit = /gc/gomemlimit:bytes
// gc_limiter_enabled = /gc/limiter/last-enabled:gc-cycle

Sensor Derivation:

  1. In-Use Memory: Captures active heap objects, stack allocations, metadata, chunk allocations, and unreleased GC arenas without double-counting scavenged pages.
  2. GOMEMLIMIT Validation:
    • If /gc/gomemlimit:bytes returns math.MaxInt64 (unconfigured) when --enable-feature=memory-limiter is set, Prometheus fails fast at startup with an explicit error: memory limiter requires GOMEMLIMIT or --auto-gomemlimit to be configured.
  3. GC CPU Limiter Override:
    • If /gc/limiter/last-enabled:gc-cycle advanced in the last evaluation window, Go's GC CPU limiter has engaged (meaning Go is spending >50% CPU in GC trying to stay under GOMEMLIMIT). In this state, the memory limiter immediately escalates to StateHardLimit, regardless of whether in_use / GOMEMLIMIT has crossed hard_limit_ratio.

2.3 State Transitions & Flapping Observability

As specified in the proposal, state transitions are evaluated directly against the configured ratios:

  • Soft Limit: Reached when in_use / GOMEMLIMIT >= soft_limit_ratio (default 0.70).
  • Hard Limit: Reached when in_use / GOMEMLIMIT >= hard_limit_ratio (default 0.85) OR gc_limiter_active.
  • Transitions Tracking: Every transition increments prometheus_memory_limiter_transitions_total{from, to} and records time in prometheus_memory_limiter_engaged_seconds_total{limit}. This enables direct measurement of transition frequency and duty cycles to evaluate the impact of flapping under sustained boundary load.

3. Subsystem Integrations & Mitigations

3.1 Scrape Loop (scrape/scrape.go)

When AllowScrape() is false:

  1. Scrape Bypass: Skip network HTTP request, decompression, and body parsing entirely.
  2. Fast Failure & Zero WAL Write:
    • Return immediately with errScrapeMemoryLimitExceeded.
    • Update target status (for UI/API).
    • Do NOT append up=0 or staleness markers to the WAL. The transaction is bypassed, ensuring $0$ heap allocation and $0$ WAL I/O during memory exhaustion. Values carry forward under the standard 5-minute lookback.
  3. Telemetry: Increment prometheus_target_scrapes_skipped_total{job, instance}.

3.2 TSDB Compaction (tsdb/db.go, tsdb/compact.go)

Mitigation level: Soft Limit.

  1. Paused Component: DB.compactBlocks() (on-disk block merging). Pausing block compaction carries zero risk of data loss and does not affect ingestion capacity.
  2. Preserved Component: DB.CompactHead() and WAL truncation MUST continue running. Active Head memory and WAL disk size remain bounded.
  3. Implementation: An atomic boolean flag blockCompactionPaused checked before acquiring the block compaction lock in tsdb.DB.run().

3.3 HTTP Handlers (web/api/v1/api.go, web/web.go, otlp/receiver.go)

When respective limiters are active:

  1. OTLP Ingestion (/api/v1/otlp) & Remote Write Receiver (/api/v1/write):
    • Mitigation level: Hard Limit.
    • Checked at HTTP handler entry, before reading the request body or decompressing protobuf.
    • Return HTTP 503 Service Unavailable with header Retry-After: 5.
  2. Remote Read (/api/v1/read) & Federation (/federate):
    • Mitigation level: Soft Limit.
    • Checked at HTTP handler entry before initiating series postings queries.
    • Return HTTP 503 Service Unavailable with Retry-After: 5.

3.4 Rules Engine (rules/manager.go)

Mitigation level: Hard Limit.

  1. Alerting Rules: Never paused. Safety invariant: alerts must fire even under resource pressure.
  2. Recording Rules:
    • Parse PromQL expressions in rule groups to identify independent vs dependent recording rules.
    • Any recording rule whose output series name is not referenced in an alerting rule or downstream recording rule is skipped during Hard Limit.
    • Increment prometheus_rule_group_iterations_skipped_total{rule_group}.

4. Configuration Schema (config/config.go)

runtime:
  gogc: 100
  memory_limiter:
    # Evaluation interval for memory pressure
    check_interval: 100ms

    # Soft limit threshold (non-destructive mitigations)
    soft_limit_ratio: 0.70

    # Hard limit threshold (destructive mitigations)
    hard_limit_ratio: 0.85

    enforcement:
      # Soft Limit mitigations
      pause_block_compaction: true
      reject_remote_read: true
      reject_federation: true

      # Hard Limit mitigations
      fail_scrapes: true
      reject_otlp: true
      reject_remote_write: true
      pause_recording_rules: true

5. Exported Telemetry Specification

Metric Name Type Labels Description
prometheus_memory_limiter_active Gauge limit="soft|hard" 1 if the mitigation limit is currently engaged, 0 otherwise.
prometheus_memory_limiter_engaged_seconds_total Counter limit="soft|hard" Cumulative seconds spent in soft/hard mitigation states (for exact duty cycle calculation).
prometheus_memory_limiter_transitions_total Counter from, to Number of state machine transitions (for flapping detection).
prometheus_memory_limiter_limit_bytes Gauge limit="soft|hard" Absolute byte threshold computed from ratio and GOMEMLIMIT.
prometheus_memory_limiter_in_use_bytes Gauge In-use memory measured by controller (total - released).
prometheus_target_scrapes_skipped_total Counter job, instance Total scrapes skipped due to memory limits.
prometheus_tsdb_block_compaction_paused Gauge 1 if on-disk block compaction is paused by memory limiter.
prometheus_rule_group_iterations_skipped_total Counter rule_group Number of independent recording rule group iterations skipped.

@dashpole
dashpole force-pushed the memory_limiter_prototype branch from 30588f1 to 53c1496 Compare August 8, 2026 02:15
dashpole pushed a commit that referenced this pull request Aug 8, 2026
* Initial plan

* Add Azure AD certificate-based authentication support

Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>

* Update documentation for certificate-based authentication

Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>

* Address code review feedback - improve error messages and format detection

Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>

* Replace third-party go-pkcs12 with official golang.org/x/crypto/pkcs12

Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>

* Extract certificate parsing to common util/certutil package

Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>

* Fix linting errors: import ordering and comment formatting

Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bragi92 <28612268+bragi92@users.noreply.github.com>
@dashpole
dashpole force-pushed the memory_limiter_prototype branch from 9b499bf to 95978b4 Compare August 13, 2026 19:12
r0h1tb and others added 27 commits August 13, 2026 19:29
…l fields

Lightsail SD guarded only PrivateIpAddress before building the label set, then
dereferenced six more pointers unconditionally:

    lightsailLabelAZ:            model.LabelValue(*inst.Location.AvailabilityZone),
    lightsailLabelBlueprintID:   model.LabelValue(*inst.BlueprintId),
    lightsailLabelBundleID:      model.LabelValue(*inst.BundleId),
    lightsailLabelInstanceName:  model.LabelValue(*inst.Name),
    lightsailLabelInstanceState: model.LabelValue(*inst.State.Name),
    lightsailLabelInstanceSupportCode: model.LabelValue(*inst.SupportCode),

Every one of those is optional in the AWS API. Location is a *ResourceLocation
and State an *InstanceState, so those two chain through a second pointer each.
If the API omits any of them the refresh goroutine panics with a nil pointer
dereference and takes the whole Prometheus process down, rather than degrading
the single target.

Each label is now emitted only when its source field is present, matching the
guarding already used throughout elasticache.go. An instance that carries
nothing but a private IP still produces a scrapeable target.

Adds discovery/aws/lightsail_test.go; the package had no Lightsail coverage,
which is why this went unnoticed. Without the fix
TestLightsailRefreshAllOptionalFieldsNil panics at lightsail.go:262.

Signed-off-by: Rohit Behera <126186063+r0h1tb@users.noreply.github.com>
Signed-off-by: Minh Vu <vuhoangminh97@gmail.com>
Signed-off-by: Minh Vu <vuhoangminh97@gmail.com>
Serverless MSK clusters have no Provisioned configuration, and
provisioned clusters without Open Monitoring have a nil OpenMonitoring
field. The refresh label building dereferences both without checking,
crashing Prometheus.

Exposes prometheus#19184

Signed-off-by: Raj <rajeshrajendirandev@gmail.com>
Serverless MSK clusters return a nil Provisioned field and provisioned
clusters without Open Monitoring return a nil OpenMonitoring field.
The refresh label building dereferenced both unconditionally, crashing
the whole Prometheus process during service discovery. Guard the
dereferences and omit the affected labels when the configuration is
absent.

Fixes prometheus#19184

Signed-off-by: Raj <rajeshrajendirandev@gmail.com>
Serverless clusters do not expose broker nodes and are already excluded
from ListClustersV2 by the PROVISIONED filter, so an explicitly
configured serverless cluster ARN is now skipped with a warning instead
of being processed. Also document that the JMX and node exporter labels
are absent, not false, when Open Monitoring is not enabled.

Suggested by @matt-gp in the PR review.

Signed-off-by: Raj <rajeshrajendirandev@gmail.com>
Since describeClusters and listClusters both only return provisioned
clusters now, Provisioned and CurrentBrokerSoftwareInfo are always set;
only Open Monitoring remains optional.

Suggested by @matt-gp in review.

Signed-off-by: Raj <rajeshrajendirandev@gmail.com>
…9359)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…theus#19350)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…theus#19357)

* fix(deps): update module github.com/moby/moby/client to v0.5.1
* build: bump minimum Go version to 1.25.10

---------

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
… c8921c7 (prometheus#19244)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…rometheus#19109)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This fixes scroll issues in the native histogram buckets table and also
improves the display by aligning the values better and adding a light
background bar representing the bucket count.

Signed-off-by: Julius Volz <julius.volz@gmail.com>
…ometheus#19356)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…us#19347)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…0-beta.37 (prometheus#19358)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…#19362)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* promql: honour @ modifier when evaluating info() series

---------

Signed-off-by: Jeanette Tan <jeanette.tan@grafana.com>
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
* chore(deps): update modules

---------

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* fix(deps): update dependency react-router-dom to v7
* ui: migrate legacy app to React Router v7

---------

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
* chore(deps): update mantine ui

* chore(deps): keep eslint plugin at v0.5.3

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

---------

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
…s#18883)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: linasm <linas.medziunas@gmail.com>
roidelapluie and others added 28 commits August 13, 2026 19:30
…ing (prometheus#19401)

* tsdb: document that OOO chunk IDs are no longer monotonically increasing

Follow-up to prometheus#19216: the HeadChunkID doc still promised a monotonically
increasing per-series number, which no longer holds for out-of-order
chunk IDs now that they wrap modulo 2^23. Also spell out the "much less
than" relation in the oooHeadChunkID comment, where << is easy to
misread as a bit-shift.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

* Update tsdb/head_read.go

Co-authored-by: George Krajcsovits <krajorama@users.noreply.github.com>
Signed-off-by: Julien <291750+roidelapluie@users.noreply.github.com>

---------

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
Signed-off-by: Julien <291750+roidelapluie@users.noreply.github.com>
Co-authored-by: George Krajcsovits <krajorama@users.noreply.github.com>
…lockCompaction until TSDB integration is implemented
@dashpole
dashpole force-pushed the memory_limiter_prototype branch from 95978b4 to 33955d2 Compare August 13, 2026 19:31
@dashpole dashpole closed this Aug 14, 2026
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.