Сквозная трассировка с помощью OpenTelemetry

Вне зависимости от того, внедряете вы микросервисы или нет (а скорее всего, не стоит), ваша система, скорее всего, состоит из множества компонентов. Самая простая система, вероятно, состоит из обратного прокси, приложения и базы данных. В этом случае мониторинг — не просто хорошая идея, это требование. Чем больше компонентов, через которые может проходить запрос, тем выше требования.

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

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

Спецификация W3C Trace Context

Решение для трассировки должно предоставлять стандартный формат для работы с разнородными технологическими стеками. Такой формат должен соответствовать спецификации, формальной или фактической.

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

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

Эта спецификация определяет стандартные HTTP-заголовки и формат значений для распространения контекстной информации, которая позволяет реализовать сценарии распределенной трассировки. Спецификация стандартизирует способ передачи и изменения контекстной информации между сервисами. Контекстная информация уникально идентифицирует отдельные запросы в распределенной системе, а также определяет средства для добавления и распространения контекстной информации, специфичной для поставщика.

— https://www.w3.org/TR/trace-context/

Из документа вытекают две важные концепции:

  • Трассировка проходит по пути запроса, который охватывает несколько компонентов.
  • Трасса привязана к одному компоненту и связана с другой трассой отношениями «ребенок-родитель».

На момент написания этой статьи спецификация является рекомендацией W3C, что является финальной стадией.

Trace Context уже имеет множество реализаций. Одной из них является OpenTelemetry.

OpenTelemetry как золотой стандарт

Чем ближе вы к операционной части ИТ, тем выше вероятность того, что вы слышали об OpenTelemetry:

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

OpenTelemetry общедоступен на нескольких языках и подходит для использования.

— https://opentelemetry.io/

OpenTelemetry — это проект, управляемый CNCF. До OpenTelemetry существовало два проекта:

  • OpenTracing, сфокусированный на трассировках, как следует из его названия.
  • OpenCensus, целью которого было управление метриками и трассировками.

Оба проекта объединились и добавили сверху журналы. Теперь OpenTelemetry предлагает набор «слоев», сфокусированных на наблюдаемости:

  • API инструментария на различных языках
  • Канонические реализации, опять же на разных языках
  • Инфраструктурные компоненты, такие как коллекторы
  • Форматы совместимости, такие как Trace Context от W3C.

Обратите внимание, что хотя OpenTelemetry является реализацией Trace Context, он делает больше. Trace Context ограничивает себя HTTP, в то время как OpenTelemetry позволяет пересекать не-веб компоненты, такие как Kafka. Это выходит за рамки данной статьи в блоге.

Пример использования

Мой любимый пример использования — это магазин электронной коммерции, поэтому не будем его менять. В данном случае магазин построен на микросервисах, каждый из которых доступен через REST API и защищен за API-шлюзом. Чтобы упростить архитектуру для статьи в блоге, я буду использовать только два микросервиса: catalog управляет продуктами, а pricing обрабатывает цены на продукты.

Когда пользователь заходит в приложение, главная страница извлекает все продукты, получает их соответствующую цену и отображает их.

Чтобы сделать ситуацию более интересной, catalog — это приложение Spring Boot, написанное на Kotlin, а pricing — это приложение Python Flask.

Трассировка должна позволить нам проследить путь запроса через шлюз, оба микросервиса и, если возможно, базы данных.

Трассировка на шлюзе

Точка входа — самая интересная часть трассировки, поскольку она должна генерировать идентификатор трассировки: в данном случае точкой входа является шлюз. Для реализации демонстрации я буду использовать Apache APISIX:

Apache APISIX предоставляет богатые возможности управления трафиком, такие как балансировка нагрузки, динамическое восходящее движение, освобождение канавы, разрыв цепи, аутентификация, наблюдаемость и т.д.

— https://apisix.apache.org/

Apache APISIX основан на архитектуре плагинов и предлагает плагин OpenTelemetry:

Плагин opentelemetry может использоваться для сообщения данных трассировки в соответствии со спецификацией OpenTelemetry.

Плагин поддерживает только бинарно-кодированный OLTP через HTTP.

— https://apisix.apache.org/docs/apisix/plugins/opentelemetry/

Давайте настроим плагин opentelemetry:

apisix:
  enable_admin: false              #1
  config_center: yaml              #1
plugins:
  - opentelemetry                  #2
plugin_attr:
  opentelemetry:
    resource:
      service.name: APISIX         #3
    collector:
      address: jaeger:4318         #4
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Запустите Apache APISIX в автономном режиме, чтобы было легче следить за демонстрацией. В любом случае, это хорошая практика в производстве
  2. Настройте opentelemetry как глобальный плагин
  3. Задайте имя службы. Это имя будет отображаться в компоненте отображения трасс.
  4. Отправьте трассировки в службу jaeger. В следующем разделе будет описано это.

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

global_rules:
  - id: 1
    plugins:
      opentelemetry:
        sampler:
          name: always_on          #1  
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Трассировка влияет на производительность. Чем больше мы трассируем, тем больше влияние. Следовательно, мы должны тщательно взвесить влияние на производительность по сравнению с преимуществами наблюдаемости. Однако для демонстрации мы хотим отслеживать каждый запрос.

Сбор, хранение и отображение трассировок

Хотя Trace Context является спецификацией W3C, а OpenTelemetry — стандартом де-факто, на рынке существует множество решений для сбора, хранения и отображения трасс. Каждое решение может предоставлять все три возможности или только часть из них. Например, стек Elastic обрабатывает хранение и отображение, но для сбора трасс вы должны полагаться на что-то другое. С другой стороны, Jaeger и Zipkin предоставляют полный набор для реализации всех трех возможностей.

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

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

Ниже приведены соответствующие порты образа:

Порт Протокол Компонент Функция
16686 HTTP запрос обслуживать фронтенд
4317 HTTP коллектор принимать протокол OpenTelemetry Protocol (OTLP) через gRPC, если он включен
4318 HTTP коллектор принимать протокол OpenTelemetry Protocol (OTLP) по HTTP, если он включен

Бит Docker Compose выглядит следующим образом:

services:
  jaeger:
    image: jaegertracing/all-in-one:1.37           #1
    environment:
      - COLLECTOR_OTLP_ENABLED=true                #2
    ports:
      - "16686:16686"                              #3
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Использовать изображение all-in-one.
  2. Очень важно: включить коллектор в формате OpenTelemetry
  3. Открыть порт пользовательского интерфейса

Теперь, когда мы настроили инфраструктуру, мы можем сосредоточиться на включении трассировки в наших приложениях.

Трассировки в приложениях Flask

Сервис pricing — это простое приложение Flask. Он предлагает одну конечную точку для получения цены одного продукта из базы данных.

@app.route('/price/<product_str>')                           #1-2
def price(product_str: str) -> Dict[str, object]:
    product_id = int(product_str)
    price: Price = Price.query.get(product_id)               #3
    if price is None:
        return jsonify({'error': 'Product not found'}), 404
    else:
        low: float = price.value - price.jitter              #4
        high: float = price.value + price.jitter             #4
        return {
            'product_id': product_id,
            'price': round(uniform(low, high), 2)            #4
        }
Вход в полноэкранный режим Выход из полноэкранного режима
  1. Конечная точка
  2. Маршрут требует идентификатор продукта
  3. Получение данных из базы данных с помощью SQLAlchemy
  4. Реальные системы ценообразования никогда не возвращают одну и ту же цену в течение долгого времени; давайте немного рандомизируем цену для развлечения

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

Теперь настало время инструментировать приложение. Доступны два варианта: автоматическое и ручное инструментирование. Автоматическая не требует больших усилий и быстро приносит победу; ручная требует целенаправленного времени на разработку. Я бы посоветовал начать с автоматического и добавлять ручное только в случае необходимости.

Нам нужно добавить пару пакетов Python:

opentelemetry-distro[otlp]==0.33b0
opentelemetry-instrumentation
opentelemetry-instrumentation-flask
Вход в полноэкранный режим Выход из полноэкранного режима

Нам нужно настроить пару параметров:

  pricing:
    build: ./pricing
    environment:
      OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317     #1
      OTEL_RESOURCE_ATTRIBUTES: service.name=pricing      #2
      OTEL_METRICS_EXPORTER: none                         #3
      OTEL_LOGS_EXPORTER: none                            #3
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Отправка трасс в Jaeger
  2. Задайте имя сервиса. Это имя будет отображаться в компоненте отображения трасс.
  3. Нас не интересуют ни журналы, ни метрики.

Теперь, вместо того чтобы использовать стандартную команду flask run, мы обернем ее:

opentelemetry-instrument flask run
Войти в полноэкранный режим Выход из полноэкранного режима

Только этим мы уже собираем диапазоны от вызовов методов и маршрутов Flask.

При необходимости мы можем вручную добавить дополнительные диапазоны, например:

from opentelemetry import trace

@app.route('/price/<product_str>')
def price(product_str: str) -> Dict[str, object]:
    product_id = int(product_str)
    with tracer.start_as_current_span("SELECT * FROM PRICE WHERE ID=:id", attributes={":id": product_id}) as span: #1
        price: Price = Price.query.get(product_id)
    # ...
Войти в полноэкранный режим Выйти из полноэкранного режима
  1. Добавьте дополнительную область с настроенными меткой и атрибутом.

Трассировки в приложениях Spring Boot

Сервис catalog — это реактивное приложение Spring Boot, разработанное на Kotlin. Он предлагает две конечные точки:

  • Одна для получения одного продукта
  • Другая — для получения всех товаров.

Обе сначала ищут в базе данных продуктов, а затем запрашивают цену у вышеупомянутого сервиса pricing.

Что касается Python, мы можем использовать автоматические и ручные инструменты. Начнем с самого легкого — автоматического инструментария. На JVM мы достигаем этого с помощью агента:

java -javaagent:opentelemetry-javaagent.jar -jar catalog.jar
Войти в полноэкранный режим Выход из полноэкранного режима

Как и в Python, он создает диапазоны для каждого вызова метода и точки входа HTTP. Он также использует вызовы JDBC, но у нас реактивный стек и поэтому мы используем R2DBC. Для справки, на GitHub открыт вопрос о добавлении поддержки.

Нам нужно настроить поведение по умолчанию:

  catalog:
    build: ./catalog
    environment:
      APP_PRICING_ENDPOINT: http://pricing:5000/price
      OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317     #1
      OTEL_RESOURCE_ATTRIBUTES: service.name=orders       #2
      OTEL_METRICS_EXPORTER: none                         #3
      OTEL_LOGS_EXPORTER: none                            #3
Входить в полноэкранный режим Выходить из полноэкранного режима
  1. Отправлять трассировки в Jaeger
  2. Задайте имя службы. Это имя будет отображаться в компоненте отображения трасс.
  3. Нас не интересуют ни журналы, ни метрики.

Что касается Python, мы можем улучшить игру, добавив ручную инструментацию. Доступны два варианта: программный и основанный на аннотациях. Первый вариант немного сложный, если мы не используем Spring Cloud Sleuth. Давайте добавим аннотации.

Нам нужна дополнительная зависимость:

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-instrumentation-annotations</artifactId>
    <version>1.17.0-alpha</version>
</dependency>
Вход в полноэкранный режим Выход из полноэкранного режима

Будьте внимательны, артефакт был совсем недавно перемещен из io.opentelemetry:opentelemetry-extension-annotations.

Теперь мы можем аннотировать наш код:

@WithSpan("ProductHandler.fetch")                                               //1
suspend fun fetch(@SpanAttribute("id") id: Long): Result<Product> {             //2
    val product = repository.findById(id)
    return if (product == null) Result.failure(IllegalArgumentException("Product $id not found"))
    else Result.success(product)
}
Вход в полноэкранный режим Выйти из полноэкранного режима
  1. Добавить дополнительную область с настроенной меткой
  2. Использовать параметр как атрибут, с ключом id и значением параметра во время выполнения.

Результат!

Теперь мы можем поиграть с нашей простой демонстрацией, чтобы увидеть результат:

curl localhost:9080/products
curl localhost:9080/products/1
Войти в полноэкранный режим Выйти из полноэкранного режима

Ответы не интересны, но давайте посмотрим на пользовательский интерфейс Jaeger. Мы находим обе трассировки, по одной на каждый вызов:

Мы можем погрузиться в пролеты одной трассы:

Обратите внимание, что мы можем вывести поток последовательности без приведенной выше диаграммы UML. Еще лучше то, что последовательность отображает вызовы внутри компонента.

Каждый пролет содержит атрибуты, которые добавила автоматическая инструментация, и те, которые мы добавили вручную:

Заключение

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

Тем не менее, большинство систем так или иначе полагаются на HTTP. Хотя настройка не является тривиальной, она также не слишком сложна. Отслеживание HTTP-запросов между компонентами — это хорошее начало на пути к наблюдаемости вашей системы.

Полный исходный код этой заметки можно найти на GitHub:

nfrankel / opentelemetry-tracing

Демонстрация сквозной трассировки с помощью OpenTelemetry

Чтобы пойти дальше:

  • Спецификация W3C Trace Context
  • Реализации контекста трассировки
  • Руководство для начинающих по OpenTelemetry
  • Автоматические инструменты Python
  • Дистрибутив Python
  • Jaeger Getting Started

Первоначально опубликовано на A Java Geek 28 августаth, 2022

Оцените статью
devanswers.ru
Добавить комментарий