ПроКодинг - Откроем для вас мир IT!

Вы когда-нибудь открывали дашборд Grafana только для того, чтобы увидеть график с тысячами отдельных линий? Или получали счет за облачное хранилище метрик, который неожиданно вырос на 300% после релиза нового микросервиса? Это классические симптомы высокой кардинальности временных рядов. Если вы работаете с системами мониторинга вроде Prometheus или VictoriaMetrics, то знаете: каждая новая уникальная комбинация лейблов создает новый временной ряд. И если этих комбинаций слишком много, ваша инфраструктура начинает задыхаться.

Проблема не в том, что метрики плохие. Проблема в том, как мы их используем. Многие инженеры ошибочно полагают, что чем больше деталей можно добавить в лейблы, тем лучше будет отладка. Но на практике это приводит к взрывному росту потребления памяти и CPU. Давайте разберемся, почему это происходит, как рассчитать реальную нагрузку и какие приемы помогут сохранить голову трезвой, а бюджет - целым.

Что такое кардинальность и почему она опасна

Начнем с базы, но без академической скуки. В контексте систем мониторинга (особенно тех, что используют формат экспорта Prometheus), кардинальность - это количество уникальных временных рядов. Каждый временной ряд определяется уникальной комбинацией имени метрики и всех ее лейблов. Например, метрика http_requests_total с лейблами {method="GET", status="200"} - это один ряд. Если вы добавите лейбл user_id, и у вас есть 100 000 активных пользователей, вы получите 100 000 новых рядов. Умножьте это на другие лейблы, и вы поймете масштаб проблемы.

Почему это критично? Системы мониторинга хранят данные во временных рядах. Каждый ряд занимает место в оперативной памяти (для быстрого доступа) и на диске (для долгосрочного хранения). Высокая кардинальность напрямую бьет по трем вещам:

  • Потребление RAM: Индексы временных рядов живут в памяти. Больше рядов - больше индексов - выше риск OOM (Out Of Memory).
  • Производительность запросов: Чем больше рядов нужно сканировать для построения графика, тем дольше ответит база данных.
  • Стоимость хранения: Облачные провайдеры часто берут деньги за объем ingested data или количество уникальных серий.

Представьте, что вы пытаетесь найти книгу в библиотеке. Если книг мало, вы найдете ее быстро. Если библиотечный фонд удвоился за ночь из-за того, что кто-то завел отдельную полку для каждого читателя, поиск станет кошмаром. То же самое происходит с вашей базой временных рядов.

Где чаще всего случаются ошибки кардинальности

Опыт показывает, что 90% проблем с высокой кардинальностью исходят из трех источников. Зная их, вы можете заранее защитить свою систему.

1. ID пользователей и сессий. Самая частая ошибка - добавление user_id, session_id или trace_id в лейблы метрик. Эти значения имеют огромную вариативность. Если у вас миллион уникальных пользователей, каждый запрос может создавать новый уникальный набор лейблов. Метрики предназначены для агрегации, а не для трассировки конкретных событий. Для последних используйте логи или трейсинг (например, Jaeger или Tempo).

2. Динамические пути URL. Лейбл path или url с полным путем запроса (например, /api/v1/users/12345/profile) убивает производительность. Вместо этого нормализуйте путь до шаблона: /api/v1/users/:id/profile. Это превратит тысячи уникальных путей в один шаблон.

3. Изменчивые версии и сборки. Добавление точной хеш-суммы коммита Git (git_commit_hash) в каждую метрику означает, что при каждом деплое все старые ряды становятся «мертвыми», а новые создаются заново. Хотя это полезно для корреляции, это резко увеличивает количество активных серий. Лучше использовать версию релиза или дату сборки.

Как измерить текущую ситуацию

Прежде чем чинить, нужно понять масштаб бедствия. Большинство современных систем позволяют легко оценить кардинальность.

Если вы используете Prometheus систему мониторинга и алертинга, выполните следующий запрос в UI:

count({__name__=~".+"})

Этот запрос покажет общее количество активных временных рядов. Теперь посмотрим на распределение по метрикам, чтобы найти главных виновников:

topk(10, count by (__name__) ({__name__=~".+"}))

Эта команда вернет топ-10 метрик с наибольшим количеством серий. Скорее всего, вы увидите там метрики HTTP-сервера или бизнес-логики с подозрительно высокими числами.

В VictoriaMetrics базах данных для метрик аналогичный анализ делается через API или встроенный интерфейс vmui. Там также можно посмотреть динамику роста числа серий за последние сутки. Резкий скачок обычно указывает на недавний деплой кода с новыми лейблами.

Сравнение влияния типов лейблов на кардинальность
Тип лейбла Пример значения Уровень риска Рекомендация
Статический region="eu-west-1" Низкий Безопасно использовать
Конечный домен status="200|404|500" Низкий/Средний Хорошо для агрегации
Шаблонизированный path="/users/:id" Средний Требует контроля количества шаблонов
Динамический ID user_id="123456789" Критический Избегать в метриках, использовать логи
Временной штамп timestamp="1695900000" Катастрофический Никогда не использовать в лейблах
Концептуальная иллюстрация нормализации данных: от хаоса к порядку

Стратегии борьбы с высокой кардинальностью

Итак, вы нашли проблемные метрики. Что делать дальше? Вот проверенные тактики, которые используют команды в крупных компаниях.

1. Нормализация путей и параметров

Это первое, что нужно сделать. Если ваш фреймворк (Spring Boot, Gin, Express) позволяет настроить middleware для метрик, убедитесь, что он заменяет конкретные ID в URL на плейсхолдеры. Вместо /orders/12345 должно быть /orders/{id}. Большинство библиотек клиентских метрик (client libraries) делают это автоматически, если правильно настроены маршрутизаторы. Проверьте свои настройки - иногда разработчики случайно отключают эту функцию ради «деталей».

2. Использование relabel_configs (для Prometheus)

Если вы не можете изменить код приложения, используйте возможности конфигурации Prometheus. В разделе scrape_config есть механизм relabel_configs, который позволяет изменять, добавлять или удалять лейблы перед тем, как они попадут в базу. Вы можете удалить ненужные лейблы или переписать их значения.

Например, чтобы удалить лейбл instance_id, который меняется при каждом рестарте пода Kubernetes, добавьте:

relabel_configs:
  - action: labeldrop
    regex: instance_id

Также можно объединять несколько лейблов в один или фильтровать метрики по названию, чтобы вообще не собирать шумные сервисы.

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

Иногда приложение отправляет слишком сырые данные. Вместо того чтобы экспортировать метрику для каждого пользователя отдельно, попробуйте агрегировать данные внутри приложения. Например, вместо отправки события «пользователь X совершил действие Y» каждые 5 секунд, считайте счетчик действий в памяти процесса и экспортируйте итоговое значение раз в 15 секунд. Это снижает нагрузку на сеть и базу.

4. Разделение горячих и холодных данных

Не все метрики нужны в реальном времени с одинаковой детализацией. Используйте функции downsampling (прореживания). Современные системы, такие как VictoriaMetrics или Thanos, позволяют хранить данные высокой точности (raw resolution) недолго (например, 7 дней), а затем агрегировать их до более низкого разрешения (hourly/daily) для долгосрочного хранения. Это не снижает кардинальность самих рядов, но значительно уменьшает объем хранимых точек данных и ускоряет исторические запросы.

Инструменты и практики для предотвращения будущего хаоса

Лучшая защита - профилактика. Внедрите эти практики в процесс разработки.

Code Review для метрик. При добавлении новой метрики или лейбла проверяйте код так же строго, как логику приложения. Задавайте вопросы: «Какой максимальный cardinality может дать этот лейбл?», «Есть ли у него конечное множество значений?». Если ответ неочевиден - требуйте обоснования.

Лимиты в конфигурации. В Prometheus и VictoriaMetrics можно задать лимиты на максимальное количество активных серий (max_series). Если приложение превысит лимит, оно либо перестанет принимать новые метрики, либо упадет. Это больно, но лучшеcontrolled failure, чем тихое падение всей системы мониторинга из-за нехватки памяти.

Мониторинг самого мониторинга. Создайте алерт на рост общего количества серий. Например, алерт, который срабатывает, если число активных временных рядов увеличилось на 20% за последний час. Это позволит вам заметить проблему сразу после деплоя, а не через неделю, когда пользователи начнут жаловаться на медленные дашборды.

Оптимизированный дашборд мониторинга с контролируемой нагрузкой

Когда высокая кардинальность оправдана?

Не всегда нужно бороться с кардинальностью любой ценой. Иногда она необходима. Например, если вы мониторите специфическое оборудование с уникальными серийными номерами, где количество устройств ограничено (до нескольких тысяч). Или если вы строите сложную бизнес-аналитику прямо поверх метрик, где нужна детализация по каждому продукту в магазине (при условии, что продуктов не миллионы).

Главное правило: кардинальность должна быть предсказуемой и контролируемой. Если вы знаете, что у вас ровно 500 нод кластера и 10 сервисов, то 5000 рядов - это нормально. Если вы не знаете верхнюю границу вариативности лейбла - это красный флаг.

Часто задаваемые вопросы

Чем отличается кардинальность метрик от объема данных?

Объем данных зависит от частоты сбора (например, каждые 15 секунд) и длительности хранения. Кардинальность - это количество уникальных временных рядов (комбинаций метрика+лейблы). Можно иметь малый объем данных, но огромную кардинальность (если сбор редкий, но рядов очень много), что нагружает индексацию и память. И наоборот: высокая частота сбора при низкой кардинальности дает большой объем на диске, но легкую индексацию.

Как влияет высокая кардинальность на работу Grafana?

Grafana сама по себе легковесна, но она отправляет запросы в бэкенд (Prometheus/VictoriaMetrics). При высокой кардинальности запросы типа «показать все линии для метрики X» требуют от базы просканировать огромный индекс. Это приводит к таймаутам запросов, медленной загрузке панелей и повышенному потреблению CPU на стороне сервера метрик, а не самой Grafana.

Можно ли динамически менять кардинальность без перезапуска сервиса?

Да, большинство современных библиотек (например, Micrometer для Java, prometheus-client для Python) позволяют регистрировать метрики динамически. Однако важно помнить, что удаление метрики из коллекции не всегда мгновенно удаляет соответствующие временные ряды из базы данных. Они могут оставаться активными до конца периода retention или пока не будут явно удалены средствами базы данных.

Что делать, если я уже утонул в кардинальности и не могу остановить продакшн?

Сначала используйте механизмы фильтрации на уровне scrape-конфигурации (как описано выше с relabel_configs), чтобы перестать собирать лишние лейблы. Это не требует изменения кода приложений. Затем, если база позволяет, очистите старые ненужные серии вручную или скриптом. В крайнем случае, временно увеличьте ресурсы (RAM/CPU) для базы данных, чтобы выиграть время для рефакторинга кода.

Подходит ли OpenTelemetry для решения проблем с кардинальностью?

OpenTelemetry (OTel) сам по себе не решает проблему кардинальности, так как это стандарт передачи данных, а не методология проектирования метрик. Однако OTel SDK предоставляет мощные инструменты для обработки данных (processors), которые позволяют фильтровать атрибуты спанов и метрик перед экспортом. Вы можете настроить экспортёр так, чтобы он отбрасывал высококардинальные атрибуты, оставляя только те, которые нужны для ваших дашбордов.