Метрики и проклятие кардинальности

Когда "больше" не значит "лучше"

Представьте, что вы пытаетесь найти иголку в стоге сена.

Кардинальность - это когда кто-то подкидывает вам ещё миллион стогов сена и говорит: "Ищи быстрее!" В метриках кардинальность (cardinality) - это количество уникальных комбинаций меток (labels). И это один из самых коварных врагов систем мониторинга.

Что такое кардинальность?

Кардинальность — это количество уникальных "вещей", которые вы отслеживаете.

Пример без кардинальности (хорошо):

http_requests_total{method="POST", status="200"} 1500
http_requests_total{method="GET", status="200"}  3000
http_requests_total{method="POST", status="500"} 5

Кардинальность = 3 (3 уникальные комбинации меток)

Пример с высокой кардинальностью (проблема):

http_requests_total{method="POST", status="200", user_id="12345", session_id="abcde", request_id="fghij", city="Moscow", browser="Chrome", version="96.0", os="Windows", device="Desktop"} 1
http_requests_total{method="POST", status="200", user_id="67890", session_id="klmno", request_id="pqrst", city="London", browser="Firefox", version="95.0", os="macOS", device="Laptop"} 1
# ... для каждого запроса создается новый временной ряд (time series)!

Кардинальность = N (где N может быть миллионы)

Почему кардинальность это проблема?

Взрывной рост объема данных

Каждая уникальная комбинация меток создает новый временной ряд в базе данных. Системы мониторинга хранят их в памяти.

Посчитаем: - 100 серверов × 100 метрик × 10 значений меток = 100000 временных серий - Каждая серия: 8 байт на точку × 1 точка/сек × 86400 сек/день = ~7 МБ/день на одну серию - Итого: 100000 × 7 МБ = 700 ГБ в день!

Падение производительности при выполнении запросов

# Низкая кардинальность - быстро
sum(rate(http_requests_total[5m])) by (status)

# Высокая кардинальность - медленно или не работает
sum(rate(http_requests_total[5m])) by (user_id)  # 1М уникальных user_id

Стоимость хранения

Облачные сервисы мониторинга (Datadog, New Relic) берут деньги за количество временных серий. Если используется свое хранилище - "место" под метрики тоже стоит денег. Высокая кардинальность = огромные счета.

Типы метрик и их кардинальность

1. "Измеритель" (Gauge)

Текущее значение: температура, количество подключений, использование памяти.

memory_usage_bytes{instance="server1", job="api"} 1500000000

Кардинальность: Обычно низкая (по instance, job).

2. Счетчик (Counter)

Только растет (может и сбрасываться): количество запросов, ошибок, байт.

http_requests_total{method="POST", endpoint="/api/users", status="200"} 12345

Кардинальность: Может быть высокой если добавлять user_id, session_id.

3. Гистограмма (Histogram)

Распределение значений: задержки, размеры ответов.

http_request_duration_seconds_bucket{le="0.1", method="GET"} 1500
http_request_duration_seconds_bucket{le="0.5", method="GET"} 3000
http_request_duration_seconds_sum{method="GET"} 1250.7
http_request_duration_seconds_count{method="GET"} 5000

Создает несколько серий на одну метрику (buckets + sum + count). Кардинальность умножается!

4. Сводка (Summary)

Как гистограмма, но с квантилями на стороне приложения.

http_request_duration_seconds{quantile="0.5", method="GET"} 0.2
http_request_duration_seconds{quantile="0.9", method="GET"} 0.8
http_request_duration_seconds_sum{method="GET"} 1250.7
http_request_duration_seconds_count{method="GET"} 5000

Принципы проектирования метрик для SRE

Принцип 1: метки для группировки, значения для измерения

Плохо:

# Кардинальность взрывается
request_latency_ms{user_id="123", endpoint="/api/users/123/profile"} 150

Хорошо:

# Низкая кардинальность, можно агрегировать
http_request_duration_seconds_bucket{method="GET", handler="/api/users/:id", status="200", le="0.1"} 1500

Принцип 2: ограниченные множества значений для меток

Значения меток должны быть из предсказуемого, ограниченного набора:

Допустимые значения (низкая кардинальность): - status: "200", "404", "500", "502" - method: "GET", "POST", "PUT", "DELETE" - handler: "/api/users", "/api/orders", "/health" - datacenter: "pd-35", "pd-36"

Опасные значения (высокая кардинальность): - user_id: "12345", "67890", ... (миллионы значений) - request_id: UUID (бесконечные значения) - email: "user@example.com" (персональные данные (ПД)!) - ip_address: "192.168.1.1" (высокая кардинальность, могуть быть ПД)

Принцип 3: для разных целей - разные метрики

Для мониторинга SLO (низкая кардинальность):

# SLI: задержка API
http_request_duration_seconds{handler="/api/checkout"}

# Агрегируем по handler, не по конкретному ID пользователя
sum(rate(http_request_duration_seconds_sum[5m])) by (handler)
/
sum(rate(http_request_duration_seconds_count[5m])) by (handler)

Для дебага (высокая кардинальность, отдельная система):

# В логах или трейсах, а не в метриках
{
  "request_id": "abc-123",
  "user_id": "user_456",
  "duration_ms": 150,
  "endpoint": "/api/users/456/profile"
}

Принцип 4: гистограммы вместо отдельных метрик

Плохо (высокая кардинальность):

# Для каждого пользователя своя метрика
user_request_latency{user_id="123"} 150
user_request_latency{user_id="456"} 200
...

Хорошо (низкая кардинальность):

# Одна гистограмма для всех пользователей
http_request_duration_seconds_bucket{le="0.1"} 1000
http_request_duration_seconds_bucket{le="0.5"} 5000
http_request_duration_seconds_bucket{le="1.0"} 5500
# Можно посчитать перцентили без хранения user_id

Практические правила

Правило 10/100/1000

  • < 10 значений метки: идеально
  • 10-100 значений: приемлемо
  • 100-1000 значений: точно это нужно?
  • > 1000 значений: плохо, нужно пересмотреть дизайн

Чек-лист при добавлении новой метки:

  1. Сколько уникальных значений будет? Если > 1000 - стоп.
  2. Нужно ли это для алертинга? Если нет - может быть в логах.
  3. Можно ли агрегировать? Может, вместо user_id использовать user_segment="premium"/"freetier"?
  4. Есть ли ПД? user_id, email, IP - опасны для метрик.

Пример: эволюция метрики

Версия 1 (катастрофа):

api_request_duration{user_id="123", request_id="abc", ip="1.2.3.4", country="RU", city="Moscow", browser="Chrome 96.0", endpoint="/api/users/123"} 150

Кардинальность: бесконечная

Версия 2 (лучше):

api_request_duration{method="GET", handler="/api/users/:id", status="200", user_tier="premium"} 150

Кардинальность: method(4) × handler(50) × status(5) × user_tier(3) = 3000

Версия 3 (идеально для SLO):

api_request_duration_seconds_bucket{handler="/api/users", le="0.1"} 1000

Кардинальность: handler(50) × bucket(10) = 500

Инструменты и техники борьбы с кардинальностью

0. Сначала полдумать

1. Prometheus: relabel_configs для фильтрации

scrape_configs:
  - job_name: 'api'
    metric_relabel_configs:
    # Удаляем метки с высокой кардинальностью
    - action: labeldrop
      regex: '(user_id|request_id|session_id|ip_address)'
    # Оставляем только нужные
    - action: keep
      source_labels: [__name__]
      regex: '(http_requests_total|http_request_duration_seconds).*'

2. Статические метки против динамических

// Плохо: динамическая метка из данных запроса
metrics.HTTPRequestsTotal.WithLabelValues(
    r.Method,
    r.URL.Path,  // Высокая кардинальность!
    strconv.Itoa(r.StatusCode),
).Inc()

// Хорошо: статические или нормализованные метки
path := normalizePath(r.URL.Path)  // "/api/users/:id" вместо "/api/users/123"
metrics.HTTPRequestsTotal.WithLabelValues(
    r.Method,
    path,
    strconv.Itoa(r.StatusCode),
).Inc()

3. Агрегация на стороне приложения

# Вместо отправки каждой метрики
# Агрегируем локально и отправляем статистики
statsd.timing('api.request_duration', 150, tags=[
    'method:GET',
    'handler:users',
    'status:200'
])

4. Использование разных систем для разных целей

  • Prometheus: Метрики для алертинга и SLO (низкая кардинальность)
  • Elasticsearch/ClickHouse: Детальные логи для дебага (высокая кардинальность)
  • Jaeger/Tempo: Трейсы для анализа производительности

Реальные случаи из жизни SRE

Метка "endpoint" с path parameters

Проблема: /api/users/123, /api/users/456 создают разные серии.

Решение:

func normalizePath(path string) string {
    // Заменяем ID на placeholder
    path = regexp.MustCompile(`/users/\d+`).ReplaceAllString(path, "/users/:id")
    path = regexp.MustCompile(`/orders/\d+`).ReplaceAllString(path, "/orders/:id")
    return path
}

Метрики с user_id для бизнес-аналитики

Проблема: Продукт хочет метрики по пользователям. 1M пользователей = 1M временных рядов.

Решение: Отдельная система! - Metabase + PostgreSQL для бизнес-аналитики - ClickHouse для детальных данных - Prometheus только для агрегированных метрик SLO

Метка "version" для canary развертываний

Хороший пример контролируемой кардинальности:

http_requests_total{version="v1.2.3", deployment="canary"} 100
http_requests_total{version="v1.2.2", deployment="stable"} 10000

Кардинальность ограничена количеством версий.

Метрики - это инструмент!!! Это не цель!!!

Вопросы, которые должен задавать SRE:

  1. Для чего эта метрика? Алертинг? Дебаг? Аналитика?
  2. Как мы будем её использовать в запросах? by (user_id) или by (service)?
  3. Что будет, если её cardinality вырастет в 100 раз?
  4. Можно ли получить эти данные из логов вместо метрик?

Золотые метрики SRE (The Four Golden Signals)

  1. Задержка (Latency): http_request_duration_seconds (гистограмма)
  2. Трафик (Traffic): http_requests_total (counter)
  3. Ошибки (Errors): http_requests_total{status=~"5.."} (counter)
  4. Насыщенность (Saturation): cpu_usage, memory_usage (gauge)

У всех низкая кардинальность!

Дисциплина

Кардинальность это не техническая проблема. Это проблема ~~раздолбайства~~ дисциплины и дизайна.

Обычно так: "Давайте добавим все метки, на всякий случай!" Нормальные SRE: "Каждая метка должна быть обоснована. Каждая новая временная серия должна оплачиваться бизнес-ценностью."

Самые надежные системы мониторинга не те, что собирают больше всего данных, а те, что собирают правильные данные в правильном формате и собранные данные приносят пользу, а не "они есть".

Если вы не знаете, как будете использовать метрику в алерте или дашборде то и не создавайте её!. Если метка имеет больше 1000 уникальных значений, то она не для Prometheus. Если вам точно нужны эти детали используйте логи и трейсы.

Метрики с контролируемой кардинальностью это как хорошо организованная библиотека: легко найти нужную книгу. Метрики с высокой кардинальностью как свалка: всё есть, но ничего не найти.

Задача SRE - проектировать библиотеки, а не свалки.