open atlas
↑ К треку
Разборы System Design SDC · 05 · 01

Спроектируй систему метрик и алертинга

Платформа метрик и алертинга: приём временных рядов на масштабе, pull против push, даунсэмплинг и уровни хранения, взрыв кардинальности, убивающий БД, вычисление правил алертов и дашборды, дешёвые на чтении.

SDC Senior ◷ 34 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Дежурный инженер добавил один невинный лейбл к метрике — user_id, чтобы разложить задержку запросов по клиентам. Деплой ушёл в пятницу. К воскресенью кластер мониторинга остался без памяти, все дашборды отваливались по таймауту, и команда летела вслепую в те самые выходные, когда глаза на системе нужны больше всего. Они не добавили трафика — они умножили число различных временных рядов на миллион. Один лейбл высокой кардинальности превратил здоровую БД метрик в бомбу из памяти. Урок: система метрик — это не система логов с графиками сверху, а специализированное хранилище временных рядов, весь дизайн которого определяется одним числом — сколько уникальных рядов оно должно держать в памяти одновременно.

Требования

Платформа метрик и алертинга непрерывно отвечает на один операционный вопрос: здорова ли система прямо сейчас, и если нет — кого будить? Это делится на функциональные и нефункциональные требования.

Функциональные. Собирать числовые измерения (счётчики, gauge, гистограммы) с тысяч сервисов и хостов. Хранить их как временные ряды — поток точек (timestamp, value), опознаваемых именем метрики плюс набором пар ключ/значение — лейблов (http_requests_total{service="checkout", status="500", region="eu"}). Давать инженерам запрашивать их выразительным языком (rate, агрегация, оценка перцентилей). Непрерывно вычислять правила алертов и слать в нотификатор (PagerDuty, Slack). Рисовать дашборды, которые перезапрашивают данные при каждом обновлении.

Нефункциональные. Приём должен быть дёшев на точку, потому что точек чудовищно много. Запросы по свежим данным должны быть быстрыми (дашборды обновляются каждые 15–30 с; нарушение SLO должно быть видно за секунды). Система должна стоять особенно когда наблюдаемая система падает — мониторинг, умирающий в инциденте, хуже бесполезного. И хранение многоуровневое: сырое разрешение на дни, грубые свёртки на годы.

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

Оценка

Прикинь до рисования квадратиков. Пусть 5 000 хостов, каждый экспортирует ~1 000 метрик, скрейп раз в 15 секунд.

активных рядов     = 5 000 хостов × 1 000 метрик            = 5 000 000 рядов
темп приёма        = 5 000 000 рядов ÷ 15 с                 ≈ 333 000 сэмплов/с
сэмплов в день     = 333 000 × 86 400                        ≈ 2,9 × 10^10 сэмплов/день
на диске (сырое)   = ~2,9 × 10^10 × ~1,5 байта (сжато)       ≈ 43 ГБ/день
индекс в памяти    = ~5 000 000 рядов × ~неск. сотен байт    ≈ несколько ГБ индекса лейблов

Важны два числа. Сэмплов в секунду (~333К/с) задаёт путь записи — комфортно для одного хорошо настроенного узла, тривиально шардируется дальше. Активные ряды (5М) задают память, потому что БД держит в памяти индекс от лейблов к рядам плюс самый свежий чанк каждого ряда. Теперь представь лейбл user_id с миллионом значений: 5М рядов становятся 5 миллиардами, индекс уже не влезает в RAM, и узел умирает — ровно как во вступлении. Сжатое хранение (Gorilla-стиль: delta-of-delta для меток времени и XOR для float, ~1,5 байта/сэмпл против 16 сырых) делает терпимым число на диске; ничто не делает терпимой неограниченную кардинальность.

Высокоуровневый дизайн

Форма такова: путь сбора (затащить сэмплы внутрь), ядро хранения (TSDB) и два пути чтения, которые его делят: ad-hoc запросы для дашбордов и плановый вычислитель правил, гоняющий тот же язык запросов, чтобы считать алерты и предагрегированные «recording rules». Отдельный alert manager дедуплицирует, группирует и маршрутизирует зажжённые алерты, чтобы один кривой деплой не разбудил двенадцать человек двенадцать раз. Старые данные компактятся и даунсэмплятся в дешёвое object storage для долгого хранения.

Глубокое погружение

Pull против push

Затащить сэмплы внутрь можно двумя способами, и выбор расходится волной по всему дизайну.

Pull (Prometheus): сервер держит список таргетов и по расписанию скрейпит HTTP-эндпойнт /metrics каждого. Сервер контролирует темп сэмплинга, неудачный скрейп — сам по себе сигнал (метрика up/down приходит бесплатно), а service discovery (из Kubernetes, Consul) держит список таргетов свежим. Цена: сервер должен дотягиваться до каждого таргета по сети, а короткоживущим задачам, исчезающим между скрейпами, нужен push-шлюз как обходной путь.

Push (StatsD, OpenTelemetry, line protocol InfluxDB): клиенты шлют сэмплы в коллектор. Подходит эфемерным задачам, serverless и клиентам за NAT, до которых сервер не дотянется. Цена: сервер не отличит «нет данных, потому что здоров и простаивает» от «нет данных, потому что отправитель умер», нужен отдельный сигнал живости, а кривой клиент может тебя залить — backpressure и rate-limiting переезжают на уровень приёма.

Ни один не верен повсеместно. Pull доминирует в мониторинге инфраструктуры (стабильные, обнаруживаемые таргеты); push — в событиях приложения и serverless. Многие крупные платформы гоняют оба: pull для флота, push-шлюз для батч-задач.

Почему это работает

Почему pull упрощает обнаружение отказа? Потому что отсутствие успешного скрейпа — информация, которую сервер наблюдает напрямую. С pull сервер знает, что пытался дотянуться до таргета X в 14:00:15 и получил connection refused, поэтому пишет up{instance="X"} = 0 — алерт может зажечься на этом без всякого содействия мёртвого таргета. С push тишина двусмысленна: сервер не видит сэмплов от X, но не отличит «X здоров и нечего было репортить» от «X упал» от «сеть между X и коллектором порвалась». Кончается тем, что ты добавляешь heartbeat-метрики и dead-man’s-switch алерты, чтобы воссоздать то, что pull даёт бесплатно. Поэтому мониторинг инфраструктуры тяготеет к pull: то, что больше всего хочешь обнаружить — погасший хост — ровно тот случай, который push обрабатывает хуже всего.

Кардинальность: взрыв, убивающий БД

Это важнейший операционный факт о системах метрик, и это вступление. Временной ряд однозначно опознаётся полным набором лейблов, так что число рядов — это произведение кардинальностей каждого лейбла:

рядов для одной метрики = ∏ (различных значений каждого лейбла)

http_requests_total{service, status, region}
  service: 50  ×  status: 8  ×  region: 5   = 2 000 рядов       ✅ норм

добавить лейбл user_id (1 000 000 различных):
  50 × 8 × 5 × 1 000 000              = 2 000 000 000 рядов ❌ БД умирает

Каждый ряд несёт фиксированный оверхед: запись в инвертированном индексе лейблов, открытый чанк в памяти для входящих сэмплов, файловые дескрипторы. Число активных рядов, а не темп сэмплов, выносит бюджет памяти — и оно растёт мультипликативно с каждым неограниченным лейблом. Правила, держащие его в узде, — необсуждаемая senior-практика: никогда не клади значения неограниченной кардинальности в лейблы (user ID, email, полные URL с query string, request ID, метки времени). Им место в логах или трейсах, построенных для поиска по ключам высокой кардинальности; метрики построены для агрегации по ограниченным наборам лейблов. Когда тебе и правда нужна задержка по клиенту, ты либо группируешь клиентов в ограниченный лейбл-тир, либо отвечаешь на этот вопрос из трейсов/логов, а не из TSDB (Time Series DataBase — специализированной БД временных рядов) метрик.

Частая ошибка

Соблазнительная ошибка — обращаться с лейблом метрики как с полем лога. Логи и трейсы индексированы для поиска высокой кардинальности — «покажи каждое событие по запросу abc-123» — и хранятся дёшево, потому что каждое событие живёт один раз. Лейбл метрики иной: он не добавляет поле к событию, он форкает целый новый временной ряд, который БД обязана отслеживать всю его жизнь, с оверхедом индекса и памяти на ряд. Так path="/user/12345/orders" не добавляет одно измерение; он плодит свежий ряд на каждый отдельный путь, навсегда. Починка — нормализовать до того, как станет лейблом: path="/user/:id/orders" (шаблон маршрута, ограниченный) — а сырое значение высокой кардинальности толкать в логи. Правило большого пальца: если не можешь перечислить возможные значения лейбла на доске, ему не место в метрике.

Даунсэмплинг и уровни хранения

Никому не нужно 15-секундное разрешение для графика длиной в год — и хранить его расточительно и медленно на запросе. Поэтому в фоне работает компактор, сливающий свежие высокоразрешённые блоки и считающий даунсэмплированные свёртки: 5-минутные агрегаты, затем 1-часовые, каждая хранится дольше. Типичная многоуровневая политика:

сырое (15 с)    → хранится 15 дней   (разбор инцидентов, свежие дашборды)
5-мин свёртка   → хранится 90 дней   (тренды ёмкости, недельные обзоры)
1-час свёртка   → хранится 2 года    (год-к-году планирование)

Даунсэмплинг не может просто хранить среднее — на каждое окно хранят несколько агрегатов (min, max, sum, count), чтобы запрос всё ещё мог посчитать осмысленный rate или приближённый перцентиль по грубым данным. Старые блоки живут в object storage (S3/GCS), а не на диске горячего узла; движок запросов читает их по требованию и кэширует результаты. Это kappa-подобное разделение между маленьким, быстрым, in-memory горячим уровнем и большим, дешёвым, холодным — и поэтому запрос «покажи год» медленнее, но возможен, а запрос «последние 30 минут» мгновенен.

Узкие места и компромиссы

Связывающее ограничение — память на активные ряды, и почти каждый компромисс о её защите. Кардинальность — это режим отказа; релейблинг/дроп на приёме и жёсткие лимиты рядов на таргет — защита. Разрешение против стоимости — это ручка: скрейп раз в 15 с покажет быстрые всплески, но обойдётся в 4× хранения против 60 с — большинство команд скрейпят часто и даунсэмплят рано. Свежесть алертов против стоимости запроса — скрытая связка: правила алертов гоняют тот же движок запросов, что и дашборды, так что тяжёлое recording rule или дорогой запрос дашборда конкурирует за тот же CPU — медленный путь запроса делает алерты поздними, и так система мониторинга тихо отказывает в том самом инциденте, который должна ловить. Зрелые системы предсчитывают горячие агрегаты recording rules (дешёвые чтения на момент алерта) и изолируют путь алертинга от ad-hoc нагрузки дашбордов. Наконец, система должна быть доступнее того, за чем следит: гоняй её отдельно (свой кластер, свой регион), держи зависимости минимальными и убедись, что когда всё остальное горит, то, что следит за пожаром, ещё стоит.

Викторина

Инженер добавляет лейбл `request_id` (уникальный UUID на запрос) к метрике, чтобы «упростить отладку». Что произойдёт, и где правильное место этим данным?

Викторина

Ты мониторишь флот стабильных, обнаруживаемых через service discovery хостов и больше всего хочешь ловить погасший хост. Pull или push, и почему?

Закончи аналогию

Число временных рядов для метрики — это _______ различных значений каждого из её лейблов; поэтому добавление одного неограниченного лейбла (вроде user_id) не добавляет чуть-чуть, а умножает число рядов к бесконечности и исчерпывает память БД.

Вспомните перед уходом
  1. 01
    Почему кардинальность, а не темп сэмплов, убивает БД метрик?
  2. 02
    Сравни pull и push и скажи, когда каждый побеждает.
  3. 03
    Как даунсэмплинг и уровни хранения держат систему дешёвой и быстрой?
Итог

Платформа метрик и алертинга — это специализированная БД временных рядов, а не система логов с графиками — и весь её дизайн гнётся вокруг одного числа, количества активных рядов, равного произведению кардинальностей каждого лейбла. Оценка делает это конкретным: ~5М рядов при ~333К сэмплов/с комфортны для пропускной способности записи, но задают многогигабайтный бюджет памяти, который один неограниченный лейбл (user_id из вступления) раздувает в миллиарды. Архитектура — это путь сбора (pull для стабильных обнаруживаемых флотов, ловящий хост-даун бесплатно; push для эфемерных/serverless за NAT), ядро TSDB, держащее горячие ряды в RAM со сжатием в стиле Gorilla, и два пути чтения — дашборды и вычислитель правил — которые его делят. Старые данные даунсэмплятся компактором в уровни хранения (сырое 15с → 5-мин → 1-час) в дешёвом object storage. Узкое место всегда — память на активные ряды, защищаемая ограниченными лейблами и дропом на приёме; тонкая ловушка в том, что алертинг гоняет тот же движок запросов, что и дашборды, так что дорогой запрос делает алерты поздними — поэтому зрелые системы предагрегируют recording rules, изолируют путь алертов и гоняют мониторинг надёжнее того, за чем он следит. Теперь, когда встретишь предложение добавить лейбл к метрике, первый вопрос будет автоматическим: сколько различных значений он может принять? Если не перечислить на доске — это логи или трейсы, а не лейбл.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.