Метрики и проклятие кардинальности¶
Когда "больше" не значит "лучше"¶
Представьте, что вы пытаетесь найти иголку в стоге сена.
Кардинальность - это когда кто-то подкидывает вам ещё миллион стогов сена и говорит: "Ищи быстрее!" В метриках кардинальность (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 значений: плохо, нужно пересмотреть дизайн
Чек-лист при добавлении новой метки:¶
- Сколько уникальных значений будет? Если > 1000 - стоп.
- Нужно ли это для алертинга? Если нет - может быть в логах.
- Можно ли агрегировать? Может, вместо user_id использовать
user_segment="premium"/"freetier"? - Есть ли ПД? 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:¶
- Для чего эта метрика? Алертинг? Дебаг? Аналитика?
- Как мы будем её использовать в запросах?
by (user_id)илиby (service)? - Что будет, если её cardinality вырастет в 100 раз?
- Можно ли получить эти данные из логов вместо метрик?
Золотые метрики SRE (The Four Golden Signals)¶
- Задержка (Latency):
http_request_duration_seconds(гистограмма) - Трафик (Traffic):
http_requests_total(counter) - Ошибки (Errors):
http_requests_total{status=~"5.."}(counter) - Насыщенность (Saturation):
cpu_usage,memory_usage(gauge)
У всех низкая кардинальность!
Дисциплина¶
Кардинальность это не техническая проблема. Это проблема ~~раздолбайства~~ дисциплины и дизайна.
Обычно так: "Давайте добавим все метки, на всякий случай!" Нормальные SRE: "Каждая метка должна быть обоснована. Каждая новая временная серия должна оплачиваться бизнес-ценностью."
Самые надежные системы мониторинга не те, что собирают больше всего данных, а те, что собирают правильные данные в правильном формате и собранные данные приносят пользу, а не "они есть".
Если вы не знаете, как будете использовать метрику в алерте или дашборде то и не создавайте её!. Если метка имеет больше 1000 уникальных значений, то она не для Prometheus. Если вам точно нужны эти детали используйте логи и трейсы.
Метрики с контролируемой кардинальностью это как хорошо организованная библиотека: легко найти нужную книгу. Метрики с высокой кардинальностью как свалка: всё есть, но ничего не найти.
Задача SRE - проектировать библиотеки, а не свалки.