Späť na blog

Prometheus Kardinalita Explózia: Detekcia, Prevencia a Obnova

Cardinality explozie neuvidis v testoch, ale na fakture. V piatok poobede som si myslel, ze toto bude kludny deploy. Grafy vsak nezacali kricat, ale zmizli. Prometheus mal 64GB RAM a stale OOM. Pozrel som /api/v1/status/tsdb a videl som klasiku: niekto pridal user_id label ku http_requests_total.

Na papieri to vyzera rozumne: “chcem latenciu per user”. V Prometheovi kazda kombinacia labelov znamena novu time series. 10 milionov userov = 10 milionov serii. Monitoring sa zmeni na incident.

Testované na: Prometheus 2.47, 50-nodový Kubernetes cluster, 2M aktívnych time series

Pochopenie Kardinality

Čo Vytvára Time Series

Kardinalita metriky = súčin všetkých hodnôt labelov

Príklad:
  http_requests_total{
    method="GET",       # 5 hodnôt (GET, POST, PUT, DELETE, PATCH)
    status="200",       # 50 hodnôt (200, 201, 400, 401, 404, 500...)
    endpoint="/api/v1"  # 100 hodnôt (endpointy)
  }

Kardinalita: 5 × 50 × 100 = 25,000 time series

Pridaj user_id label s 1M používateľmi:
Kardinalita: 5 × 50 × 100 × 1,000,000 = 25,000,000,000 time series
                                        └─ Prometheus umiera

Dopad na Pamäť

Prometheus použitie pamäte:

Per aktívna time series:
  - ~3KB RAM pre nedávne vzorky (posledné 2 hodiny)
  - ~1.5KB pre TSDB head chunks

Real-world príklad:
  Pred: 500,000 time series × 3KB = 1.5GB
  Po pridaní user_id: 50,000,000 × 3KB = 150GB

Jeden zlý label spôsobuje 100x nárast pamäte

Detekcia

TSDB Status Endpoint

# Skontroluj aktuálnu kardinalitu
curl -s localhost:9090/api/v1/status/tsdb | jq .

# Output:
{
  "seriesCountByMetricName": [
    {"name": "http_requests_total", "value": 25000000},  # ČERVENÁ VLAJKA
    {"name": "process_cpu_seconds_total", "value": 500},
    ...
  ],
  "labelValueCountByLabelName": [
    {"name": "user_id", "value": 10000000},  # ČERVENÁ VLAJKA
    {"name": "instance", "value": 50},
    {"name": "method", "value": 5},
    ...
  ],
  "seriesCountByLabelValuePair": [
    {"name": "job=api-server", "value": 25000000},
    ...
  ]
}

PromQL Queries

# Celkové aktívne time series
prometheus_tsdb_head_series

# Time series vytvorené per sekunda (detekcia spikov)
rate(prometheus_tsdb_head_series_created_total[5m])

# Pamäť použitá TSDB head
prometheus_tsdb_head_chunks_storage_size_bytes

# Kardinalita podľa názvu metriky
topk(10, count by (__name__) ({__name__=~".+"}))

# Kardinalita podľa labelu
topk(10, count by (user_id) ({user_id=~".+"}))

Proaktívny Monitoring

# prometheus-alerts.yaml
groups:
- name: cardinality
  rules:
  - alert: VysokaKardinalitaMetriky
    expr: |
      topk(1, count by (__name__) ({__name__=~".+"})) > 100000
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "Metrika {{ $labels.__name__ }} má >100k series"

  - alert: TimeSeriesExplozia
    expr: |
      rate(prometheus_tsdb_head_series_created_total[5m]) > 1000
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "Vytvára sa {{ $value }}/sec nových time series"

  - alert: VysokaKardinalitaLabelu
    expr: |
      prometheus_tsdb_head_series > 1000000
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Celkové time series prekračuje 1M"

Prevencia

Relabel Config na Drop High-Cardinality Labelov

# prometheus.yml
scrape_configs:
  - job_name: 'api-servers'
    static_configs:
      - targets: ['api:8080']
    metric_relabel_configs:
      # Dropni metriky s user_id labelom úplne
      - source_labels: [user_id]
        regex: .+
        action: drop

      # Alebo dropni len label, nechaj metriku
      - regex: user_id
        action: labeldrop

      # Dropni metriky matchujúce pattern
      - source_labels: [__name__]
        regex: "expensive_metric_.*"
        action: drop

      # Hashuj high-cardinality labely pre zníženie kardinality
      - source_labels: [request_id]
        regex: (.+)
        target_label: request_id_bucket
        replacement: "bucket_${1:0:2}"  # Prvé 2 znaky = 256 bucketov
        action: replace
      - regex: request_id
        action: labeldrop

Recording Rules pre Agregáciu

# Namiesto ukladania high-cardinality metrík,
# agreguj ich pri scrape time

groups:
- name: aggregations
  rules:
  # Agreguj per-user metriky na per-endpoint
  - record: http_requests:by_endpoint:rate5m
    expr: |
      sum by (endpoint, method, status) (
        rate(http_requests_total[5m])
      )

  # Nechaj len top N hodnôt labelov
  - record: http_requests:top_endpoints:rate5m
    expr: |
      topk(100,
        sum by (endpoint) (rate(http_requests_total[5m]))
      )

Application-Level Prevencia

// Zlé: High-cardinality label
var httpRequests = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
    },
    []string{"method", "status", "endpoint", "user_id"},  // ZLÉ!
)

// Dobré: Odstráň neohraničené labely
var httpRequests = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
    },
    []string{"method", "status", "endpoint"},  // Ohraničená kardinalita
)

// Ak potrebuješ per-user metriky, použi histogramy alebo logy
var requestDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Buckets: prometheus.DefBuckets,
    },
    []string{"method", "endpoint"},  // Žiadne user_id!
)

Ohraničenie Hodnôt Labelov

// Ohranič kardinalitu endpointov
func normalizeEndpoint(path string) string {
    // /users/12345 → /users/:id
    // /orders/abc-def → /orders/:id

    patterns := []struct {
        regex       *regexp.Regexp
        replacement string
    }{
        {regexp.MustCompile(`/users/[^/]+`), "/users/:id"},
        {regexp.MustCompile(`/orders/[^/]+`), "/orders/:id"},
        {regexp.MustCompile(`/\d+`), "/:id"},
    }

    result := path
    for _, p := range patterns {
        result = p.regex.ReplaceAllString(result, p.replacement)
    }

    // Catch-all pre neznáme patterny
    if strings.Count(result, "/") > 5 {
        return "/other"
    }

    return result
}

Obnova

Núdzové Postupy

# 1. Identifikuj vinníka
curl -s localhost:9090/api/v1/status/tsdb | jq '.data.seriesCountByMetricName[:10]'

# 2. Pridaj drop pravidlo okamžite
# Uprav prometheus.yml, pridaj do metric_relabel_configs:
# - source_labels: [__name__]
#   regex: "bad_metric_name"
#   action: drop

# 3. Reload Prometheus config (bez reštartu)
curl -X POST localhost:9090/-/reload

# 4. Vynúť TSDB head kompakciu pre uvoľnenie pamäte
# (Prometheus 2.39+)
curl -X POST localhost:9090/api/v1/admin/tsdb/head_compaction

# 5. Ak stále OOMuje, zmaž zlé metriky
# VAROVANIE: Toto je deštruktívne!
curl -X POST -g 'localhost:9090/api/v1/admin/tsdb/delete_series?match[]=bad_metric_name'

# 6. Vyčisti tombstones
curl -X POST localhost:9090/api/v1/admin/tsdb/clean_tombstones

Prevencia Budúcich Incidentov

# prometheus.yml
global:
  scrape_interval: 15s

  # Limituj vzorky per scrape
  sample_limit: 50000  # Per target

  # Limituj labely per vzorku
  label_limit: 30
  label_name_length_limit: 200
  label_value_length_limit: 2000

scrape_configs:
  - job_name: 'api'
    sample_limit: 10000  # Override per job
    metric_relabel_configs:
      # Dropni všetky metriky s podozrivými labelmi
      - source_labels: [user_id, customer_id, request_id, session_id]
        regex: .+
        action: drop

Monitoring Dashboard

Grafana Panely

# Panel 1: Celkové Time Series
prometheus_tsdb_head_series

# Panel 2: Rýchlosť Rastu Time Series
rate(prometheus_tsdb_head_series_created_total[5m])

# Panel 3: Použitie Pamäte
prometheus_tsdb_head_chunks_storage_size_bytes / 1024 / 1024 / 1024

# Panel 4: Top 10 Metrík podľa Kardinality
topk(10, count by (__name__) ({__name__=~".+"}))

# Panel 5: Churn Rate (vytvorené - zmazané series)
rate(prometheus_tsdb_head_series_created_total[5m])
- rate(prometheus_tsdb_head_series_removed_total[5m])

# Panel 6: Scrape Duration (môže indikovať problémy s kardinalitou)
prometheus_target_scrape_pool_sync_total

Kardinalita Budget

# Nastav kardinalita budgety per tím/službu
# Implementuj cez recording rules + alerty

groups:
- name: cardinality_budgets
  rules:
  # Sleduj kardinalitu per job
  - record: job:prometheus_series:count
    expr: count by (job) ({__name__=~".+"})

  # Alert keď job prekročí budget
  - alert: KardinalitaBudgetPrekroceny
    expr: |
      job:prometheus_series:count{job="api-server"} > 50000
    labels:
      severity: warning
    annotations:
      summary: "Job {{ $labels.job }} prekračuje 50k series budget"

Best Practices

Pravidlá pre Labely

## Bezpečné Labely (ohraničená kardinalita)
✅ method: GET, POST, PUT, DELETE, PATCH (5 hodnôt)
✅ status_code: 200, 201, 400, 401, 403, 404, 500, 502, 503 (~20 hodnôt)
✅ service_name: ohraničené počtom služieb (~100)
✅ environment: dev, staging, prod (3 hodnoty)
✅ region: us-east-1, us-west-2, eu-west-1 (~10 hodnôt)

## Nebezpečné Labely (neohraničená kardinalita)
❌ user_id: milióny používateľov
❌ request_id: nekonečné
❌ email: milióny
❌ ip_address: potenciálne milióny
❌ trace_id: nekonečné
❌ timestamp: nekonečné
❌ url_path (raw): neohraničené (potrebuje normalizáciu)

## Pravidlo
Kardinalita labelu by mala byť < 1000 hodnôt
Celková kardinalita metriky by mala byť < 10,000 series

Architektúra pre High-Cardinality Dáta

Potrebuješ per-user metriky? Nepoužívaj Prometheus labely.

Alternatívne prístupy:

1. Logy + Log agregácia
   User aktivita → Štruktúrované logy → Loki/Elasticsearch
   Query: sum(rate({job="api"} |= "user_id=123")) by (endpoint)

2. Event streaming
   User eventy → Kafka → ClickHouse/TimescaleDB
   Query: SELECT count(*) FROM events WHERE user_id = 123

3. Exemplars (Prometheus 2.26+)
   Pripoj trace_id k histogram bucketom
   Nízka kardinalita metrík + vysoká kardinalita exemplárov

4. Remote write do špecializovaného TSDB
   High-cardinality → Victoria Metrics / M3DB / Thanos
   Lepšie spracovanie kardinality

Checklist

## Prometheus Kardinalita Management

### Detekcia
- [ ] Monitoruj prometheus_tsdb_head_series
- [ ] Alert na rýchlosť vytvárania series > 1000/sec
- [ ] Kontroluj /api/v1/status/tsdb pravidelne
- [ ] Dashboard ukazujúci top metriky podľa kardinality

### Prevencia
- [ ] Relabel configs na drop nebezpečných labelov
- [ ] sample_limit per scrape target
- [ ] Application-level ohraničenie labelov
- [ ] Code review pre nové metriky

### Plán Obnovy
- [ ] Zdokumentuj núdzové drop postupy
- [ ] Vedz ako použiť delete_series
- [ ] Otestuj proces config reload
- [ ] Runbook pre kardinalita incidenty

### Best Practices
- [ ] Kardinalita labelu < 1000 hodnôt
- [ ] Žiadne neohraničené labely (user_id, request_id)
- [ ] Použi logy pre high-cardinality dáta
- [ ] Recording rules pre agregáciu

Záver

Kardinalita explózia zabíja Prometheus rýchlo:

  1. Jeden zlý label môže vytvoriť milióny series
  2. Monitoruj prometheus_tsdb_head_series konštantne
  3. Použi relabel_configs na drop nebezpečných labelov pred ingestionom
  4. Ohranič všetky hodnoty labelov na úrovni aplikácie

Skontroluj svoj TSDB status teraz. Explózia možno už prebieha.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Prometheus Kardinalita Explózia: Detekcia, Prevencia a Obnova". https://www.michal-drozd.com/sk/blog/prometheus-cardinalita-explozia/ (Publikované 23. júla 2025).