# Cardinality explosion: как убить Prometheus и как чинить _Observability и мониторинг · LinuxLab Knowledge Base_ **TL;DR:** Cardinality = uniq(metric × labels). Каждый series ≈ 3 KB RAM, 10M series = 30 GB RAM, Prom OOM-цикл. Source: user_id-labels, пути с ID, dynamic version. Diag - topk по __name__. Cure - drop labels через relabel или нормализация в коде. ## Что такое cardinality В Prometheus каждая уникальная **комбинация labels** = отдельный **time series**: ``` http_requests_total{method="GET", status="200"} 12345 http_requests_total{method="GET", status="500"} 67 http_requests_total{method="POST", status="200"} 543 http_requests_total{method="POST", status="500"} 12 ``` Это **4 series**. Cardinality этой метрики = `2 (methods) × 2 (statuses) = 4`. Добавили label `region` с 5 значениями: `4 × 5 = 20 series`. Добавили `pod` с 100 значениями: `20 × 100 = 2000 series`. Добавили `user_id` с **1M значениями**: `2000 × 1M = 2 billion`. Это **multiplicative**. Один высоко-кардинальный label убивает. ## Стоимость Prometheus держит каждый active series в RAM: - **head chunk** (последние 2 часа), ~2 KB raw + index - **postings index**, label → series mapping - **WAL**, write-ahead log Эмпирическое правило: **~3 KB RAM per active series** (Prometheus defaults). | Series count | RAM | Хост | |--------------|-----|------| | 100K | 300 MB | small | | 1M | 3 GB | medium | | 10M | 30 GB | large + tuning | | 50M | 150 GB | OOM, нужен Thanos/Mimir/VictoriaMetrics | | 100M+ | impossible в одном Prom | При OOM Prometheus рестартует, replay WAL (10-30 min), снова OOM. Цикл, метрик нет. ## Топ-источники cardinality 1. **user_id, customer_id, request_id, trace_id, session_id** в metric labels, *никогда*. Это в логи и traces. 2. **Динамические URL paths**: `/api/users/12345/orders/678` каждый ID = новый label value. - **Решение**: route normalization. Express.js: используй `req.route.path` (`/api/users/:id/orders/:oid`), не `req.url`. 3. **Container/pod IDs**: `pod=app-7f8b9c-abc123` уникален на каждый rollout. Через retention рассасывается, но накапливается. - **Решение**: drop `pod` label если не нужен в queries, оставь `deployment` или `app`. 4. **Версия в каждой метрике**: `version=1.4.2-build-12345` → каждый release добавляет cardinality. - **Решение**: версия в одной info-метрике (`build_info{version="..."}` = 1), join при необходимости. 5. **Histograms с многими buckets**: 50 buckets × 5 endpoints × 10 statuses = 2500 series только на один histogram. - **Решение**: меньше buckets, [[metric-types|native histograms]]. 6. **JVM/Go runtime метрики per-class**, некоторые exporters детализируют до class name. - **Решение**: `metric_relabel` drop по regex. 7. **kube-state-metrics labels из аннотаций**, `app.kubernetes.io/version` каждый раз новый. - **Решение**: kube-state-metrics flag `--metric-labels-allowlist`. ## Диагностика, кто виноват ### Топ метрик по cardinality ```promql topk(20, count by (__name__)({__name__=~".+"})) ``` Покажет 20 имён метрик с самым большим числом series. Кандидаты на cleanup. ### Топ labels внутри метрики ```promql count(my_high_card_metric) by (suspect_label) ``` Сколько series с каждым значением подозреваемого label. Если значений тысячи, он source проблемы. ### Через `tsdb` API ```bash curl -s 'http://prom:9090/api/v1/status/tsdb' | jq '.data | { headStats: .headStats, seriesCountByMetricName: (.seriesCountByMetricName[:10]), labelValueCountByLabelName: (.labelValueCountByLabelName[:10]) }' ``` Endpoint `/api/v1/status/tsdb` (Prom 2.14+), встроенная статистика. Top metrics, top labels, top label values. ### `promtool tsdb analyze` ```bash promtool tsdb analyze /var/lib/prometheus ``` Анализ блоков на диске. Top label pairs, top metrics, churn rate. Запускать на остановленном Prometheus или на snapshot. ## Churn rate, невидимая cardinality «Active series» = живые сейчас. Но **churn**, как быстро series appear/disappear, тоже жрёт ресурсы: - Каждый новый series → write в index - Старые остаются в WAL, в blocks, retention Pod restart → new `pod=` label value → новый series. Высокий churn pod-id-based labels, медленные queries (range много series). Prometheus 2.50+ метрика: ``` prometheus_tsdb_head_series_created_total ``` Rate >10K/час = высокий churn, копай. ## Чинить, стратегии ### 1. Drop label в коде Самое чистое. Пример Go: ```go // ПЛОХО requestCounter := promauto.NewCounterVec( prometheus.CounterOpts{Name: "http_requests_total"}, []string{"method", "endpoint", "user_id"}, // ← user_id убийца ) // ХОРОШО requestCounter := promauto.NewCounterVec( prometheus.CounterOpts{Name: "http_requests_total"}, []string{"method", "endpoint"}, ) ``` ### 2. Drop через metric_relabel Когда не контролируешь exporter: ```yaml scrape_configs: - job_name: app metric_relabel_configs: - regex: 'user_id' action: labeldrop - source_labels: [__name__] regex: 'go_gc_pauses_seconds_bucket' action: drop ``` Применяется **на стороне Prometheus** до записи в TSDB. ### 3. Normalize URL paths Сервис Express.js: ```js app.use((req, res, next) => { res.on('finish', () => { const route = req.route?.path || 'unmatched'; // не req.url requestCounter.labels(req.method, route, res.statusCode).inc(); }); next(); }); ``` ### 4. Aggregation через recording rules Если нужны и high-card метрики (для ad-hoc), и low-card (для alerting), храни обе: ```yaml # Raw остаётся короткое retention # Recording rules собирают агрегаты с длинным retention groups: - name: aggregations rules: - record: app:http_requests:rate5m_low_card expr: sum without(pod, instance, user_id)(rate(http_requests_total[5m])) ``` ### 5. Move to VictoriaMetrics VictoriaMetrics держит series **в 3-7× меньше RAM**, лучше масштабируется. Drop-in replacement через `remote_write`. Не решает cardinality принципиально, но даёт запас. Cluster VM: 100M+ series без Thanos-комплексности. ## Active vs total series - **Active** = scraped в последние 5 minutes (head chunk) - **Total** в TSDB = active + retention 15d Active определяет RAM (head). Total, disk. ```promql prometheus_tsdb_head_series # active prometheus_tsdb_blocks_loaded * # rough total ``` ## Limits Prometheus 2.x не имеет hard cardinality limit (но есть soft). Включай **server-side limits**: ```yaml scrape_configs: - job_name: app sample_limit: 50000 # max samples per scrape label_limit: 30 # max labels per series label_value_length_limit: 200 target_label: max_targets # limit per relabel ``` Если scrape exceeds, target помечается как error, метрики не записываются. Защищает от runaway exporter. Cortex/Mimir/VM имеют per-tenant limits на active series. ## Cardinality в OpenTelemetry/Loki/Tempo Та же проблема: - **OTel metrics**, те же лимиты, attributes = labels - **Loki**, labels = streams. >10K streams в tenant = деградация. `structured_metadata` для high-card identifiers ([loki-grafana-logging](/kb/loki-grafana-logging.md)). - **Tempo**, search index по resource attributes. Не клади request_id в resource. Принцип универсальный: **labels, низкая кардинальность, payload свободно**. ## Real-world cases - **Cloudflare incident 2022**: один сервис добавил `customer_id` label в metric → 5M series за час, Prometheus OOM, мониторинг down 6 часов. - **GitLab incident 2018**: kube-state-metrics всегда читал annotations, каждый CI job (миллионы) → cardinality blowup. - **Honeycomb internal**: история о labelmap копированиях k8s аннотаций. Pattern: **«а давай ещё один label добавим, удобнее»** → через 3 месяца не запускается. ## Когда что-то пошло не так - **Prom OOM-цикл**, `topk(20, count by (__name__)({}))` после рестарта (быстрее чем replay WAL). Найди и drop через `metric_relabel_configs` для cooldown. - **`/targets` показывает "sample limit exceeded"**, `sample_limit` превышен. Подними или (лучше) почини exporter. - **Slow queries (>30s)**, много series в селекторе. Добавь больше selectors, узжай range. `topk(... count by (label)(metric))` найдёт жирные labels. - **Запросы возвращают subset**, Prom при OOM удаляет старые series из head, query даёт пустоту. Чек `prometheus_tsdb_head_truncations_total`. - **VictoriaMetrics 503** на push, превышены `-storage.maxLabelsPerTimeseries` или `-storage.maxLabelValueLen`. Проверь VM logs. - **Cardinality OK сразу после рестарта**, но через час OOM. Это **churn**, series растут со временем. Проверь rate `prometheus_tsdb_head_series_created_total`. ## Cheat sheet ```promql # Топ-20 метрик по числу series (главная диагностика) topk(20, count by (__name__)({__name__=~".+"})) # Сколько values у каждого label count(http_requests_total) by (label_name) # Churn, series созданы за час increase(prometheus_tsdb_head_series_created_total[1h]) # Active series count prometheus_tsdb_head_series # Memory от series (rough) prometheus_tsdb_head_series * 3000 # bytes ``` ## Команды ```bash promtool query instant 'topk(20, count by (__name__)({__name__=~".+"}))' ``` ТОП-20 метрик по cardinality - первая команда при OOM ```bash curl -s http://prom:9090/api/v1/status/tsdb | jq '.data | {headSeries: .headStats.numSeries, topMetrics: .seriesCountByMetricName[:5], topLabels: .labelValueCountByLabelName[:5]}' ``` Встроенная Prom-статистика по TSDB - active series, top metrics, top label values ```bash promtool tsdb analyze /var/lib/prometheus ``` Анализ blocks на диске - churn, top labels, label-value distribution ```bash promtool query instant 'count(http_requests_total) by (user_id)' | head -20 ``` Если user_id в metric labels - количество series по каждому user_id ```bash curl -s 'http://prom:9090/api/v1/label/__name__/values' | jq '. | length' ``` Сколько уникальных metric names всего - быстрая sanity-проверка ```bash promtool query instant 'rate(prometheus_tsdb_head_series_created_total[1h]) * 3600' ``` Series creation rate - churn (>10K/hour - investigate) ```bash curl -X POST 'http://prom:9090/api/v1/admin/tsdb/delete_series?match[]={__name__="bad_metric"}' ``` Удалить series конкретной метрики из TSDB (требует --web.enable-admin-api) ## См. также - [Prometheus: scrape, TSDB, PromQL и production-pitfalls](/kb/prometheus-basics.md) - [Service discovery в Prometheus: k8s, Consul, file_sd, relabel](/kb/service-discovery-prometheus.md) - [Типы метрик: counter, gauge, histogram, summary](/kb/metric-types.md) - [SLI / SLO / error budget: SRE-метрики без шума](/kb/sli-slo-error-budget.md) - [Loki: label-based логи, LogQL, Promtail/Vector pipeline](/kb/loki-grafana-logging.md) - [OpenTelemetry: signals, OTLP, Collector pipeline](/kb/opentelemetry.md)