2026 г.

Наблюдаемость: метрики, логи, трассировки

Проект ЦИТадель

Мониторинг отвечает на заранее сформулированные вопросы: доступен ли сервис, достаточно ли места на диске, не превышена ли допустимая доля ошибок. Но в распределённой системе приходится исследовать и неожиданные сочетания условий: почему платёж задерживается только у части пользователей, в одном регионе и после определённого обновления? Наблюдаемость характеризует, насколько хорошо внутреннее состояние системы можно понять по доступным внешним данным. Мониторинг и наблюдаемость не сменяют друг друга: первый обнаруживает известные нежелательные состояния, вторая помогает исследовать как известные, так и новые отказы. Оба свойства требуют заранее спроектированной телеметрии. Ни установка агента, ни покупка хранилища сами по себе её не создают.

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

1. Три сигнала и их разделение труда

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

  • Метрики — числовые ряды во времени: число запросов, распределение задержки, объём памяти. Отдельные события обычно агрегируются до записи, поэтому стоимость зависит прежде всего от числа временных рядов и частоты сбора, а не прямо от числа запросов. Метрики удобно агрегировать, изображать на графиках и использовать в алёртах, но они обычно не сохраняют сведения о конкретном запросе.
  • Логи — записи о событиях с полями и текстовым сообщением. Они могут содержать подробный контекст, но объём при подробном журналировании растёт вместе с потоком событий. Цена зависит от сбора, индексации, срока хранения и запросов, поэтому состав и жизненный цикл логов нужно проектировать.
  • Трассировки описывают выполнение отдельных операций как набор участков — span. Родительские связи и ссылки соединяют работу процессов и сервисов, а времена участков позволяют найти источник задержки. Трасса сохраняет контекст конкретной операции, но при выборке видна лишь часть общего потока.

Полезная, но не строгая мнемоника: метрики показывают, что изменилось; трассировки помогают локализовать изменение; логи дают подробности. Любой из сигналов иногда отвечает сразу на несколько вопросов, а расследование не обязано идти в одном порядке. Ценность возникает, когда сигналы можно сопоставить по времени, сервису, версии и идентификатору трассы.

2. Метрики: типы и кардинальность

Рассмотрим модель Prometheus. Обычно приложение или экспортёр публикует значения по HTTP на адресе /metrics, а Prometheus периодически опрашивает цель. Автоматическая метрика up показывает, удался ли последний сбор; она подтверждает доступность точки сбора, но не работоспособность всех функций сервиса. В модели Prometheus используются четыре основных типа метрик:

  • счётчик (counter) накапливает число событий или объём: обслуженные запросы, ошибки, переданные байты. После перезапуска процесса он может сброситься. Функция rate() учитывает такие сбросы и оценивает среднюю скорость роста за выбранное окно;
  • уровень (gauge) отражает текущее значение и может меняться в обе стороны: число работающих задач, глубина очереди, температура;
  • гистограмма считает наблюдения по корзинам, а также хранит их число и сумму. Классическая гистограмма позволяет агрегировать корзины нескольких экземпляров и приближённо вычислять перцентили функцией histogram_quantile(). Точность зависит от выбранных границ корзин. Нативная гистограмма хранит распределение компактнее и поддерживается не всеми библиотеками и хранилищами одинаково;
  • сводка (summary) также хранит число и сумму, а некоторые клиентские библиотеки вычисляют заданные квантили в приложении. Такие квантили относятся к заранее выбранному окну и обычно не поддаются корректному объединению между экземплярами.

Среднее значение не заменяет распределение. Если 99 запросов заняли по 10 мс, а один — 5 с, среднее составит около 60 мс и скроет медленный хвост. Перцентили показывают этот хвост, но их нужно интерпретировать вместе с объёмом выборки и окном: p99 по десяти запросам неустойчив, а p99 за сутки может скрыть краткий инцидент. Границы корзин следует выбирать с учётом SLO и характерных задержек.

Метрики снабжаются метками (labels): методом, кодом ответа, экземпляром, регионом. Каждая уникальная комбинация имени метрики и значений меток образует отдельный временной ряд. Поэтому идентификатор пользователя или запроса, адрес с параметрами и текст ошибки создают неограниченную кардинальность. Число рядов, память и стоимость запросов могут быстро вырасти. В метриках оставляют ограниченные и полезные для агрегирования измерения: например, шаблон маршрута /users/{id}, но не фактический путь /users/1848293. Индивидуальный контекст сохраняют в логах и трассировках.

3. SLI, SLO и бюджет ошибок: зачем всё это меряется

Метрика становится основанием для решения, когда понятно, какое качество она отражает и какое значение считается приемлемым. В SRE для этого используют три связанных понятия:

  • SLI (service level indicator) — измеренная доля хороших событий или другое измерение качества с точки зрения пользователя: доля успешных запросов, доля ответов быстрее 300 мс, свежесть обработанных данных. Загрузка CPU может помогать в диагностике, но обычно не является пользовательским SLI;
  • SLO (service level objective) — внутренняя цель для SLI на заданном окне: например, не менее 99,9% успешных запросов за последние 30 дней. SLO выбирают по потребностям пользователей и продукта. Его не следует смешивать с SLA — внешним соглашением, которое может включать иные показатели и последствия нарушения;
  • бюджет ошибок — допустимая доля плохих событий, равная 1 − SLO. При SLO 99,9% бюджет равен 0,1%. Для SLI по запросам это 0,1% запросов, а не минуты простоя. Лишь для непрерывно измеряемой доступности 0,1% от 30 суток соответствует 43,2 минуты.

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

Алёрт, который будит дежурного, должен сообщать о значимом симптоме и требовать понятного действия. Высокая загрузка CPU сама по себе может быть нормальной, тогда как быстрый расход бюджета ошибок показывает ущерб пользовательскому качеству. Причинные сигналы уместны в срочном алёрте, если надёжно предсказывают близкий отказ и для них существует немедленное действие; иначе их оставляют на дашборде или превращают в несрочную задачу. Для SLO применяют скорость сгорания бюджета (burn rate): значение 1 израсходует весь бюджет ровно за окно SLO, значение 10 — в десять раз быстрее. Сочетание нескольких скоростей и пар длинного и короткого окон позволяет быстро заметить тяжёлый отказ и не пропустить более медленную деградацию.

4. Логи: дисциплина против энтропии

Для логов важна не только полнота, но и предсказуемая схема. Запись обычно содержит время с часовым поясом, уровень, имя и версию сервиса, устойчивое имя события, сообщение и предметные поля. Формат должен машинно разбираться; JSON удобен, но не обязателен, если сборщик надёжно понимает другой формат. Текст полезен человеку, а отдельные типизированные поля позволяют искать и агрегировать без регулярных выражений.

Контекст трассировки связывает сигналы. Если у операции есть trace_id и span_id, их добавляют в относящиеся к ней записи. Отдельный request_id нужен лишь при самостоятельной семантике, например как публичный идентификатор обращения. Идентификаторы не заменяют поля service.name, event.name и код результата: без них нельзя найти похожие события во всех запросах.

Стандартный вывод контейнера — только источник данных. Агент на узле или другой сборщик должен прочитать поток до исчезновения контейнера, дополнить его метаданными и передать в хранилище. Срок хранения, индексируемые поля и выборка определяются назначением данных. Аудитные события нельзя выборочно отбрасывать как обычные диагностические логи. Пароли, ключи доступа, содержимое сессий и персональные данные не должны попадать в телеметрию; редактирование следует выполнять как можно ближе к источнику.

5. Трассировки: причинность, ставшая инструментом

При входе в систему создаётся идентификатор трассы, а отдельная операция внутри неё представляется span: у него есть имя, начало, длительность, статус, атрибуты и идентификатор родителя. Для HTTP стандарт W3C Trace Context задаёт заголовки traceparent и tracestate. В gRPC контекст передают в метаданных, а в очередях — в предусмотренных форматом полях сообщения. Обычные вызовы образуют дерево родительских span-ов; асинхронные и пакетные операции могут связываться ссылками, поэтому общая структура бывает графом.

Связь с причинностью из курса распределённых систем здесь прямая, но трассировка — не реализация часов Лэмпорта. Родительский контекст фиксирует известную причинную связь, тогда как времена начала и конца обычно получены от физических часов разных узлов. Пропущенное инструментирование, выборка и рассинхронизация часов делают картину неполной. По трассе нельзя восстановить все отношения «произошло раньше» или доказать отсутствие события.

OpenTelemetry предоставляет общие API и SDK для инструментирования, семантические соглашения, протокол OTLP и Collector. Collector строит конвейеры из приёмников, обработчиков и экспортёров: например, принимает OTLP, удаляет чувствительные атрибуты и направляет трассы и метрики в разные системы. OpenTelemetry не является хранилищем или интерфейсом анализа и не делает все серверные системы взаимозаменяемыми; оно отделяет приложение от конкретного способа доставки телеметрии.

Объём трасс регулируют выборкой (sampling). При головной выборке решение принимается в начале трассы и распространяется вместе с контекстом: это просто и дёшево, но редкая ошибка может не попасть в выборку. Хвостовая выборка принимает решение после получения span-ов и может предпочесть ошибки и медленные операции, однако Collector должен временно хранить данные и правильно собирать span-ы одной трассы на одном решающем узле. Даже она не восстанавливает span, который не был создан или доставлен.

6. Инструментирование: наблюдаемость — свойство кода

Автоматическое инструментирование подключается без изменения исходного кода или с минимальным изменением запуска. Оно перехватывает поддерживаемые библиотеки и хорошо видит входящие запросы, исходящие вызовы и обращения к базам. При этом всё равно нужны совместимые версии библиотек, конфигурация экспортёра, имя сервиса и проверка результата. Автоматика не знает, что означает предметная операция и какой результат пользователь считает успешным.

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

Состав первого дашборда помогают определить две мнемоники. RED для обслуживающей системы означает поток операций (rate), ошибки и распределение длительности. USE для ресурса — использование (utilization), насыщение (saturation) и ошибки. Они задают начальные вопросы, но не заменяют пользовательские SLI и особенности конкретной системы.

7. Методика: как этим пользоваться в три часа ночи

Ни один порядок не подходит всем инцидентам, но следующая последовательность даёт полезную отправную точку:

  1. Определить симптом и границы. По SLI и обзорным метрикам выяснить начало, масштаб и затронутые операции, регионы и версии. События развёртывания и изменения конфигурации помогают сформулировать гипотезу, но совпадение по времени ещё не доказывает причину.
  2. Найти место. Выбрать ошибочные или медленные трассы из того же окна. Если хранилище метрик поддерживает exemplars, конкретное наблюдение в корзине гистограммы может сразу вести к трассе. Сравнить несколько трасс: один пример может быть исключением.
  3. Проверить причину. По trace_id и времени найти структурные логи нужных сервисов, затем сопоставить их с ресурсными метриками. Сообщения «пул исчерпан» или «истёк тайм-аут» часто описывают ближайшее следствие, поэтому гипотезу нужно проверить изменением, воспроизведением либо исчезновением симптома после отката.

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

8. Практикум: стенд наблюдаемости за вечер

Выполняйте практикум на отдельной машине с Docker Compose. Зафиксируйте версии образов и пакетов, не публикуйте интерфейсы стенда во внешнюю сеть и не используйте в тестах производственные данные. Понадобятся Prometheus, node_exporter, Grafana, OpenTelemetry Collector Contrib, Jaeger в однопроцессной конфигурации и два небольших Python-сервиса: frontend вызывает backend. Минимальный маршрут данных:

Python-сервисы --OTLP--> OpenTelemetry Collector --OTLP-трассы--> Jaeger
                                      |--точка /metrics <-- Prometheus --> Grafana
                                      `--debug-логи----> стандартный вывод
node_exporter --------точка /metrics <-- Prometheus

Экспортёр debug лишь печатает записи в лог Collector и годится для опыта, но не заменяет хранилище логов. Это ограничение стенда нужно явно указать в отчёте.

Задание 1. Метрики хоста и PromQL. Поднимите Prometheus, node_exporter и Grafana. Если node_exporter работает в контейнере, подключите пространства PID и необходимые каталоги хоста в режиме только для чтения согласно его документации; иначе вы можете измерять контейнер вместо хоста. Проверьте цель по метрикам up и scrape_duration_seconds. Загрузку CPU экземпляра оцените запросом:

100 * (1 - avg by (instance) (
  rate(node_cpu_seconds_total{mode="idle"}[5m])
))

Для дисков начните с node_filesystem_avail_bytes и node_filesystem_size_bytes, исключив псевдофайловые системы и выбрав нужные точки монтирования. Постройте USE-дашборд и для каждого графика запишите единицу измерения и вопрос, на который он отвечает.

Задание 2. Конвейер и автоматическое инструментирование. Настройте в Collector приёмник OTLP, обработчик batch, экспортёр prometheus с отдельной точкой сбора, OTLP-экспортёр трасс в Jaeger и экспортёр debug для логов. Скопируйте ресурсный атрибут service.name в ограниченную метку метрики service, чтобы запросы не смешивали два приложения. В Python-контейнерах установите opentelemetry-distro и opentelemetry-exporter-otlp, после установки библиотек приложения выполните opentelemetry-bootstrap -a install. Запускайте оба процесса через opentelemetry-instrument, задав разные OTEL_SERVICE_NAME, адрес Collector и значения otlp для OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER и OTEL_LOGS_EXPORTER. Убедитесь, что Collector получает данные обоих сервисов, а не только собственные метрики.

Нагрузите frontend утилитой hey, wrk или коротким циклом клиента. Найдите фактическое имя гистограммы длительности HTTP-запросов: преобразование OTLP в формат Prometheus может изменить точки в именах и добавить суффикс единицы. Для классической гистограммы запрос p95 имеет вид:

histogram_quantile(0.95,
  sum by (le) (
    rate(http_server_request_duration_seconds_bucket{service="frontend"}[5m])
  )
)

Подставьте имя своей метрики и сохраните остальные метки, если сравниваете маршруты или сервисы. Постройте p50, p95 и p99 рядом с числом запросов. Объясните, почему квантили нельзя усреднять между экземплярами и как границы корзин ограничивают точность.

Задание 3. Трассы и корреляция логов. Направьте трассы из Collector в Jaeger по OTLP. Выполните один запрос и найдите span frontend, его клиентский вызов и серверный span backend. Проверьте, что идентификатор трассы один, а родительские идентификаторы различаются. Добавьте вручную span вокруг одного этапа бизнес-логики. Затем создайте структурную запись внутри этого span и убедитесь, что в выводе debug присутствуют trace_id и span_id. Не помещайте идентификатор запроса в метки метрик.

Задание 4. Расследование вслепую. Добавьте в backend управляемую тестовым флагом задержку 400 мс и запись с устойчивым именем события. Один участник включает флаг, другой не смотрит конфигурацию. Исследователь должен: определить окно и затронутый маршрут по гистограмме; сравнить несколько медленных трасс; найти задержанный span; по trace_id найти лог; отключить флаг и проверить исчезновение симптома. Запишите использованные запросы и время расследования. Повторите без доступа к трассам и сравните не только скорость, но и надёжность вывода.

Задание 5. SLO и скорость сгорания. Сформулируйте учебный SLO: 99% запросов frontend быстрее 300 мс. Настройте в гистограмме границу 0,3 с. Для пятиминутного окна скорость сгорания вычисляется так:

(
  1 -
  sum(rate(http_server_request_duration_seconds_bucket{
    service="frontend", le="0.3"
  }[5m]))
  /
  sum(rate(http_server_request_duration_seconds_count{
    service="frontend"
  }[5m]))
) / 0.01

Подставьте фактическое имя метрики. Рассчитайте тот же показатель для 30 минут, создайте алёрт, который требует превышения выбранного порога на обоих окнах, и вызовите его задержкой из задания 4. Предусмотрите отсутствие трафика, чтобы деление на ноль не выдавало ложный результат. Короткие окна и порог предназначены только для лаборатории; для 30-дневного SLO параметры выводят из допустимой доли расхода бюджета и проверяют на исторических данных.

Задание 6*. Контролируемый опыт с кардинальностью. Только в изолированном стенде добавьте к отдельному тестовому счётчику метку с уникальным идентификатором и создайте не более тысячи значений. Сравните число рядов этой метрики, prometheus_tsdb_head_series и память до и после опыта. Некоторые OpenTelemetry SDK ограничивают кардинальность и объединяют лишние сочетания; если сработала такая защита, найдите атрибут otel.metric.overflow=true и объясните потерю детализации. Удалите метку и перезапустите стенд с чистым учебным хранилищем. Такой идентификатор нельзя переносить в производственную метрику.

Итоги

  • Мониторинг проверяет известные условия, а наблюдаемость позволяет исследовать состояние системы; оба требуют спроектированной телеметрии.
  • Метрики, логи и трассировки дополняют друг друга, но формула «что — где — почему» остаётся мнемоникой, а не обязательным порядком расследования.
  • Счётчики анализируют через скорость роста, распределения — через гистограммы и перцентили. Каждая комбинация меток создаёт ряд, поэтому неограниченные идентификаторы метрикам противопоказаны.
  • SLI измеряет пользовательское качество, SLO задаёт цель на окне, бюджет ошибок равен допустимой доле плохих событий. Для срочных SLO-алёртов используют скорость сгорания на нескольких окнах.
  • Структурные логи содержат устойчивые поля и контекст трассы, но не секреты. Сборщик должен забрать вывод эфемерного контейнера и применить правила обработки и хранения.
  • Трасса связывает span-ы через распространяемый контекст, но остаётся неполным наблюдением и не заменяет логические часы. OpenTelemetry создаёт и переносит телеметрию, а не хранит и не анализирует её.
  • Расследование начинается с границ симптома, переходит к нескольким трассам и логам и заканчивается проверкой причинной гипотезы.

Литература

  1. B. Beyer et al. (ред.), "Site Reliability Engineering," O'Reilly, 2016; A. Murphy et al., "The Site Reliability Workbook," O'Reilly, 2018. Электронные версии и дополнительные материалы Google SRE, в том числе глава Alerting on SLOs.
  2. Документация OpenTelemetry: основные понятия, архитектура Collector, экспортёр Prometheus и автоматическое инструментирование Python.
  3. Документация Prometheus: модель данных, гистограммы и сводки, имена и метки.
  4. W3C Trace Context — формат и правила передачи контекста трассы; Jaeger: Getting Started — однопроцессный стенд и приём OTLP.
  5. C. Majors, L. Fong-Jones, G. Miranda, "Observability Engineering," O'Reilly, 2022.
  6. Материалы сайта: «Надёжность поверх ненадёжной сети», курс «Распределённые системы» — глава 2 о причинности и глава 11 о проверке и метриках восстановления, «Облака на практике».
404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией