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.
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 pada z OOM?
Umów bezpłatną 30-minutową rozmowę. Pomożemy zidentyfikować źródła high cardinality i skonfigurować filtrowanie.