Сегодня хочется обсудить такую тему, как “Наблюдаемость распределенных систем”. На эту тему уже сказано много. В данной статье не открою Америку. Но хотел бы ее рассмотреть с практической стороны. Статья предназначена для новичков.
Немного теории
Если вы знакомы с понятием мониторинга, то наблюдаемость отличается от него тем, что она отвечает не только на вопрос "работает ли система?", но и на более глубокий вопрос "почему система ведёт себя именно так?". Представьте, что мониторинг - это как индикаторы на приборной панели автомобиля, а наблюдаемость - это возможность заглянуть под капот и понять, что именно происходит с двигателем в любой момент времени.
В веб-разработке наблюдаемость строится на трех основных столпах, которые называют "три кита" observability.
Первый столп — это логи (logs). Логи представляют собой записи отдельных событий, которые происходят в вашем приложении: пользователь авторизовался, произошла ошибка при обращении к базе данных, отправился email. Логи дают вам нарратив, историю того, что происходило. У логов есть разные уровни логирования от информационных до критических ошибок.
Второй столп — метрики (metrics). Это агрегированные числовые данные, которые собираются и усредняются за определенный период времени. Например, сколько запросов в секунду обрабатывает ваш сервер, какой средний процент использования памяти, сколько времени в среднем занимает выполнение SQL-запросов. Метрики позволяют видеть тренды и аномалии на графиках.
Третий столп — трассировки или traces. Это особенно важно для современных распределённых систем. Трассировка показывает путь одного запроса через всю вашу систему: как он пришёл с фронтенд, затем на API-сервер, потом к базе данных, может быть к внешнему сервису, и обратно. Каждый этап этого путешествия называется span (промежуток), и вместе они образуют полную картину обработки запроса.
Популярные инструменты, которые помогают строить наблюдаемость: для логов часто используют ELK Stack (Elasticsearch, Logstash, Kibana). Для метрик - Prometheus с Grafana для визуализации. Для трассировок - Jaeger или Zipkin. Существуют также комплексные коммерческие решения вроде Datadog, New Relic, или Sentry для отслеживания ошибок.
Ключевая идея заключается в том, что мы создаем уникальный идентификатор для всего запроса (traceId). Этот ID будет одинаковым на всех этапах обработки, даже если они происходят на разных серверах. Это позволяет нам "склеить" все части одного запроса вместе.
Каждый этап работы мы оборачиваем в span — это как глава в книге. У каждого span'а есть своё имя, время начала, время окончания, и он может хранить дополнительную информацию в metadata. Очень важно, что span'ы могут быть вложенными — для этого мы храним parentSpanId, который указывает на родительский span. Это создает иерархию, дерево операций.
Важно отметить. Первое — даже когда операция завершается ошибкой, мы всё равно должны закрыть span с endTime. Это важно, потому что мы хотим знать, сколько времени заняла операция до того момента, как она упала. Возможно, сервис отвечал очень долго перед тем как упасть, и это важная диагностическая информация. Второе — нам надо добавить поле status со значением 'error' и поле error с детальной информацией об ошибке. Это позволяет системе трассировки визуально выделить проблемные span'ы, обычно красным цветом.
Это и есть суть трассировки — возможность взять один запрос пользователя и увидеть его полный путь через всю систему с временными метками на каждом этапе.
Связь метрик и трейсов
Чтобы связать метрики с трейсами используются образцы (exemplars). Это стандартный механизм в OpenMetrics/Prometheus. Идея проста: к метрике (обычно гистограмме или счётчику) прикрепляется ссылка на конкретный трейс, который был "репрезентативным" для данного bucket'а.
http_request_duration_seconds_bucket{le="0.5"} 1234 # {trace_id="abc123"} 0.42 1625000000
Когда видишь, например, всплеск p95 latency — кликаешь на exemplar и можешь увидеть значение trace_id этго bucket. Поддерживается в Grafana.
Связь логов и трейсинга
Ключевая идея простая: вставить trace_id и span_id в каждую лог-запись. Так можно найти все логи из всех сервисов для одного запроса — и наложить их на timeline.
На коленке. Сделаем свой трейсинг
Предлагаю перейти ближе к практике и собрать учебный проект, на котором можно увидеть вышесказанное.
Будем использовать проект из 3 nodejs приложений, вызывающие друг друга последовательно. Перед ними будет стоять nginx. Это простой проект для понимания.

Нам надо “пронзить” все узлы участвующие в запросе идентификатором. Этот идентификатор будет путешествовать по всем сервисам с помощью http заголовка. Назовем этот заголовок X-TRACE-ID. В нашем примере первый узел, который встречает запрос - это nginx. Он автоматически генерирует свой идентификатор с именем $request_id. Его мы и будем использовать для значения заголовка X-TRACE-ID. Нам еще нужны каждый этап путешествия запроса обернуть в span-ы, чтобы разбить весь запрос на составные части. В каждом nodejs приложении будем создавать свой span. У нас получится матрешка из span-ов. Для того чтобы указать родственную связь с вложенным span (в нашем случае с вложенным приложением) мы будем использовать http заголовок X-PARENT-SPAN-ID. Т.к. nginx не может по запросу сгенерировать число, то в этот заголовок положим просто константу 0000000000000000. Nginx отправит запрос дальше, но уже с заголовками X-TRACE-ID и X-PARENT-SPAN-ID.
Наглядно это можно изобразить так:

Я подготовил небольшой проект, который продемонстрирует идею наблюдаемости.
Этот проект можно найти в github
https://github.com/BondarenkoAlex/observability-manual-example
Склонируйте реп
git clone git@github.com:BondarenkoAlex/observability-manual-example.git
Нам нужно запустить docker compose для этого у вас должен быть установлен docker.
docker compose up --build

После этого приложение будет доступно на http://localhost:8080. Чтобы система выглядела более живой - сгенерируем трафик командой
for ((i=1;i<=30;i++)); do curl -v --header "Connection: keep-alive" "http://localhost:8080/"; done
Мы сгенерировали трафик к нашей системе. И теперь мы готовы к анализу.
Откроем grafana, которая доступна по адресу http://localhost:3000/dashboards
Далее щелкаем на дашборд с именем HTTP 5xx Responses. Должны увидеть примерно следующую картину:

Точки на графике это ссылки по которым можно найти конкретный трейс (trace_id), который был "репрезентативным" для данного bucket. Мы можем кликнуть на эту точки. Тогда отобразится попап в котором мы можем увидеть значения trace_id и span_id, и другую информацию.

Теперь мы можем скопировать идентификатор trace_id и пойти, например, в jaeger и посмотреть водопад запросов связанные с этим идентификатором. Jaeger доступен по адресу http://localhost:16686 Вбиваем в поисковую строку значение trace_id и видим водопад запросов. Тут мы также можем найти значение span_id любого сеервиса.

Потом с этим же trace_id можно пойти и посмотреть все логи в kibana.
Она будет доступна по адресу http://localhost:5601/app/discover
Например, в поисковую строку вводим значение trace_id и находим все логи. Далее мы можем сузить логи добавив интересующий нас span_id. По логам мы легко сможем восстановить всю картину в том числе и время выполнения.

Тут нет какой-то строгой последовательности просмотра Grafana, Jaeger, Kibana. Главное, что они связаны друг с другом с помощью trace_id и span_id.
Если у вас появилась какая-то аномалия в системе, вы сможете восстановить полную картину.
Нет большой необходимости оборачивать в трейс “каждую функцию”. Вам в большинстве случаев достаточно измерить время работы сервиса. Всю остальную информацию вы сможете достать из логов.
Заключение
Наблюдаемость распределенной системы - важная тема. Без нее вы слепы и не сможете ответить на вопрос “что случилось?” и “что это вызвало?”. И очень важно, вы не сможете быстро найти и пофиксить причину сбоя.
Еще хотелось рассмотреть эту тему на живом примере, который можно потрогать и поэкспериментировать.
Наблюдаемость помогает:
Обнаруживать проблемы до того, как они станут критическими.
Находить причины инцидентов.
Быстро устранять неисправности.
Предотвращать повторение ошибок.
Автоматизировать масштабирование.
Повышать надежность системы.
Выявлять атаки и инциденты безопасности.
Принимать технические и бизнес-решения на основе данных.

