Существует расхожее мнение, что мы, как разработчики программного обеспечения, тратим 90% нашего времени на отладку и только 10% непосредственно на написание кода. Конечно, это соотношение слишком преувеличено! Но это правда, что отладка занимает значительную часть нашего рабочего времени, особенно в эпоху расцвета микросервисных архитектур. На сегодняшний день уже никого не удивишь тем, что на наших производственных серверах одновременно работают сотни или даже тысячи микросервисов.

Когда дело доходит до отладки программного обеспечения, мы традиционно полагаемся на логи. Однако не все логи одинаково полезны. Они могут быть несодержательными, например, указывать только код ошибки или отражать только общую информацию, как, например, «Что-то пошло не так». Даже если в логе отражена более конкретная ошибка, например «Идентификатор запроса пользователя недействителен» с кодом ошибки неправильного запроса «400», нам все равно могут потребоваться часы или даже дни, чтобы выяснить первопричину проблемы из-за большого количества вовлеченных в работу сервисов. И вот здесь в игру вступает трассировка запросов (request tracing).

Реализация трассировки запросов в нашем Node.js-приложении может помочь нам увидеть всю последовательность событий, приведших к возникновению ошибки. Благодаря этому мы сможем точно определить, какие сервисы могут быть связаны с ошибкой, и быстро найти ее первопричину. В рамках этой статьи мы с вами познакомимся с определением трассировки запросов, оценим ее важность и разберемся, как мы можем эффективно применять трассировку запросов в нашем Node.js-приложении.

Что такое трассировка запросов?

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

Зачем нам может понадобиться трассировка запросов?

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

Трассировки (или трейсы – traces) же помогают нам обнаружить все запросы, выполняемые в рамках одного родительского запроса. Мы также можем увидеть запрос, который был сделан непосредственно перед родительским запросом. Короче говоря, трассировка запросов дает нам целостное представление о контексте и процессе выполнения запроса. Комбинируя трассировки с логами, мы можем определить основную причину проблемы. Кроме того, трассировка сокращает время отладки, поскольку помогает выявить первопричину проблемы простым и быстрым способом, визуализирую логи каскадов ошибок.

Небольшая просьба от Amplication

Доброго времени суток, коллега-разработчик! Раз уж вас заинтересовала наша статья трассировке запросов, у нас есть к вам одна небольшая просьба... Мы бы были очень признательны, если бы вы поставили звезду нашему репозиторию Amplication на GitHub. Вы можете думать об этом как о виртуальном “дай пять”, благодаря которому день нашего разработчика станет чуточку лучше. К тому же я пообещал команде устроить вечеринку с пиццей, когда мы наберем 10 000 звезд. Так что, если вы хотите увидеть радостных разработчиков с томатным соусом на лицах, ставьте нам ???? на GitHub!

Пожалуйста, не игнорируйте эту просьбу! Нажмите на эту блестящую звездочку и сделайте день команды Amplication чуточку ярче)

Как работает трассировка запросов

Когда пользователь выполняет действие, например, нажимает на кнопку «Подтвердить», чтобы купить обувь в интернет-магазине, выполняется сразу несколько запросов. Во-первых, после того, как пользователь нажмет «Подтвердить», будет создан новый трейс (трассировка). Затем будет активирован родительский спан (диапазон). Внутри это родительского спана может происходить запрос API для проверки аутентификации пользователя и другого API для подтверждения наличия у пользователя разрешения на совершение платежа. Также могут выполняться дополнительные запросы API, например проверка количества обуви на складе или проверка достаточности баланса пользователя для совершения платежа.

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

Как реализовать трассировку запросов в Node.js-приложении?

Чтобы реализовать трассировку запросов в Node.js-приложении, в первую очередь нам нужно назначить каждому запросу уникальный идентификатор, который будет передаваться вместе с этим запросом от сервиса к сервису. В результате мы сможем использовать этот идентификатор для отслеживания и визуализации запросов в нашей системной и определения первопричин ошибок.

Популярные инструменты, поддерживающие трассировку запросов

Существует целый ряд опенсорсных серверных инструментов, предоставляющих возможность хранения и визуализации трейсов, среди которых можно выделить Jaeger, Zipkin и Signoz. Раньше существовало сразу два популярных формата трейсов: OpenTracing и OpenCensus. Инструменты обычно поддерживали только один из них, что усложняло интеграцию приложений с необходимыми инструментами. К счастью, в 2019 году OpenTracing и OpenCensus объединились в OpenTelemetry, что позволило разработчикам программного обеспечения относительно легко реализовывать трассировку запросов.

OpenTelemetry — это независимый от поставщика набор инструментов, которые могут визуализировать трейсы, собирать данные о времени, затраченном на обработку каждого запроса, и фиксировать запросы как отдельные спаны в трейсах. Чтобы различные инструменты наблюдения могли визуализировать трейсы, приложения должны реализовывать форматы OpenTelemetry.

Реализация трассировки запросов в форме middleware

Допустим, у нас есть приложение, которое позволяет пользователям постить обзоры на фильмы в блоге. Приложение предоставит пользователям API для создания учетной записи, входа в систему и создания новых постов в блоге. Для реализации трассировки запросов в нашем приложении мы будем использовать OpenTelemetry. Затем трейсы будут отправляться в Jaeger. В качестве базы данных наше приложение будет использовать MongoDB, а в качестве веб-сервера — Express. Чтобы OpenTelemetry могла связывать трейсы с запросами API, нам нужно установить следующие зависимости:

npm install --save @opentelemetry/api
npm install --save @opentelemetry/sdk-trace-node
npm install --save opentelemetry-instrumentation-express
npm install --save @opentelemetry/instrumentation-mongodb
npm install --save @opentelemetry/instrumentation-http

Нам также необходимо установить зависимость Jaeger для пересылки трейсов OpenTelemetry в Jaeger, чтобы позже мы могли просматривать трейсы в GUI Jaeger.

npm install --save @opentelemetry/exporter-jaeger

Чтобы реализовать middleware для трассировку запросов, нам нужно будет создать в корневой папке нашего проекта новый файл под именем tracing.js. Скопируйте следующий код в этот файл:

const { Resource } = require("@opentelemetry/resources");
const { SemanticResourceAttributes } = require("@opentelemetry/semantic-conventions");
const { SimpleSpanProcessor } = require("@opentelemetry/sdk-trace-base");
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
const { trace } = require("@opentelemetry/api");
//экспортер
const { JaegerExporter } = require("@opentelemetry/exporter-jaeger");
//инструментации
const { ExpressInstrumentation } = require("opentelemetry-instrumentation-express");
const { MongoDBInstrumentation } = require("@opentelemetry/instrumentation-mongodb");
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");
const { registerInstrumentations } = require("@opentelemetry/instrumentation");

//Экспортер
module.exports = (serviceName) => {
  const exporter = new JaegerExporter({
    endpoint: "http://localhost:14268/api/traces",
  });

  const provider = new NodeTracerProvider({
    resource: new Resource({
      [SemanticResourceAttributes.SERVICE_NAME]: serviceName,
    }),
  });

  provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
  provider.register();
  registerInstrumentations({
    instrumentations: [
      new HttpInstrumentation(),
      new ExpressInstrumentation(),
      new MongoDBInstrumentation(),
    ],
    tracerProvider: provider,
  });

  return trace.getTracer(serviceName);
};

Нам также нужно прописать в коде приложения инструментации, чтобы можно было связывать трейсы с MongoDB, Express и HTTP запросами:

instrumentations: [
      new HttpInstrumentation(),
      new ExpressInstrumentation(),
      new MongoDBInstrumentation(),
  ]

Трейсы будут отправляться на конечную точку Jaeger: http://localhost:14268/api/traces. Нам также нужно добавить одну строчку кода в главный файл приложения, чтобы позволить OpenTelemetry отправлять трейсы в приложение:

const tracer = require("./tracing")("blog-movie-review-service");

Прежде чем мы сможем запустить наше приложение для проверки трассировки, нам нужно запустить сервис Jaeger, который должен сохранять трейсы. Чтобы запустить сервис Jaeger, вам достаточно выполнить следующую команду в Docker:

docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -p 5775:5775/udp \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14250:14250 \
  -p 14268:14268 \
  -p 14269:14269 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.32

Давайте запустим приложение, а затем перейдем на страницу с GUI Jaeger, чтобы увидеть полученные трейсы.

Рисунок 1: Захваченные трейсы отображаются на странице с GUI Jaeger.
Рисунок 1: Захваченные трейсы отображаются на странице с GUI Jaeger.

Мы видим, что трейсы были успешно перехвачены OpenTelemetry и сохранены в Jaeger. Чтобы посмотреть на трейс, связанный с запросом API для публикации нового поста в блоге, кликните по первому трейсу (рисунок 1). В результате мы увидим подробную информацию об этом трейсе (рисунок 2).

Рисунок 2: Подробная информация о трейсе успешного запроса на публикацию нового поста.
Рисунок 2: Подробная информация о трейсе успешного запроса на публикацию нового поста.

Здесь мы видим содержимое трейса: спаны для HTTP-запроса (POST /v1/posts) и запроса к MongoDB (mongodb.insert). Теперь двайте посмотрим, как выглядит неудачный запрос API, когда мы указываем недопустимый authorId для публикации:

Рисунок 3: Подробная информация о трейсе неудавшегося запроса на публикацию нового поста в блоге.
Рисунок 3: Подробная информация о трейсе неудавшегося запроса на публикацию нового поста в блоге.

Мы сразу видим, что при запросе API не удалось выполнить HTTP-запрос, из-за проверки валидности authorId, и до запроса в MongoDB дело не доходит. Применяя трассировку запросов к нашему приложению таким образом, нам намного легче его отлаживать и обнаруживать, на каком этапе обработки запроса к API возникает проблема.

Лучшие практики трассировки запросов

Как мы видели, реализация трассировки запросов в Node.js-приложении позволяет нам быстро и эффективно исследовать проблемы в нашем программном обеспечении. Чтобы добиться оптимальных результатов от трассировки запросов, мы должны следовать трем главным лучшим практикам при задействовании трассировки запросов в нашем приложении.

1. Используйте стандартные форматы OpenTelemetry

Использование стандартных форматов OpenTelemetry позволяет другим инструментам, которые могут интегрироваться с OpenTelemetry (например, Zipkin, Jaeger или Prometheus), эффективно хранить и визуализировать трейсы. Это позволяет нам выбрать инструмент, который соответствует нашим конкретным потребностям, например, для достижения лучшей визуализации без необходимости менять формат трейсов.

2. Используйте уникальные строки для каждого источника

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

3. Выберите подходящее имена для спанов

Выбор имени спана, совпадающего с именем метода, вызываемого в рамках спана, позволяет нам выяснить, что происходило в приложении в момент возникновения проблемы. Мы также должны добавлять пользовательский тег спана в рамках процесса сохранения трейса. Это позволяет нам искать пользовательский тег напрямую, вместо того, чтобы извлекать идентификатор трейса, а затем производить поиск по нему. Например, мы можем добавить кастомный тег спана для трейса, связанного с публикацией нового поста в блоге. Когда нам будет нужно исследовать проблему, связанную с публикацией постов в блоге, мы можем использовать этот кастомный тег спана, и все трейсы публикации новых постов с обзорами на фильмы будут мгновенно найдены.

Заключение

В мире распределенного программного обеспечения и микросервисной архитектуры одних логов приложения недостаточно для отладки при возникновении проблем. Чтобы добиться наблюдаемости и чтобы мы могли быстро отлаживать проблемы с программным обеспечением, нам нужно реализовать трассировку запросов в нашем Node.js-приложении. Стандартный формат трассировки OpenTelemetry упрощает эту задачу, поскольку благодаря нему мы можем использовать один из многих доступных инструментов, помогающих в процессе трассировки запросов.


Микросервисы — популярный способ создания и поддержки современных веб-сервисов. Приглашаем всех желающих на открытый урок, на котором познакомимся с плюсами и минусами микросервисного подхода; узнаем, как мигрировать монолит на микросервисы, а также разберем решение на базе NodeJS + протокола GRPC. Записаться на урок можно на странице онлайн-курса "Node.js Developer".

Комментарии (0)