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:
- Jeden zlý label môže vytvoriť milióny series
- Monitoruj
prometheus_tsdb_head_serieskonštantne - Použi relabel_configs na drop nebezpečných labelov pred ingestionom
- Ohranič všetky hodnoty labelov na úrovni aplikácie
Skontroluj svoj TSDB status teraz. Explózia možno už prebieha.
Súvisiace články
- OpenTelemetry Tail Sampling - Observabilita v škále
- Structured Logging Performance - Alternatíva log agregácie
Súvisiace články
Cardinality Contracts: sprav z Prometheus labelov API s budgetom
Definuj budgety na cardinality, over ich v CI a pridaj runtime firewall, aby si zastavil explozie labelov pred produkciou.
Prometheus native histogramy v produkcii: rollout plán, budgety a failure módy
Prometheus native histogramy vedia odpáliť pamäť, WAL aj remote_write. Návod na postupné nasadenie, budgety a konkrétne queries na verifikáciu.
Tail-based sampling v OpenTelemetry: Sizing, pamäťové pády a cost model
Praktický sizing guide pre tail sampling v OpenTelemetry Collector. Od decision_wait cez memory limity až po cost-benefit analýzu.
Dash Contracts v Go: CI kompilator pre Grafana dashboardy a Prometheus alerty
Vytiahni PromQL z dashboardov a rules suborov, over selektory proti /metrics a zastav CI este pred deployom.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.