linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • Симулятор
  • База знаний
  • Собеседование
Index
Categories
All entries
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
home/linux/kb/Observability и мониторинг/cardinality-explosion

kb/observability ── Observability и мониторинг ── advanced

Cardinality explosion: как убить Prometheus и как чинить

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 или нормализация в коде.

view as markdownaka: cardinality, cardinality-explosion, high-cardinality-metrics, prometheus-oom

Что такое 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 countRAMХост
100K300 MBsmall
1M3 GBmedium
10M30 GBlarge + tuning
50M150 GBOOM, нужен 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 * <avg block series>  # 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).
  • 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-basicsPrometheus: scrape, TSDB, PromQL и production-pitfallsPrometheus - сервер мониторинга: сам опрашивает приложения по HTTP, собирает числовые метрики, хранит во встроенной БД ~15 дней. По ним строят графики в Grafana и алерты через Alertmanager. Стандарт de-facto в Kubernetes.
  • service-discovery-prometheusService discovery в Prometheus: k8s, Consul, file_sd, relabelProm discoverит targets через k8s API, Consul, file_sd (static). relabel_configs - до scrape (filter+rewrite labels). metric_relabel - после scrape (drop bad metrics). Без relabel - cardinality из k8s взрывается.
  • metric-typesТипы метрик: counter, gauge, histogram, summary4 типа метрик: counter (только вверх), gauge (любое значение), histogram (buckets для p99), summary (quantile в клиенте). Native histogram (Prom 2.40+) - sparse buckets, аккуратнее по памяти. Exemplars связывают метрику с trace_id.
  • sli-slo-error-budgetSLI / SLO / error budget: SRE-метрики без шумаSLI - метрика для пользователя (availability, p99 latency). SLO - цель за период (99.9% за 30d). Error budget = 1-SLO, расходуется на инциденты+релизы. Multi-window burn rate alerting заменяет threshold-алерты, меньше шума.
  • loki-grafana-loggingLoki: label-based логи, LogQL, Promtail/Vector pipelineLoki - log aggregation с label-based индексом (не full-text как Elastic). Дёшево на S3-storage. Promtail/Vector как агенты. LogQL похож на PromQL: фильтр + parse + aggregation. Cardinality - враг.
  • opentelemetryOpenTelemetry: signals, OTLP, Collector pipelineOpenTelemetry - CNCF-стандарт для metrics+traces+logs в одном SDK. OTLP протокол (gRPC или HTTP). Collector принимает, фильтрует, роутит в Prom/Tempo/Loki/Jaeger. Auto-instrumentation без code change.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки