Мониторинг: метрики, логи, трейсы и что из этого когда
В главе про SLI и SLO мы выбирали показатель, который что-то значит для пользователя. В главе про бюджет ошибок считали, сколько этого показателя можно потратить. Обе главы молча опирались на допущение, которое пора вскрыть: что показатель вообще откуда-то берётся, и берётся правильно.
Это допущение неверно по умолчанию. SLO «99,9 % запросов быстрее 300 мс» невозможно проверить, если гистограмма латентности собрана с границами бакетов 0,25 и 0,5 секунды — между ними нет числа 0,3, и никакой запрос к хранилищу его не достанет. SLO «99,95 % успешных ответов» будет весело выполняться, пока падает DNS: сервер не видит запросов, которых до него не дошли, а значит и ошибок у него ноль. Мониторинг — это не «графики, чтобы смотреть», а измерительная система, у которой есть разрешение, систематическая ошибка, слепые зоны и цена. Эта глава — про устройство четырёх типов сигналов, про то, какой класс вопросов каждый закрывает и сколько стоит, и про то, где инженерное решение упирается в организационное. Как из сигналов делать поводы кого-то будить — следующая глава.
Мониторинг отвечает на два разных вопроса
Разделение, которое экономит много споров:
- «Сейчас плохо?» — вопрос про состояние. Ответ нужен всегда, быстро, дёшево, круглосуточно, с гарантией доставки. Это симптомная телеметрия: она отслеживает то, что чувствует пользователь.
- «Почему плохо?» — вопрос про причину. Ответ нужен изредка, только во время расследования, зато подробный и в разрезе, который заранее не был известен.
Требования у этих задач противоположные. Первая хочет узкого, стабильного, всегда включённого потока. Вторая — широкого, богатого, дорогого, который держат в горячем виде дни, а не годы. Попытка обслужить обе одним инструментом даёт либо дорогой мониторинг, который всё равно не отвечает на «почему», либо дешёвый, который врёт про «сейчас». Отсюда следствие, которое стоит принять до выбора технологий: симптомная телеметрия обязана быть простой настолько, чтобы работать в момент, когда сломано всё остальное; причинная имеет право быть сложной, потому что её отказ не мешает узнать, что сервис лежит.
Слово «observability» (наблюдаемость) в маркетинге означает «наш продукт дороже». В инженерном смысле определение полезное и проверяемое: система наблюдаема, если по её внешним выходам можно ответить на вопрос, который вы не задавали заранее, не выкатывая новый код. Тест простой: пришла жалоба «у клиентов из Казахстана с Android медленно оформляется заказ» — можете ли вы проверить это за десять минут по уже собранным данным? Если нужно добавить метку и подождать деплоя, система ненаблюдаема в этом разрезе, и это факт, а не оценка.
Четыре сигнала и что каждый умеет
| Сигнал | Единица данных | Отвечает на вопрос | Кардинальность | Стоимость | Типичный срок хранения |
|---|---|---|---|---|---|
| Метрики | число во времени с набором меток | «сколько сейчас и как менялось» | низкая, известна заранее | очень низкая на запрос | 13–15 мес. после прореживания |
| Логи | текстовая или структурированная запись события | «что именно произошло в этом месте кода» | любая | высокая, растёт с трафиком | 7–30 дней горячих |
| Трейсы | дерево спанов одного запроса | «где ушло время и кто кого звал» | любая | высокая, режется сэмплированием | 3–14 дней |
| Профили | распределение ресурса по стекам вызовов | «на чём именно занят процессор или память» | средняя | средняя, непрерывный сбор | 7–30 дней |
Пятый сигнал, формально не сигнал, но по практической ценности во время инцидента обгоняющий остальные, — журнал изменений: деплои, миграции, переключения фичефлагов, изменения конфигурации, работы провайдера. Большинство инцидентов начинается с изменения, и первый вопрос дежурного — «что поменялось за последний час»; если ответ не выводится на тот же экран, что и графики, вы потеряете двадцать минут на переписку. Стоит это почти ноль — аннотации на графике по событию из CI (стратегии релиза, а безопасные выкатки — глава 13).
Правило, которое реально решает, что куда класть
Формулировка, которая работает лучше всех таксономий:
Метрики отвечают на вопросы, которые вы знали заранее. Логи и трейсы — на те, которых не знали.
Метрика — это заранее выбранная агрегация. Вы решили считать запросы в разрезе маршрута и кода ответа — получите ответ мгновенно и дёшево, но только про маршрут и код. Вопрос «а как у клиентов конкретного тарифа» метрика не берёт, если разрез не заложен, а заложить его — значит умножить количество рядов. Лог и трейс, наоборот, хранят сырое событие со всем контекстом и отвечают на любой вопрос — за деньги и за время выполнения запроса к хранилищу.
Из этого правила выводится всё остальное: низкокардинальные разрезы, нужные постоянно, — в метрики; высококардинальный контекст (идентификаторы пользователя, заказа, запроса) — в логи и трейсы. Метка user_id в метрике — почти всегда ошибка проектирования, и сейчас будет видно, во сколько она обходится.
Метрики: арифметика кардинальности
Временной ряд в системах вроде Prometheus — это уникальная комбинация имени метрики и всех значений меток. Не метрика — ряд. Строка http_requests_total{service="orders", route="/checkout", method="POST", code="200"} — один ряд. Поменяйте любое значение — другой ряд, отдельный кусок памяти, отдельная запись в индексе.
Посчитаем на реальном порядке величин: 12 сервисов, в среднем по 25 маршрутов, 2 метода на маршрут, 5 различимых классов кода ответа.
service— 12 рядов;+ route— 12 × 25 = 300;+ method— 600;+ code— 3 000.
Три тысячи рядов — это ничто, любой ноутбук потянет. Дальше начинается интересное.
- Добавили
pod(мы же хотим видеть, какой инстанс тормозит), 60 подов: 3 000 × 60 = 180 000. - Латентность считаем гистограммой с 12 бакетами. Гистограмма Prometheus — это 12 рядов
_bucketплюс+Inf, плюс_sum, плюс_count= 15 рядов вместо одного; возьмём 14 как среднее по метрикам с меньшим числом бакетов: 180 000 × 14 = 2 520 000. - И кто-то добавляет
user_id, потому что «полезно же»: 2 520 000 × 50 000 = 1,26 × 10¹¹.
Последнее число не «много». Оно физически невозможно, и вот почему.
Память. Эмпирическое правило для Prometheus — от 2 до 8 КБ оперативной памяти на активный ряд (голова TSDB, индекс, незакрытый чанк), зависит от версии и от того, сколько рядов вы одновременно трогаете запросами. При 4 КБ: 3 000 рядов → 12 МБ (незаметно); 180 000 → 720 МБ (уже надо думать); 2 520 000 → около 10 ГБ только на активные ряды, без памяти под выполнение запросов, то есть отдельная машина с 32 ГБ на грани.
Диск. Prometheus после сжатия хранит примерно 1,3–2 байта на сэмпл. При интервале сбора 15 секунд ряд даёт 4 сэмпла в минуту, то есть 5 760 в сутки. Для 2,52 млн рядов при 2 байтах:
2 520 000 × 5 760 × 2 Б = 29,0 ГБ в сутки ≈ 871 ГБ в месяц
При горячем хранении 15 дней это 435 ГБ на диске — терпимо, но растёт линейно вместе с кардинальностью, без пощады.
Привычка, которая всё это предотвращает: прежде чем добавить метку, умножить в уме. Метка на 60 значений — это ×60 к памяти, диску, времени выполнения запросов и счёту от вендора; иногда оно того стоит (pod часто стоит). Метка на 50 000 значений не стоит никогда.
Где здесь организационная граница. Убрать метку технически — одна строка. Организационно — вы отнимаете разрез у команды, которая на него смотрит раз в квартал, но в тот раз он спас ей полдня. Аргумент «это стоит нам 10 ГБ памяти» проигрывает аргументу «мне это было нужно», если у затрат нет владельца. Работает только явное правило: у каждой метрики есть владелец и потребитель (алерт, панель SLO или регулярный отчёт), а ряд без потребителя удаляется по ревизии. Правило должно быть записано и согласовано до, а не во время спора.
Типы метрик и что с ними нельзя делать
Counter — монотонно растущий счётчик. Абсолютное значение бессмысленно, смотреть надо на производную: rate() или increase(). Счётчик переживает рестарт процесса сбросом в ноль, и корректная функция расчёта скорости это обнаруживает; поэтому счётчики никогда не «уменьшают» — убывание означает рестарт, а не бизнес-событие.
Gauge — мгновенное значение: длина очереди, открытые соединения, память. Опасность в том, что между двумя измерениями с интервалом 15 секунд может произойти что угодно: пик очереди длиной 3 секунды виден с вероятностью около 20 %. Если пики важны — считайте их счётчиком превышений порога, а не gauge.
Histogram — счётчики попаданий в бакеты, из которых оценивают квантили: единственный корректный способ смотреть на распределение задержек в агрегате. Summary — квантили, посчитанные на стороне приложения; обычно не берут из-за главного свойства квантилей — они не складываются.
Почему нельзя усреднять перцентили
Это не педантизм, это ошибка в разы. Два инстанса за одно окно:
- A: 10 000 запросов, из них 9 800 по 10 мс и 200 по 800 мс. Сортируем: позиции 1–9 800 — 10 мс, позиции 9 801–10 000 — 800 мс. Позиция p99 — 9 900-я, попадает в хвост. p99(A) = 800 мс.
- B: 10 000 запросов, все ровно по 20 мс. p99(B) = 20 мс.
Дашборд усредняет: (800 + 20) / 2 = 410 мс. Красиво, тревожно, неправильно. Истинный p99 объединённого потока: всего 20 000 запросов, позиция p99 — 19 800-я; отсортированный ряд — 9 800 значений по 10 мс (позиции 1–9 800), затем 10 000 по 20 мс (позиции 9 801–19 800), затем 200 по 800 мс. Позиция 19 800 — это 20 мс, то есть истинный p99 = 20 мс.
Ошибка в 20 раз, и не в консервативную сторону: усреднение выдумало проблему, которой нет. Бывает и наоборот — усреднение прячет реальный хвост одного инстанса среди здоровых соседей. Правильно так: суммировать бакеты гистограммы по всем инстансам, а квантиль считать один раз от суммы.
# Верно: складываем счётчики бакетов, потом оцениваем квантиль
histogram_quantile(
0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{job="checkout"}[5m]))
)
# Неверно: сначала квантиль по каждому инстансу, потом среднее
avg(histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{job="checkout"}[5m])))
То же касается усреднения по времени: «средний p99 за месяц» — величина без интерпретации. Про то, почему усреднение по времени вообще опасно при разговоре о доступности, — ниже, в разделе про бюджет.
Гистограммы: что именно возвращает квантиль
Гистограмма не хранит значения, она хранит счётчики «сколько наблюдений было не больше, чем le», а квантиль восстанавливается интерполяцией внутри бакета, в который попала нужная позиция. Отсюда конкретная арифметика. Пусть за окно 10 000 запросов, накопленные счётчики:
граница le |
накоплено |
|---|---|
| 0,1 | 9 700 |
| 0,25 | 9 850 |
| 0,5 | 9 990 |
| 1 | 10 000 |
Позиция p99 — 9 900-я. Она между le=0,25 (9 850) и le=0,5 (9 990). В бакете (0,25; 0,5] — 140 наблюдений, нам нужно 50-е из них. Линейная интерполяция:
0,25 + (9 900 − 9 850) / 140 × (0,5 − 0,25) = 0,25 + 0,357 × 0,25 = 0,339 с
Формула предполагает равномерное распределение внутри бакета. В реальности задержки обычно сгрудились у левого края: если все 140 наблюдений лежат около 0,26 с, истинный p99 — 0,26 с при оценке 0,339 с, ошибка 30 %; лежали бы у 0,49 — оценка была бы занижена. Что из этого следует практически:
- Значение
histogram_quantile— утверждение «p99 лежит внутри бакета», а не число. Точность равна ширине бакета, и знаки после запятой этого не меняют. - Границы бакетов выбираются под пороги SLO, а не по умолчанию. Если SLO обещает «быстрее 300 мс», в наборе обязана быть граница
0,3: тогда проверка перестаёт зависеть от интерполяции — доля считается делением счётчиков, точно. - Дефолтный набор Prometheus (
.005 .01 .025 .05 .1 .25 .5 1 2.5 5 10) не подходит почти никому: он про сервис с задержками в десятки миллисекунд и разваливается на медленных ручках.
# Точная доля запросов быстрее порога SLO — без интерполяции,
# потому что граница 0.3 явно есть в наборе бакетов
sum(rate(http_request_duration_seconds_bucket{job="checkout", le="0.3"}[5m]))
/
sum(rate(http_request_duration_seconds_count{job="checkout"}[5m]))
Стоит знать про нативные гистограммы Prometheus: вместо фиксированных границ — экспоненциальная сетка с настраиваемым разрешением, хранится в одном ряду вместо пятнадцати, что снимает и проблему выбора бакетов, и часть проблемы кардинальности (документация).
И ещё одна систематическая ошибка, о которой в контексте мониторинга забывают, — coordinated omission: если замеряющий клиент блокируется в ожидании ответа, он перестаёт слать запросы во время замедления, и самые плохие замеры просто не появляются, а распределение выходит оптимистичным на порядок. Классический разбор — доклад Гила Тене «How NOT to Measure Latency»; про корректные замеры — «Измерения» и нагрузочное тестирование.
Каркасы: золотые сигналы, RED, USE
Три ходовых набора, отвечающих на вопрос «с чего начать инструментирование».
- Четыре золотых сигнала (Google SRE): задержка, трафик, доля ошибок, насыщение. Деталь, которую все теряют: задержку успешных и неуспешных запросов надо считать раздельно — быстрый 500-й ответ прекрасно улучшает общий p99 и маскирует аварию.
- RED (Rate, Errors, Duration) — для сервисов, обрабатывающих запросы: три метрики на сервис, одинаковые у всех, отсюда единый дашборд для всего парка (формулировка Тома Уилкинса).
- USE (Utilization, Saturation, Errors) — для ресурсов: CPU, диск, пул соединений, очередь (метод Брендана Грегга). USE отвечает «что упёрлось», RED — «кому от этого плохо».
Честная оговорка: каркас — это чеклист покрытия, а не выбор SLI. RED даст долю ошибок по HTTP-коду; является ли она тем, что чувствует пользователь, каркас не знает — сервис, честно отдающий 200 OK с пустым списком товаров из-за упавшего поиска, по RED идеально здоров. Выбор показателя — работа из главы 02. Отдельно стоит насыщение: это единственный сигнал, который опережает аварию (загрузка пула 85 % ещё не даёт ошибок, но говорит, что запас кончается), и превращается он в планирование ёмкости.
Какой сигнал брать под какой вопрос
заранее?"} A -->|"да, спрашиваем постоянно"| M["Метрика
дёшево, всегда есть,
низкая кардинальность"] A -->|"нет, возник по ходу"| B{"Про один запрос
или про поток?"} M -->|"видно, что плохо,
но не видно где"| B B -->|"один запрос,
несколько сервисов"| T["Трейс
где ушло время,
кто кого звал"] B -->|"один запрос,
один сервис"| L["Лог
какие значения,
какая ветка кода"] B -->|"поток, разрез
заранее неизвестен"| W["Широкие события
одна запись на запрос,
десятки полей"] T --> C{"Время сгорело внутри
одного процесса?"} C -->|"да, и непонятно на чём"| PR["Профиль
CPU, аллокации,
блокировки"] C -->|"нет, ждали соседа"| T2["Идём в трейс соседнего сервиса"]
Порядок здесь не случайный: метрика → трейс → лог → профиль. Каждый следующий шаг дороже предыдущего и требует более узкого вопроса. Дежурный, который начинает расследование с грепа по логам, гарантированно потратит больше времени, чем тот, кто сначала посмотрел, какая доля запросов и какого именно сервиса пострадала.
Логи: дороже, чем кажется
Считаем на том же порядке величин: сервис держит 10 000 запросов в секунду, одна строка access-лога в структурированном виде — примерно 600 байт (метки, идентификаторы, путь, коды, тайминги).
10 000 × 600 Б = 6 МБ/с = 518 ГБ в сутки
И это только access-лог. Прикладные логи обычно дают ещё в 3–5 раз больше строк, то есть реалистично 1,5–2,5 ТБ в сутки сырых данных; после сжатия и с учётом индексов на диске остаётся примерно того же порядка, а хранение 30 дней — это 45–75 ТБ. Такой кластер стоит уже не как «часть инфраструктуры», а как отдельная статья бюджета, часто сопоставимая со стоимостью самого сервиса.
Отсюда практические решения, каждое со своей платой:
- Структурированные логи (JSON) вместо текста. Плата — читаемость глазами в терминале, выигрыш — фильтрация по полю вместо регулярных выражений. На объёмах больше пары гигабайт в сутки альтернативы нет.
- Разные сроки хранения по классам: access 30 дней, debug 3 дня, аудит и события безопасности — по требованиям регулятора, отдельно и неизменяемо («Приватность и соответствие требованиям»).
- Сэмплирование логов не «каждая десятая строка», а по классам: все ошибки, все медленные, 1 % успешных. Один из немногих приёмов, снижающих счёт в разы без потери диагностической ценности.
- Никаких персональных данных и секретов в логах — не из благочестия: лог живёт дольше и копируется шире, чем база, и утекает вместе с доступом к системе логирования («Управление секретами»).
{"ts":"2026-07-16T10:04:12.318Z","level":"error","service":"checkout",
"trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7",
"route":"/api/orders","order_id":"ord_81f3","user_id":"u_44921",
"err":"payment_gateway_timeout","upstream":"billing","latency_ms":3021,
"attempt":2,"deploy":"checkout@2026.07.16-3"}
Здесь важны три поля: trace_id связывает строку с трейсом, deploy — с журналом изменений, attempt показывает, что это ретрай. Первое поле — то, что превращает логи из свалки в инструмент; без него связывание сигналов делается глазами по времени, а это работает только при трафике «десятки запросов в минуту».
Логи как участник каскада
Отдельный сюжет: логирование при аварии — усиливающая петля обратной связи. Механизм: сервис начинает ошибаться → на каждую ошибку пишется stack trace (в 20–50 раз длиннее обычной строки) → объём логов растёт в десятки раз → лог-агент упирается в CPU и сеть → сервису достаётся меньше ресурсов → ошибок становится больше. Плюс заполнение диска, после которого падает всё на хосте, включая то, что работало. Это ровно та структура, которую системное мышление называет усиливающей петлёй: следствие возвращается к причине с тем же знаком и разгоняет само себя («Петли обратной связи»; к каскадам в нашем треке — глава 11).
Что с этим делают: ограничение скорости записи логов на уровне приложения (rate limiting логгера по ключу сообщения), отдельный раздел диска под логи, жёсткий лимит CPU для агента и — обязательно — дедупликация повторяющихся сообщений (... и ещё 14 812 таких же за минуту).
Трейсы: единственный сигнал про причинность между сервисами
Трейс — дерево спанов одного запроса; спан — интервал с началом, концом, именем, атрибутами и ссылкой на родителя. Вместе они дают то, чего не даст ни метрика, ни лог: порядок и вложенность вызовов конкретного запроса, из которого видно, где именно из восьмисот миллисекунд ушло четыреста. Склеивается это контекстом, который передаётся между процессами: стандарт — W3C Trace Context, заголовок traceparent формата версия-trace_id-span_id-флаги. Ключевая часть работы — не собрать спаны, а не потерять контекст на каждой границе.
клиентская часть задержки уже потеряна G->>O: HTTP + traceparent O->>D: SQL — в протоколе места под контекст нет Note over O,D: связать медленный запрос с трейсом можно
только комментарием-хинтом в тексте SQL D-->>O: ответ за 420 мс O->>Q: publish, контекст кладём в заголовки сообщения Note over Q,B: если продюсер не положил контекст,
трейс рвётся здесь и склеить нечем Q-->>B: consume, спан продолжает тот же trace_id B->>B: обработка 2 с — в ответе пользователю не видна O-->>G: 200 OK G-->>U: 200 OK за 480 мс
Три места разрыва на этой диаграмме встречаются постоянно:
- Клиент не начинает трейс. Всё, что происходит до входа в вашу сеть — DNS, TLS, мобильная сеть, — вне трейса: сервер честно рапортует 480 мс, пользователь ждёт 2,5 секунды.
- База данных не участвует в трейсе. SQL-протокол не переносит контекст; обходной путь — комментарий в запросе (
/*traceparent=00-4bf9...-01*/ SELECT ...), который потом видно вpg_stat_activityи в медленном логе. - Асинхронная граница. Очередь передаёт контекст, только если продюсер явно положил его в заголовки сообщения; пропустили — и вся асинхронная часть обработки превращается в обрывки без связи с запросом («Обмен сообщениями»).
Сэмплирование: арифметика
Объём. 10 000 запросов в секунду, 8 спанов на запрос, 400 байт на спан после сжатия:
10 000 × 8 × 400 Б = 32 МБ/с = 2,76 ТБ в сутки
Хранить это целиком не будет никто, поэтому трейсы сэмплируют — и здесь возникает главная ошибка проектирования.
Head sampling — решение принимается в начале запроса, случайно, с фиксированной вероятностью. При 1 % объём падает до 27,6 ГБ в сутки — прекрасно. Теперь считаем, что мы этим купили и что потеряли. Пришла жалоба: «у меня всё тормозит», за сессию клиент сделал 50 запросов. Вероятность, что хотя бы один попал в выборку:
1 − 0,99⁵⁰ = 1 − 0,605 = 0,395
Меньше 40 %. В шести случаях из десяти вы отвечаете «трейсов по вашей проблеме нет». При 0,1 %: 1 − 0,999⁵⁰ = 4,9 %, то есть практически никогда. При этом для агрегированной статистики того же 1 % более чем достаточно: при 10 000 rps это 8,64 млн трейсов в сутки, погрешность ничтожна. Head sampling хорош для «как в среднем» и бесполезен для «что случилось у вот этого».
Tail sampling — решение принимается после завершения трейса, когда известны длительность и статус: сохранять все трейсы с ошибками, все медленнее порога и 1 % остальных как фон. Плата: коллектор держит спаны в памяти до истечения окна ожидания и обязан получать все спаны одного трейса — нужна маршрутизация по trace_id, а это уже не тривиальная установка.
# OpenTelemetry Collector: сохраняем всё интересное и 1 % фона
processors:
tail_sampling:
decision_wait: 10s # ждём завершения трейса
num_traces: 100000 # сколько трейсов держим в памяти
policies:
- name: errors # все ошибки — целиком
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow # всё медленнее порога SLO
type: latency
latency: { threshold_ms: 300 }
- name: baseline # фон для статистики
type: probabilistic
probabilistic: { sampling_percentage: 1 }
Оценим объём такой политики: пусть ошибок 0,3 % запросов, медленных ещё 1 %, фон 1 % — итого около 2,3 % против 100 %, то есть 63 ГБ в сутки вместо 2,76 ТБ при полном покрытии проблемных запросов. Разница между этим и наивным head sampling — не в деньгах, а в том, отвечает ли система на вопросы во время инцидента (обзор подходов).
Связывание сигналов
Три сигнала по отдельности — три вкладки, между которыми дежурный переключается вручную, сверяя время. Связанные — один путь расследования. Что для этого нужно:
- Единые имена меток во всех сигналах:
service,env,versionназываются одинаково в метрике, логе и спане. Для этого есть семантические соглашения OpenTelemetry, и брать их, а не изобретать свои, стоит хотя бы потому, что готовые дашборды рассчитаны на них. trace_idв каждой строке лога — реализуется один раз в middleware; аннотации деплоев на всех графиках — дёшево и окупается на первом же инциденте.- Exemplars в метриках: к бакету гистограммы прикрепляется
trace_idзапроса, попавшего в этот бакет. Вы видите на графике всплеск p99, кликаете по точке и попадаете в трейс запроса, который его создал, — единственный способ перейти от агрегата к примеру за один шаг, а не поиском по времени.
Где мерить: сервер, клиент, синтетика
Серверная метрика систематически оптимистичнее реальности, и разрыв надо оценивать, а не игнорировать. Пусть сервер отдаёт 0,05 % ошибок — красивая цифра, 99,95 %. Но на пути пользователя есть ещё DNS, TLS, мобильная сеть с обрывами, CDN и клиентский код; допустим, там теряется ещё 0,4 % сессий. Тогда пользователь видит:
100 % − 0,05 % − 0,4 % ≈ 99,55 %
В минутах за 30-дневный месяц (43 200 минут): вместо обещанных 43,2 минуты недоступности (99,9 %) пользователи получают 43 200 × 0,0045 = 194 минуты. Больше трёх часов, из которых на серверных графиках видно 22 минуты. Разрыв в девять раз, и это не гипербола — типичная картина для мобильных клиентов.
Что закрывает разрыв: RUM (real user monitoring) — телеметрия из браузера или приложения (реальное время до первого байта, ошибки сети и JS), единственный источник правды про ощущения пользователя; и синтетические (blackbox) пробы — регулярный запрос по критическому сценарию извне вашей сети. Пробы отвечают на вопрос «а вообще снаружи открывается» и работают, когда сервис лежит настолько, что метрик от него нет вовсе: «данных нет» и «всё хорошо» — разные состояния, различать их умеет только внешний наблюдатель. У замеров на клиенте своя цена: их объём пропорционален числу пользователей, а не серверов, и качество сети вы не контролируете; практический компромисс — сэмплировать RUM агрессивно (1–5 %) и считать по нему распределения, а не абсолютные значения.
Мониторинг и бюджет ошибок: сколько минут съедает обнаружение
Здесь самая недооценённая арифметика главы. Бюджет ошибок за 30-дневный месяц (43 200 минут):
| SLO | Доля бюджета | Бюджет в месяц |
|---|---|---|
| 99 % | 1 % | 432 минуты (7 ч 12 мин) |
| 99,5 % | 0,5 % | 216 минут |
| 99,9 % | 0,1 % | 43,2 минуты |
| 99,95 % | 0,05 % | 21,6 минуты |
| 99,99 % | 0,01 % | 4,32 минуты |
| 99,999 % | 0,001 % | 25,9 секунды |
Теперь разложим время реакции на инцидент — то, из чего складывается недоступность:
| Этап | Что определяет | Реалистично |
|---|---|---|
| Сбор метрики | интервал scrape | 15–60 с |
| Условие алерта | окно for, чтобы не шуметь |
2–5 мин |
| Доставка уведомления | группировка в Alertmanager, пуш | 30–90 с |
| Человек проснулся и открыл ноутбук | ночь, сон | 3–10 мин |
| Диагностика и митигация | качество телеметрии, наличие готовой ручки | 7–50 мин |
Сложим только то, что происходит до начала работы: 30 с + 3 мин + 1 мин + 5 мин ≈ 9,5 минуты, и это оптимистичный сценарий — быстрый сбор, короткое окно, дежурный проснулся за пять минут. Сопоставим с бюджетом:
- 99,9 % (43,2 мин/мес.): одно ночное срабатывание съедает 22 % месячного бюджета до того, как кто-то начал что-то делать. Три инцидента за месяц — и две трети бюджета потрачены на просыпание. Вывод: на этом уровне человек в петле ещё помещается, но запас крайне мал.
- 99,99 % (4,32 мин/мес.): обнаружение и просыпание вдвое превышают весь месячный бюджет. Это математический факт, а не вопрос дисциплины: четыре девятки недостижимы при участии человека в устранении. Требуется автоматическая митигация — автоматический откат по метрике, автоматический вывод инстанса из балансировки, переключение трафика без ручного решения.
- 99,999 % (26 секунд в месяц): дежурный не успеет прочитать уведомление. Это уровень, на котором любая деградация должна маскироваться самой системой, что означает избыточность в нескольких регионах, автоматический failover, тестирование этого failover’а и штат, который всё это поддерживает. Стоимость растёт не линейно: каждая девятка требует убрать следующий по значимости класс отказов, а он всегда дороже предыдущего.
Отсюда честный вывод про «пять девяток». Их обещают в презентациях, а платят за них единицы компаний, у которых цена минуты простоя действительно измеряется миллионами. Для остальных 99,9 % — это уже серьёзное обязательство, требующее дежурства, автоматики отката и работы над сокращением времени обнаружения. Разговор «а давайте 99,99 %» полезно переводить в счёт: покажите бюджет в минутах, покажите время просыпания, покажите, во что обойдётся его убрать. Обычно после этого обсуждение возвращается к тому, какой уровень нужен пользователю, а не какой красивее звучит. Экономика этого выбора — глава 03.
Что из этого следует для мониторинга конкретно. Время обнаружения — инженерный параметр, которым можно управлять, и он покупается за деньги: уменьшить интервал сбора с 60 до 15 секунд — ×4 к объёму данных; сократить окно for с 5 до 2 минут — рост числа ложных срабатываний, а с ним усталость дежурного (глава 05); ввести алерты по скорости прожигания бюджета (burn rate) — сложнее в настройке, зато реагируют на серьёзную аварию за минуты, а на мелкую утечку — за часы.
# Скорость прожигания бюджета: во сколько раз быстрее нормы тратим.
# Для SLO 99,9 % допустимая доля ошибок — 0.001. Значение 1 — тратим ровно
# по плану; 14,4 — весь месячный бюджет сгорит за двое суток.
(
sum(rate(http_requests_total{job="checkout", code=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="checkout"}[5m]))
) / 0.001
Что показывать: дашборды, которые открывают
Дашборд — интерфейс, у него есть пользователь и сценарий; смешивать сценарии — верный способ получить панель, на которую никто не смотрит.
- Уровень 1: состояние SLO. Один экран на весь продукт: для каждого пользовательского сценария — значение SLI, остаток бюджета ошибок, скорость прожигания. Открывается на приёмке смены и на еженедельном разборе. 5–10 чисел, ноль графиков «на всякий случай».
- Уровень 2: сервис. RED плюс насыщение ресурсов плюс зависимости, одинаковый по форме для всех сервисов и сгенерированный из шаблона. Открывается дежурным в первые две минуты инцидента, чтобы локализовать.
- Уровень 3: отладка. Всё остальное: живёт рядом с кодом сервиса, поддерживается командой-владельцем, может быть каким угодно.
Правило, отсекающее рост энтропии: у каждого дашборда есть владелец и дата последнего открытия (статистику просмотров умеет показывать большинство систем). Панель, которую не открывали 90 дней, не «на всякий случай» — она мусор: во время инцидента её всё равно не найдут, зато она увеличивает время поиска нужной.
Состояние «Сирота» — не теоретическое. В любой системе старше двух лет большая часть рядов и панелей находится именно в нём, и именно там лежит основной резерв экономии. Проблема в том, что переход «Сирота → Удалена» требует не технического решения, а согласия: кто-то должен взять на себя риск, что через месяц удалённое понадобится. Без явного правила («ревизия раз в квартал, порог 90 дней без обращений, восстановление по запросу») этот переход не происходит никогда.
Цена мониторинга
Цена складывается из трёх слагаемых, и обычно считают только первое. Деньги — хранение, трафик, лицензии, ресурсы под коллекторы и агенты. Модели тарификации у вендоров разные — за хост, за принятый гигабайт, за пользовательскую метрику, за индексированное поле, — и объединяет их одно свойство: счёт растёт вместе с трафиком сервиса, а не с полезностью данных. Порядок, к которому приходят команды, честно посчитавшие: телеметрия обходится в 10–30 % инфраструктурного счёта. Цифра большая, и её почти никогда не выносят в отдельную строку бюджета, из-за чего никто ей не управляет.
Люди. Мониторинг требует поддержки: обновления, миграции схем, разбор «почему пропали данные за вторник», подключение новых сервисов. В небольшой команде это половина ставки, размазанная по всем. Своя инсталляция дешевле по счёту от вендора и дороже по человеко-часам; точка перелома обычно там, где сумма от вендора превышает половину месячной зарплаты инженера.
Внимание. Каждый график и каждый ряд — то, во что кто-то будет всматриваться в три часа ночи; плотность полезного сигнала важнее полноты. То же и с алертами: сигнал, на который нет действия, обесценивает все остальные, потому что учит игнорировать их скопом (глава 05, человеческая цена дежурства — глава 06).
Где инженерное упирается в организационное. Инженер видит, что кардинальность выросла в пятьдесят раз, и знает, какую метку убрать. Он не может её убрать: метка нужна другой команде, счёт приходит третьей, решение принимает четвёртая. Технического решения у этой задачи нет. Работающие организационные — из тех, что не требуют героизма: выделенная строка бюджета на телеметрию с владельцем; квота на количество рядов на команду (превысил — сам решаешь, что убрать); ежеквартальная ревизия сирот с правом восстановления. Всё скучно, всё работает, и всё нужно вводить до того, как счёт вырастет вдвое, — после придётся резать в спешке и не то.
Мониторинг должен переживать то, что он мониторит
Четыре требования, каждое из которых написано чьей-то аварией.
- Мониторинг не живёт внутри наблюдаемой системы. Grafana в том же кластере Kubernetes, что и продакшн, — это Grafana, недоступная ровно тогда, когда нужна. То же про уведомления: если алерты идут через сервис, который лежит вместе с вами, они не придут.
- Отсутствие данных — состояние, требующее алерта, иначе полностью упавший сервис выглядит идеально здоровым: ошибок ноль, потому что запросов ноль. Плюс dead man’s switch — алерт, который обязан срабатывать всегда, и его молчание означает поломку самого мониторинга.
- Внешняя проба независимого происхождения — пусть примитивная, из другого облака или с чужого VPS.
- Деградация вместо отказа: агент, не сумевший отправить данные, обязан их отбросить, а не копить в памяти до OOM приложения (глава 10). А единственный способ узнать, что всё это работает в аварии, — устроить аварию (глава 12).
Что переносится от Google SRE, а что нет
Книга Site Reliability Engineering полезна, и глава про мониторинг распределённых систем в ней действительно хорошая. Но её написали люди, у которых была собственная TSDB (Borgmon, затем Monarch), выделенные команды на инструментарий и парк, где эффект масштаба меняет экономику. Полезно разделить, что из этого — идеи, а что — следствия ресурсов.
Переносится: симптомный подход (алертить на то, что чувствует пользователь, а не на внутренние причины) — работает и в команде из трёх человек; связь мониторинга с бюджетом ошибок (показатель, за которым не следует решение, не нужен); разделение «страница» / «тикет» / «просто данные» вместо одного уровня срочности; требование документированного ответа на вопрос «а что делать» для каждого алерта; четыре золотых сигнала как чеклист покрытия.
Не переносится: своя система хранения метрик — написание собственной TSDB почти всегда ошибка, готовых достаточно; хранение трейсов без сэмплирования — на масштабах Google это осмысленно из-за инструментов, у вас это просто счёт; отдельная команда SRE на сервис — у большинства читателей SRE это совмещаемая роль («SRE в небольшой команде»); инфраструктура, где перенос трафика между регионами рутинен, — если у вас один регион, рекомендации про наблюдение за глобальной балансировкой просто не о вас. А культ «делайте, как в книге» вреден ровно тем же, чем любой карго-культ: там описано решение задач конкретной компании конкретного размера, и переносить надо принципы и арифметику, а не список инструментов.
Практика: минимальный набор для команды из пяти человек
Порядок внедрения по убыванию отношения пользы к затратам:
- Blackbox-проба критического сценария извне плюс уведомление в мессенджер. Полчаса работы, закрывает случай «лежит всё».
- RED-метрики на каждый сервис: счётчик запросов с метками
route,method,codeи гистограмма длительности с бакетами под пороги SLO. - Структурированные логи с
trace_id, уровеньinfoв проде,debugвключается флагом на время. - Один дашборд SLO на весь продукт, по одному типовому на сервис из общего шаблона, аннотации деплоев на графиках.
- Трейсы — когда сервисов станет больше четырёх и вопрос «кто из них тормозит» перестанет решаться взглядом.
from prometheus_client import Counter, Histogram
# Метки только низкокардинальные. route — ШАБЛОН маршрута ("/orders/{id}"),
# а не конкретный путь ("/orders/8891"): иначе каждый заказ породит свой ряд.
REQUESTS = Counter(
"http_requests_total", "Всего обработанных HTTP-запросов",
["route", "method", "code"],
)
# Границы бакетов подобраны под пороги SLO: 0.3 (основной) и 1.0 (жёсткий).
# Обе обязаны присутствовать явно, иначе долю быстрее порога придётся
# оценивать интерполяцией — то есть неточно.
LATENCY = Histogram(
"http_request_duration_seconds", "Длительность обработки запроса",
["route", "method"],
buckets=(0.02, 0.05, 0.1, 0.2, 0.3, 0.5, 0.8, 1.0, 2.0, 5.0),
)
async def metrics_middleware(request, handler):
"""Одно место, где инструментируются все ручки сервиса."""
route = request.match_info.route.resource.canonical # шаблон, не сырой путь
with LATENCY.labels(route=route, method=request.method).time():
try:
response = await handler(request)
code = response.status
except Exception:
code = 500
raise
finally:
# код ответа приводим к классу: 2xx/3xx/4xx/5xx вместо 43 значений
REQUESTS.labels(route=route, method=request.method,
code=f"{code // 100}xx").inc()
return response
Две детали здесь важнее остального. Первая — шаблон маршрута вместо сырого пути: /orders/8891 в метке означает столько рядов, сколько было заказов, то есть неограниченный рост, и это самая частая причина взрыва кардинальности на практике. Вторая — схлопывание кода ответа в класс: различать 200 и 204 в метрике почти никогда не нужно, а рядов это удваивает; если конкретный код понадобится, он есть в логе. Стоимость такой инструментации: 12 сервисов × 25 маршрутов × 2 метода × 4 класса кода = 2 400 рядов на счётчик и 12 × 25 × 2 × 12 = 7 200 на гистограмму — меньше 10 000 рядов, около 40 МБ памяти. Практически бесплатно, и при этом достаточно, чтобы считать SLO по доступности и по задержке.
Типичные ошибки
- Метка с неограниченным множеством значений:
user_id,order_id, сырой путь, текст ошибки, IP-адрес. Первая причина неработающего мониторинга. - Усреднение перцентилей по инстансам или по времени: ошибка в разы в обе стороны.
- Бакеты гистограммы по умолчанию при том, что пороги SLO лежат между ними: проверить SLO становится невозможно.
- Алерты на причины вместо симптомов: «загрузка CPU 90 %» будит человека, когда пользователям хорошо, и молчит, когда им плохо по другой причине.
- Задержка успешных и неуспешных запросов в одной гистограмме. Быстрые ошибки улучшают график ровно в момент аварии.
- Мониторинг внутри наблюдаемой системы плюс отсутствие алерта на отсутствие данных: система отказывает синхронно с наблюдаемой, а мёртвый сервис выглядит здоровым.
- Дашборды и метрики без владельца. Растут неограниченно, во время инцидента мешают искать нужное.
- Head sampling трейсов при разборе единичных жалоб (шанс менее 40 %) и измерение только на сервере (систематически завышенная доступность).
Мини-итог
- Мониторинг решает две разные задачи — «сейчас плохо?» и «почему плохо?». Первая требует простоты и надёжности, вторая — богатства и глубины; одним инструментом обе не закрываются.
- Метрики отвечают на вопросы, известные заранее; логи и трейсы — на неизвестные. Низкокардинальные разрезы в метрики, идентификаторы — в логи и трейсы: одна метка на 50 000 значений превращает три тысячи рядов в сотню миллиардов.
- Перцентили не складываются и не усредняются: складываются бакеты, квантиль считается один раз от суммы. И сам квантиль по гистограмме — утверждение про бакет, а не число, поэтому границы подбираются под пороги SLO.
- Head sampling трейсов даёт статистику и не даёт ответа по конкретной жалобе: 1 % и 50 запросов клиента — меньше 40 % шанса. Tail sampling дороже в установке и решает эту задачу.
- Обнаружение и просыпание съедают около 9–10 минут. При SLO 99,9 % (43 минуты в месяц) это четверть бюджета на инцидент; при 99,99 % (4,3 минуты) человек в петле не помещается математически. Пять девяток — про полную автоматику и другой класс затрат, а не про старательность.
- Серверные метрики систематически оптимистичны: разрыв с клиентской реальностью бывает в разы и закрывается RUM и внешними пробами.
- Телеметрия стоит 10–30 % инфраструктурного счёта. Без выделенного владельца этой строкой никто не управляет, а большая часть объёма приходится на данные, которые никто не читает.
- Мониторинг обязан переживать наблюдаемую систему: отдельная площадка, алерт на отсутствие данных, dead man’s switch, внешняя проба.
Источники
- Google SRE Book, гл. 6 «Monitoring Distributed Systems» — https://sre.google/sre-book/monitoring-distributed-systems/ ; SRE Workbook, «Implementing SLOs» (в том числе алерты по burn rate) — https://sre.google/workbook/implementing-slos/
- Brendan Gregg, «The USE Method» — https://www.brendangregg.com/usemethod.html ; Tom Wilkie, «The RED Method» — https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/
- Prometheus: именование метрик, гистограммы и квантили,
histogram_quantile, устройство хранилища - OpenTelemetry: сигналы, сэмплирование, семантические соглашения, tail sampling processor; W3C Trace Context — https://www.w3.org/TR/trace-context/
- Charity Majors, Liz Fong-Jones, George Miranda, «Observability Engineering» (O’Reilly, 2022) — https://www.oreilly.com/library/view/observability-engineering/9781492076438/
- Cindy Sridharan, «Distributed Systems Observability» (O’Reilly, 2018) — https://www.oreilly.com/library/view/distributed-systems-observability/9781492033431/
- Gil Tene, «How NOT to Measure Latency» — https://www.youtube.com/watch?v=lJ8ydIuPFeU ; HdrHistogram — https://github.com/HdrHistogram/HdrHistogram
Смежные главы портала: «Наблюдаемость в распределённых системах» — про контекст и причинность подробнее; «Наблюдаемость и дежурства» — со стороны эксплуатации; «Наблюдаемость и производительность в Linux» — уровень хоста и ядра; «Временные ряды и поиск» — как устроены хранилища под метрики и логи; «Дежурства и инциденты» — те же вопросы со стороны руководителя.
Что дальше
Мы научились собирать данные и оценили, во что это обходится. Данные сами по себе никого не будят — для этого нужны правила, устроенные хитрее, чем «порог превышен»: порог на причину вместо симптома будит человека зря, окно в пять минут защищает от шума, но съедает четверть бюджета ошибок, а алерт без описанного действия учит игнорировать все алерты сразу.