Prometheus observability monitoring Kubernetes OOM

Prometheus OOMKilled przez high cardinality: diagnoza i naprawa

Naprawa Prometheus OOMKilled: identyfikacja metryk o wysokiej kardynalności, konfiguracja metric_relabel_configs do odrzucania zbędnych serii, limity serii czasowych i doboru pamięci.

Jerzy Kopaczewski ·
Prometheus restartuje się co kilka godzin z kodem OOMKilled. Każdy restart oznacza lukę w danych, utracone alerty i brak widoczności w krytycznym momencie. Przyczyna to prawie zawsze high cardinality - zbyt wiele unikalnych serii czasowych generowanych przez jedno źródło. Ten runbook pokazuje jak znaleźć winowajcę i wyeliminować problem bez utraty ważnych metryk.

Jeśli rozważasz przejście na managed observability (np. Datadog lub Grafana Cloud) aby uniknąć problemów z self-hosted Prometheus, zobacz nasz przewodnik po migracji do Datadog. Jeśli potrzebujesz optymalizacji kosztów observability, zobacz artykuł o rosnących kosztach monitoringu.

Objaw

Prometheus pod restartuje się z kodem 137 (OOMKilled):

# Sprawdź status pod
kubectl describe pod prometheus-server-0 -n monitoring | grep -A5 "Last State"
# Last State:     Terminated
#   Reason:       OOMKilled
#   Exit Code:    137

# Sprawdź aktualną liczbę serii czasowych
curl -s http://prometheus:9090/api/v1/status/tsdb | jq '.data.headStats.numSeries'

# Sprawdź pamięć
kubectl top pod prometheus-server-0 -n monitoring
# Typowy wzorzec: pamięć rośnie liniowo aż do limitu, potem OOM

# Ile serii na target?
curl -s http://prometheus:9090/api/v1/targets | jq '[.data.activeTargets[].scrapePool] | group_by(.) | map({pool: .[0], count: length})'

Reguła kciuka: Prometheus zużywa ~1-2 KB RAM per aktywna seria czasowa. 1 milion serii = ~1.5-2 GB RAM. Jeśli masz 500K serii i limit 2 GB, jesteś na granicy.

Przyczyna

High cardinality oznacza, że metryka generuje zbyt wiele unikalnych kombinacji label. Typowe źródła eksplozji:

1. Dynamiczne label z user/request ID

# Każdy unikalny user_id tworzy nową serię
http_requests_total{method="GET", path="/api/users", user_id="abc123"}
http_requests_total{method="GET", path="/api/users", user_id="def456"}
# ... x 100,000 użytkowników = 100K serii z jednej metryki

2. Label kontenerów/podów na krótkotrwałych workload

# Każdy nowy pod tworzy nową serię (pod_name zawiera UUID)
container_cpu_usage_seconds_total{pod="worker-7f8b9c4d2-xk4rm", ...}
# Po restarcie: nowy UUID = nowa seria, stara nie jest usuwana natychmiast

3. Metryki z nieograniczoną liczbą ścieżek (path)

# Każdy unikalny URL to nowa seria
http_request_duration_seconds_bucket{path="/api/users/12345/orders/67890", le="0.5"}
# Kombinacja paths x buckets x methods = eksplozja

4. Źle skonfigurowane ServiceMonitor lub PodMonitor

ServiceMonitor zbiera metryki z endpointu, który udostępnia wewnętrzne metryki frameworka z dynamicznymi label (np. gRPC per-method histogramy z pełnym path).

Naprawa

A) Zidentyfikuj metryki o najwyższej kardynalności

# Top 20 metryk po liczbie serii
curl -s http://prometheus:9090/api/v1/status/tsdb | jq -r '.data.seriesCountByMetricName | sort_by(.value) | reverse | .[0:20] | .[] | "\(.value)\t\(.name)"'

# Top 20 label par po liczbie serii
curl -s http://prometheus:9090/api/v1/status/tsdb | jq -r '.data.seriesCountByLabelValuePair | sort_by(.value) | reverse | .[0:20] | .[] | "\(.value)\t\(.name)"'

# Sprawdź konkretną metrykę
curl -s 'http://prometheus:9090/api/v1/series?match[]=http_request_duration_seconds_bucket' | jq '.data | length'

B) Odrzucanie zbędnych metryk przez metric_relabel_configs

# prometheus.yml lub w ServiceMonitor spec
scrape_configs:
  - job_name: 'my-app'
    metric_relabel_configs:
      # Odrzuć metryki z wysoką kardynalnością, których nie potrzebujesz
      - source_labels: [__name__]
        regex: 'go_gc_.*|go_memstats_.*|process_.*'
        action: drop

      # Usuń label powodujący eksplozję
      - regex: 'user_id|session_id|request_id|trace_id'
        action: labeldrop

      # Ogranicz path do znanych endpointów (zamień resztę na "other")
      - source_labels: [path]
        regex: '/(api|health|metrics|ready)(.*)'
        target_label: path
        replacement: '/${1}'
      - source_labels: [path]
        regex: '/(?!api|health|metrics|ready).*'
        target_label: path
        replacement: '/other'

W Kubernetes z ServiceMonitor:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app
spec:
  endpoints:
    - port: metrics
      metricRelabelings:
        - sourceLabels: [__name__]
          regex: 'go_gc_.*|go_memstats_.*'
          action: drop
        - regex: 'user_id|request_id'
          action: labeldrop

C) Ustaw limit serii na źródło (target)

# prometheus.yml - global sample limit
scrape_configs:
  - job_name: 'my-app'
    sample_limit: 5000  # Odrzuć scrape, jeśli źródło zwraca >5000 serii
    # Alert w logach: "scrape failed: sample limit exceeded"

D) Right-doboru pamięci

# Oblicz potrzebną pamięć:
# serie_czasowe * 2 KB = bazowe zużycie
# + 30% overhead na queries i compaction
# + 20% bufor bezpieczeństwa

# Przykład: 500K serii
# 500,000 * 2 KB = 1 GB bazowe
# + 30% = 1.3 GB
# + 20% = 1.56 GB -> ustaw limit na 2 GB

# Kubernetes resource limit:
resources:
  requests:
    memory: 1.5Gi
  limits:
    memory: 2Gi

E) Rozważ zapis zdalny (remote write) + krótszą retencję

Jeśli serii jest za dużo nawet po cleanup, wyślij dane do zewnętrznego storage (Thanos, Cortex, Mimir) i skróć lokalny okres przechowywania:

# prometheus.yml
global:
  scrape_interval: 30s  # Rozważ zwiększenie z 15s na 30s

storage:
  tsdb:
    retention.time: 6h   # Krótki lokalny okres przechowywania
    retention.size: 5GB  # Lub limit wielkości

remote_write:
  - url: http://mimir:9009/api/v1/push

Weryfikacja

# Po zmianach - reload konfiguracji (bez restartu!)
curl -X POST http://prometheus:9090/-/reload

# Poczekaj 5 minut, sprawdź liczbę serii
curl -s http://prometheus:9090/api/v1/status/tsdb | jq '.data.headStats.numSeries'
# Powinna spaść w ciągu 2-3 scrape cykli

# Sprawdź pamięć
kubectl top pod prometheus-server-0 -n monitoring

# Sprawdź czy alerty nadal działają
curl -s http://prometheus:9090/api/v1/rules | jq '.data.groups[].rules | length'

Jeśli liczba serii spadła o >30% i Prometheus nie restartuje się przez 24h, problem rozwiązany. Ustaw alert na prometheus_tsdb_head_series z progiem 80% szacowanego maksimum, aby wyłapać regresje zanim doprowadzą do OOM.

Prometheus OOM to zawsze problem kardynalności, nie pamięci. Dodanie RAM odsuwa problem o tygodnie, nie rozwiązuje go. Jedyna trwała naprawa to identyfikacja i eliminacja metryk z dynamicznymi label. metric_relabel_configs to narzędzie pierwszego wyboru - pozwala odrzucać serie przed zapisem bez zmiany kodu aplikacji.
Jerzy Kopaczewski

Prometheus pada z OOM?

Umów bezpłatną 30-minutową rozmowę. Pomożemy zidentyfikować źródła high cardinality i skonfigurować filtrowanie.