Проект ЦИТадель
Мониторинг отвечает на заранее сформулированные вопросы: доступен ли сервис, достаточно ли места на диске, не превышена ли допустимая доля ошибок. Но в распределённой системе приходится исследовать и неожиданные сочетания условий: почему платёж задерживается только у части пользователей, в одном регионе и после определённого обновления? Наблюдаемость характеризует, насколько хорошо внутреннее состояние системы можно понять по доступным внешним данным. Мониторинг и наблюдаемость не сменяют друг друга: первый обнаруживает известные нежелательные состояния, вторая помогает исследовать как известные, так и новые отказы. Оба свойства требуют заранее спроектированной телеметрии. Ни установка агента, ни покупка хранилища сами по себе её не создают.
Разберём три распространённых сигнала — метрики, логи и трассировки, — затем свяжем их с SLO, алёртами и расследованием инцидентов. В практикуме соберём минимальный стенд в контейнерах и пройдём путь от симптома до причины.
Метрики, логи и трассировки различаются моделью данных, стоимостью и удобными способами поиска. Это не исчерпывающая классификация: события развёртывания, профили выполнения и результаты синтетических проверок тоже могут быть важны. Однако три основных сигнала хорошо показывают разделение задач:
Полезная, но не строгая мнемоника: метрики показывают, что изменилось; трассировки помогают локализовать изменение; логи дают подробности. Любой из сигналов иногда отвечает сразу на несколько вопросов, а расследование не обязано идти в одном порядке. Ценность возникает, когда сигналы можно сопоставить по времени, сервису, версии и идентификатору трассы.
Рассмотрим модель Prometheus. Обычно приложение или экспортёр публикует значения по HTTP на адресе /metrics, а Prometheus периодически опрашивает цель. Автоматическая метрика up показывает, удался ли последний сбор; она подтверждает доступность точки сбора, но не работоспособность всех функций сервиса. В модели Prometheus используются четыре основных типа метрик:
rate() учитывает такие сбросы и оценивает среднюю скорость роста за выбранное окно;histogram_quantile(). Точность зависит от выбранных границ корзин. Нативная гистограмма хранит распределение компактнее и поддерживается не всеми библиотеками и хранилищами одинаково;Среднее значение не заменяет распределение. Если 99 запросов заняли по 10 мс, а один — 5 с, среднее составит около 60 мс и скроет медленный хвост. Перцентили показывают этот хвост, но их нужно интерпретировать вместе с объёмом выборки и окном: p99 по десяти запросам неустойчив, а p99 за сутки может скрыть краткий инцидент. Границы корзин следует выбирать с учётом SLO и характерных задержек.
Метрики снабжаются метками (labels): методом, кодом ответа, экземпляром, регионом. Каждая уникальная комбинация имени метрики и значений меток образует отдельный временной ряд. Поэтому идентификатор пользователя или запроса, адрес с параметрами и текст ошибки создают неограниченную кардинальность. Число рядов, память и стоимость запросов могут быстро вырасти. В метриках оставляют ограниченные и полезные для агрегирования измерения: например, шаблон маршрута /users/{id}, но не фактический путь /users/1848293. Индивидуальный контекст сохраняют в логах и трассировках.
Метрика становится основанием для решения, когда понятно, какое качество она отражает и какое значение считается приемлемым. В SRE для этого используют три связанных понятия:
1 − SLO. При SLO 99,9% бюджет равен 0,1%. Для SLI по запросам это 0,1% запросов, а не минуты простоя. Лишь для непрерывно измеряемой доступности 0,1% от 30 суток соответствует 43,2 минуты.Бюджет связывает надёжность со скоростью изменений, но сам не предписывает организационную реакцию. Команда заранее определяет политику: когда ограничить рискованные развёртывания, когда направить время на устранение системных причин и какие исключения допустимы. Деградация критической зависимости не обязательно ограничивает SLO системы, если предусмотрены кэш, резервный путь или корректное упрощение функции; для обязательной синхронной зависимости такое ограничение действительно возникает.
Алёрт, который будит дежурного, должен сообщать о значимом симптоме и требовать понятного действия. Высокая загрузка CPU сама по себе может быть нормальной, тогда как быстрый расход бюджета ошибок показывает ущерб пользовательскому качеству. Причинные сигналы уместны в срочном алёрте, если надёжно предсказывают близкий отказ и для них существует немедленное действие; иначе их оставляют на дашборде или превращают в несрочную задачу. Для SLO применяют скорость сгорания бюджета (burn rate): значение 1 израсходует весь бюджет ровно за окно SLO, значение 10 — в десять раз быстрее. Сочетание нескольких скоростей и пар длинного и короткого окон позволяет быстро заметить тяжёлый отказ и не пропустить более медленную деградацию.
Для логов важна не только полнота, но и предсказуемая схема. Запись обычно содержит время с часовым поясом, уровень, имя и версию сервиса, устойчивое имя события, сообщение и предметные поля. Формат должен машинно разбираться; JSON удобен, но не обязателен, если сборщик надёжно понимает другой формат. Текст полезен человеку, а отдельные типизированные поля позволяют искать и агрегировать без регулярных выражений.
Контекст трассировки связывает сигналы. Если у операции есть trace_id и span_id, их добавляют в относящиеся к ней записи. Отдельный request_id нужен лишь при самостоятельной семантике, например как публичный идентификатор обращения. Идентификаторы не заменяют поля service.name, event.name и код результата: без них нельзя найти похожие события во всех запросах.
Стандартный вывод контейнера — только источник данных. Агент на узле или другой сборщик должен прочитать поток до исчезновения контейнера, дополнить его метаданными и передать в хранилище. Срок хранения, индексируемые поля и выборка определяются назначением данных. Аудитные события нельзя выборочно отбрасывать как обычные диагностические логи. Пароли, ключи доступа, содержимое сессий и персональные данные не должны попадать в телеметрию; редактирование следует выполнять как можно ближе к источнику.
При входе в систему создаётся идентификатор трассы, а отдельная операция внутри неё представляется span: у него есть имя, начало, длительность, статус, атрибуты и идентификатор родителя. Для HTTP стандарт W3C Trace Context задаёт заголовки traceparent и tracestate. В gRPC контекст передают в метаданных, а в очередях — в предусмотренных форматом полях сообщения. Обычные вызовы образуют дерево родительских span-ов; асинхронные и пакетные операции могут связываться ссылками, поэтому общая структура бывает графом.
Связь с причинностью из курса распределённых систем здесь прямая, но трассировка — не реализация часов Лэмпорта. Родительский контекст фиксирует известную причинную связь, тогда как времена начала и конца обычно получены от физических часов разных узлов. Пропущенное инструментирование, выборка и рассинхронизация часов делают картину неполной. По трассе нельзя восстановить все отношения «произошло раньше» или доказать отсутствие события.
OpenTelemetry предоставляет общие API и SDK для инструментирования, семантические соглашения, протокол OTLP и Collector. Collector строит конвейеры из приёмников, обработчиков и экспортёров: например, принимает OTLP, удаляет чувствительные атрибуты и направляет трассы и метрики в разные системы. OpenTelemetry не является хранилищем или интерфейсом анализа и не делает все серверные системы взаимозаменяемыми; оно отделяет приложение от конкретного способа доставки телеметрии.
Объём трасс регулируют выборкой (sampling). При головной выборке решение принимается в начале трассы и распространяется вместе с контекстом: это просто и дёшево, но редкая ошибка может не попасть в выборку. Хвостовая выборка принимает решение после получения span-ов и может предпочесть ошибки и медленные операции, однако Collector должен временно хранить данные и правильно собирать span-ы одной трассы на одном решающем узле. Даже она не восстанавливает span, который не был создан или доставлен.
Автоматическое инструментирование подключается без изменения исходного кода или с минимальным изменением запуска. Оно перехватывает поддерживаемые библиотеки и хорошо видит входящие запросы, исходящие вызовы и обращения к базам. При этом всё равно нужны совместимые версии библиотек, конфигурация экспортёра, имя сервиса и проверка результата. Автоматика не знает, что означает предметная операция и какой результат пользователь считает успешным.
Ручное инструментирование добавляет бизнес-события, внутренние этапы и атрибуты предметной области. Эти данные образуют интерфейс эксплуатации: им нужны устойчивые имена, единицы измерения, ограниченная кардинальность и тесты. Изменение имени метрики или атрибута способно сломать алёрт так же, как изменение программного API — клиента. Для персональных данных и секретов действует принцип минимизации независимо от выбранного сигнала.
Состав первого дашборда помогают определить две мнемоники. RED для обслуживающей системы означает поток операций (rate), ошибки и распределение длительности. USE для ресурса — использование (utilization), насыщение (saturation) и ошибки. Они задают начальные вопросы, но не заменяют пользовательские SLI и особенности конкретной системы.
Ни один порядок не подходит всем инцидентам, но следующая последовательность даёт полезную отправную точку:
trace_id и времени найти структурные логи нужных сервисов, затем сопоставить их с ресурсными метриками. Сообщения «пул исчерпан» или «истёк тайм-аут» часто описывают ближайшее следствие, поэтому гипотезу нужно проверить изменением, воспроизведением либо исчезновением симптома после отката.Дашборд должен отвечать на определённый вопрос и иметь владельца. Обычно нужны обзор качества системы, представления по сервисам или путям запроса и ресурсные экраны для диагностики. Их точное число зависит от системы: произвольное ограничение бесполезно, но графики, которые никто не использует и не умеет интерпретировать, следует удалить или переработать. Во время инцидента важно также записывать запросы, временные диапазоны и доказательства, чтобы разбор не зависел от памяти участников.
Выполняйте практикум на отдельной машине с 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 и объясните потерю детализации. Удалите метку и перезапустите стенд с чистым учебным хранилищем. Такой идентификатор нельзя переносить в производственную метрику.