Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Высокая кардинальность метрик: причины и решения

Высокая кардинальность метрик: почему Prometheus «пухнет» и что делать

Что такое кардинальность временных рядов, какие лейблы её взрывают, как найти источник роста и какие приёмы снижают стоимость метрик без потери полезности.

Прямой ответ

Кардинальность — число уникальных комбинаций имя метрики + все лейблы (каждая комбинация — отдельный временной ряд — метрики-основы); стоимость метрик пропорциональна не «числу метрик», а числу рядов: http_requests_total{method,path,status} с 10 маршрутами × 4 метода × 6 статусов = 240 рядов (норма), но path как «сырой URL с id» (/users/12345) = миллион рядов (каждая сущность — свой ряд — взрыв); симптомы высокой кардинальности: память/CPU Prometheus растёт (каждый ряд — индекс+буферы — ёмкость), запросы тормозят (агрегации перебирают ряды — дашборд открывается 30 секунд — Grafana-боль), remote write в Mimir/VictoriaMetrics дорогой (Mimir — стоимость хранения — линейна рядам); главные виновники (по частоте инцидентов): (1) id в лейблах (user_id/order_id/ip клиента/trace_id — «полезно для дебага» — бесконечная кардинальность — данные — не метрики — такие вещи идут в логи/трейсы (логи, трейсы — «каждому сигналу своё» — метрики для агрегатов); (2) неограниченные значения (error_message как лейбл — текст ошибки уникален — «лейбл со свободным значением» — бомба; path без нормализации (query-параметры/сегменты-id — route-шаблон (/users/:id) вместо сырого пути — метрики-паттерн); (3) гистограммы с лишними бакетами (гистограммы — 15 бакетов × высокая кардинальность основного имени — ×16 рядов на комбинацию — дефолтные бакеты часто избыточны — свои бакеты под SLO-пороги — 8 вместо 15 — вдвое дешевле при той же полезности); (4) k8s-метки (pod_name/uid пересоздаваемых подов — ряды множатся с каждым рестартом (HPA-контекст) — churn — «новые ряды вместо обновления старых» — relabeling-решение); (5) дефолтные метрики библиотек (client-библиотеки шлют всё — неиспользуемые метрики — половина рядов мертва — инструментация-ревизия — «метрики на всякий случай» стоят денег всегда); диагностика: topk(10, count by (__name__)({__name__=~".+"})) (топ метрик по рядам — первый запрос при «пухнущем» Prometheus), prometheus_tsdb_head_series (динамика рядов — рост — метрики самого Prometheus), count by (job)({__name__=~".+"}) (какой сервис виноват); лечение по приоритетам: убрать id-лейблы (перенести в логи с теми же данными — корреляция через trace_id — Alloy-пайплайн), нормализовать значения (route-шаблоны, enum вместо текста — «статусы-коды, не сообщения»), metric_relabel_configs дроп (лишнее — не принимать — конфигурация — «лучшая метрика — та, что не собрана»), recording rules агрегаты (сырьё не хранить долго — агрегаты вместо деталей — recording rules), бакеты гистограмм под задачу; правило команды (профилактика): новые метрики — через ревью (чеклист — «лейбл с неограниченным значением» — red flag — техдолг-предотвращение).

Практика: находим и обезвреживаем взрыв рядов

Алгоритм инцидента «Prometheus ест 40 ГБ RAM»: (1) локализация — topk(5, count by (__name__, job)({__name__=~".+"})) — виновник: api_request_duration_seconds — 3.2M рядов (из 4M всего — 80%); (2) декомпозиция — count by (path) (api_request_duration_seconds) — top-значения path: /orders/8f3a.../status, /users/12345 — сырые URL (route-шаблон не подставлялся — middleware писал request.path — FastAPI-метрики — «одна строка кода — 3M рядов»); (3) фикс кода: request.scope["route"].path (шаблон /orders/{id} — кардинальность = число маршрутов = 40 — ×80 000 меньше); (4) чистка истории: старые ряды живут в TSDB до истечения retention (память не освободится сразу — storage-механика — head-срез — «починили, а память та же» — ждём retention/чистим блок при критичности — --storage.tsdb.retention.time — конфигурация — аналогия с vacuum-ожиданием — vacuum); (5) защита на входе: metric_relabel_configs на скрейпе (drop рядов с path, содержащим 10+ символов после / — «сырой путь не пройдёт» — даже при регрессии кода), лимит sample_limit в ServiceMonitor (Prometheus Operator — «сервис прислал 100k сэмплов — скрейп отклонён — алерт» — взрыв виден сразу, не через месяц); (6) профилактика: тест-assert на кардинальность в CI (тесты — «метрика с user_id — красный тест» — линт-скрипт по коду инструментации), дашборд «топ рядов по сервисам» (Grafana — тренд — рост — разбор — алерты — «кардинальность +20% за день» — алерт до инцидента — Alertmanager-маршруты); (7) экономика: после фикса — 4M → 800k рядов, RAM 40→9 ГБ, remote write трафик −75% (Mimir-стоимость — счёт облака −60% за месяц — ёмкость-деньги); вывод: кардинальность — дизайн-решение при создании метрики (каждый лейбл — множитель — «ещё один полезный лейбл» — ×N рядов — вопрос «нужна ли эта размерность в агрегатах или достаточно в логах» — центральный вопрос инструментации — три столпа).

FAQ

Какая кардинальность считается высокой и где границы? Ориентиры (зависят от железа/бэкенда): один Prometheus на 4–8 ГБ RAM — комфортно до ~1M активных рядов, 10M+ — тюнинг/шардинг/внешнее хранилище (Mimir/VictoriaMetrics — эффективнее на больших объёмах); на метрику: единицы-сотни рядов — норма, тысячи — вопрос «зачем», 10k+ на одну метрику — почти всегда ошибка дизайна (id-лейбл/не нормализовано — раздел выше); на лейбл: значения — ограниченный enum (статусы/методы/маршруты — десятки) — ок, «значение из открытого множества» (id/текст/hostname растущего парка — тысячи) — красный флаг; практическая проверка при ревью: «сколько уникальных значений будет у лейбла через год» — если ответ «не знаем/растёт» — метрика не проектируется, а взрывается — планирование — кардинальность планируется как нагрузка.

Как найти, какая метрика/сервис создаёт больше всего рядов? Запросы-диагностика: topk(10, count by (__name__)({__name__=~".+"})) (топ-10 метрик по числу рядов — 90% случаев виновник в этом списке), count by (job)({__name__=~".+"}) (по сервисам — кто генерит), count by (__name__, instance)({...}) (инстанс-разрез — один pod сошёл с ума — k8s-контекст); статус Prometheus: /api/v1/status/tsdb (topCardinality-раздел — head-ряды по метрикам — веб-интерфейс без запросов), prometheus_tsdb_head_series + rate(prometheus_tsdb_head_series_created_total[1h]) (динамика — churn — «ряды создаются и умирают» — k8s-пересоздания — relabeling); для Mimir/VM — встроенные топ-тулзы (mimirtool analyze — отчёт по usage — Mimir, vmctl/topk по active series — VictoriaMetrics); ритуал: ежемесячный «кардинальность-отчёт» (топ-растущие метрики — разбор — техдолг-ревью — «метрики-ревизия» — как зависимость-аудит — зависимости — по расписанию, не по инциденту).

Что делать с id-подобными данными, если их хочется видеть в метриках? Правильный путь — другой сигнал: id-контекст живёт в логах (поле user_id в структурированном логе — Loki-запрос «ошибки пользователя X» — LogQL — та же информация без рядов) и трейсах (атрибуты спанов — OTel — «трейсы конкретного заказа» — exemplars связывают метрику с примером — процентиili — «p99 → конкретный медленный запрос» — клик без id-лейбла); компромиссы, когда агрегат с id всё же нужен: top-N (топ-10 клиентов отдельными рядами, остальные — other — «важные тенанты видны, кардинальность ограничена» — ёмкость-паттерн), бакетирование (id → диапазон/хэш-бакет — «когорта» вместо индивида), отдельный дашборд-источник (детали — из БД/логов по запросу, не из TSDB — dbt-витрины); антипаттерн: «добавим user_id временно» — временно живёт годами — ряды копятся — техдолг — метрики с id запрещены политикой (ревью — «а как же дебаг» — через логи/трейсы — сценарий выше).

Помогают ли Mimir/VictoriaMetrics против высокой кардинальности? Помогают с объёмом, не с дизайном: эффективнее хранят/индексируют (VM — ряды в разы дешевле по RAM — VictoriaMetrics, Mimir — горизонтальный масштаб (Mimir) — «3M рядов» перестанут ронять Prometheus, но: стоимость всё равно линейна рядам (облачный счёт Mimir/VictoriaMetrics Cloud — «id-лейбл» ×1000 в счёте — деньги), запросы по высококардинальным данным медленные везде (агрегация 3M рядов — дашборд тормозит независимо от бэкенда — Grafana), полезность не появляется (ряд /users/12345 просмотрен ноль раз — хранение мусора в масштабе — дороже); порядок действий всегда: дизайн метрик (сценарий выше — нормализация/логи) → recording rules агрегаты (агрегация) → потом бэкенд-масштаб; «купим Mimir, кардинальность не проблема» — дорого и неправда — проблема дизайна решается дизайном — метрики-культура.

Источники: документация Prometheus (naming, storage), Grafana-блог о гистограммах — ссылки в шапке.