[Перевод] Секреты observability. Часть 3: распределённая трассировка с Jaeger и OpenTelemetry

image
Фото Bertrand Bouchez, Unsplash.com

В прошлой статье мы с помощью Prometheus AlertManager настроили правила, чтобы отправлять уведомления через Slack при срабатывании алертов. И хотя алерты и уведомления — это удобно и полезно, сами по себе метрики не до конца объясняют проблему. Они просто показывают, что значения одного экземпляра вышли за установленные лимиты, но в распредел ённых системах метрики не могут отследить запрос, который проходит через несколько компонентов. С распространением микросервисов системы усложняются, поэтому мы должны проследить весь путь запроса, чтобы понять, что пошло не так. Для этого можно использовать распределённую трассировку, которая записывает действия, выполняемые в связи с запросом, и дает контекст, который мы не найдем в метриках и логах.

В этой статье мы расширим возможности observability (наблюдаемости) приложения — создадим спаны (span) и экспортируем их в распределённую опенсорс-систему Jaeger. Но сначала разберемся, что такое трейс.

Все ресурсы для этой статьи можно загрузить из репозитория.


Трассировка

Трейс — это коллекция спанов, где спан — это запись операции, выполняемой одним сервисом. У него есть имя, время начала, продолжительность, контекст и дополнительные метаданные. С помощью трейсов мы отслеживаем путь запроса по всем сервисам распределённой системы.

image

Анализируя данные трейсов, можно увидеть поведение запроса, найти проблемы и узкие места, а также определить потенциальные области для улучшения и оптимизации. Для создания спанов мы используем инструментирование с помощью API или SDK, которые предоставляются клиентской библиотекой трассировщика.


Инструментирование

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

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


  • Jaeger: разработан Uber, теперь это проект CNCF со статусом «graduated».
  • Zipkin: изначально разработан Twitter на основе документа Google Dapper.
  • AWS X-Ray: распределённая система трассировки AWS.
  • Google Cloud Trace: распределённая система трассировки для Google Cloud (ранее — Stackdriver Trace).
  • Azure Application Insights: функция Azure Monitor.

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


Jaeger

Jaeger — это опенсорс-система распределённой трассировки, изначально разработанная Uber. Она используется для мониторинга и траблшутинга распределённых систем на основе микросервисов.

image

Компоненты архитектуры Jaeger:


  • Jaeger Client: реализация OpenTracing API для инструментирования приложений.
  • Jaeger Agent: сетевой демон, который ожидает получения спанов по UDP.
  • Jaeger Collector: получает трейсы от агентов и прогоняет их по конвейеру обработки.
  • Storage: место, где хранятся трейсы.
  • Jaeger Query: сервис, который извлекает трейсы из хранилища и отображает их в пользовательском интерфейсе.


Деплоймент Jaeger

Существуют разные стратегии деплоймента Jaeger в Kubernetes:


  • Все в одном: все компоненты Jaeger деплоятся в одном поде, который использует хранение в памяти.
  • Продакшен: компоненты деплоятся по отдельности. Компоненты collector и query настраиваются для работы с Cassandra или Elasticsearch (рекомендуется использовать Elasticsearch).
  • Стриминг: похоже на предыдущую стратегию, но включает возможности потоковой обработки Kafka, которая находится между collector и storage и упрощает жизнь последнему при высокой нагрузке.

Раз мы всего лишь тестируем концепцию, для простоты используем стратегию «все в одном» с Helm и оператором. В продакшене лучше использовать одну из двух других стратегий.
Добавляем репозиторий helm и устанавливаем кастомные ресурсы Jaeger с оператором.

helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update
helm install jaeger jaegertracing/jaeger-operator -n observability

Создаем кастомный ресурс Jaeger командой kubectl apply -f

apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: simplest
---
apiVersion: v1
kind: Service
metadata:
  name: jaeger-service
  namespace: observability
spec:
  selector:
    app: jaeger
  type: NodePort
  ports:
  - name: http
    port: 16686
    targetPort: 16686
    nodePort: 30007


В предыдущей статье мы использовали команду kubectl port forward для предоставления пользовательских интерфейсов. На этот раз мы создадим сервис NodePort в качестве постоянной альтернативы.

Если вы используете Minikube, выполните команду minikube ip, чтобы получить IP ноды.

image
Пользовательский интерфейс Jaeger на порте 30007

Мы готовы к тому, чтобы приступить к инструментированию сервисов Node.js, но сначала нужно подготовить OpenTelemetry.


Настройка OpenTelemetry

К счастью, настроить OpenTelemetry очень просто. Сначала нужно выбрать, какой тип инструментирования мы будем использовать.


  • Для инструментирования вручную нам понадобится Tracing SDK.
  • Для автоматического инструментирования — Node SDK. Автоматическое инструментирование включает OpenTelemetry API, так что мы можем создавать кастомные спаны в любое время.

Затем нужно определить, куда мы будем отправлять спаны. Мы будем экспортировать спаны в Jaeger, но существует много других экспортеров. У каждого экспортера своя конфигурация. Экспортер Jaeger по умолчанию отправляет спаны на localhost:6832 (URL агента Jaeger). В продакшене это связано с тем, что мы разворачиваем агент Jaeger отдельно от сервисов, но в нашем случае мы используем агент, который уже входит в под Jaeger.

Настроив экспортер, мы добавляем его в обработчик спанов и инициализируем провайдер командой register(). Чтобы автоматизировать инструментирование, нужно зарегистрировать подходящие модули. Например, если мы хотим инструментировать HTTP-вызовы, можно использовать модуль http, если вызовы MySQL — модуль mysql, и т. д. Мы будем инструментировать запросы HTTP, Express Framework и библиотеку MySQL автоматически.

Наконец, мы экспортируем трассировщик для сервиса.

const { trace } = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/node');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { SimpleSpanProcessor } = require('@opentelemetry/tracing');

const provider = new NodeTracerProvider();
const exporter = new JaegerExporter({ 
    serviceName : process.env.SERVICE_NAME,
    host: process.env.JAEGER_HOST,
    port: process.env.JAEGER_PORT
 });

provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register()
registerInstrumentations({
    tracerProvider: provider,
    instrumentations: [
      new HttpInstrumentation({
        ignoreIncomingPaths: [/\/metrics/]
      }),
      new ExpressInstrumentation(),
    ]
})
const tracer = trace.getTracer(process.env.SERVICE_NAME)

module.exports = tracer;

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

Теперь тестируем сервисы:

kubectl port-forward service/hello-service-svc -n applications 8080
curl http://localhost:8080/sayHello/iroh #On a different terminal
Hey! this is iroh, here is my message to you:  It is important to draw wisdom from many different places

В интерфейсе Jaeger мы видим доступные сервисы в списке Service. Выбираем hello-service и нажимаем Find Traces (Найти трейсы).

image
Форма поиска в Jaeger


Если вы не видите сервисы, еще раз проверьте конфигурацию OpenTelemetry. Там должно быть указание на сервис агента Jaeger через порт UDP.

Мы увидим спаны трех цветов, по одному на сервис. У каждого сервиса будет как минимум два вида спанов: спаны GET, которые представляют запросы, инструментированные с помощью HTTP, и спаны Middleware и Route, инструментированные с помощью Express. Некоторые спаны middleware связаны с функциями промежуточной обработки, которые мы создали для сбора значений метрик в части 1 этой серии.

image

Если нажать на спан, мы увидим атрибуты, по которым можно понять контекст.

image
Спан HTTP в Jaeger

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

image
Спан MySQL в Jaeger

У нас уже есть много информации. Но что если нам нужен более детализированный контроль над тем, что происходит у нас в сервисе? Как сочетать автоматическую и ручную трассировку?


Распространение контекста

Прежде чем создавать кастомные спаны, нужно решить, как проводить корреляцию между спанами. Для корреляции спаны должны обмениваться информацией через контекст. Контекст содержит информацию, которую можно передавать между функциями в одном процессе (распространение внутри процесса) и в разных (распространение между процессами).

Распространение — это механизм, с помощью которого контекст перемещается между разными сервисами и процессами.

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

Для распространения контекста используется много протоколов. Самые популярные из них:

W3C Trace-Context HTTP Propagator

B3 Zipkin Propagator

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


Ручное инструментирование

Чтобы создать первый спан, нужно импортировать трассировщик из файла, где мы настроили OpenTelemetry, и вызвать метод startSpan. Спану нужно имя, и по желанию вы можете включить кастомные атрибуты и контекст. В данном случае мы извлекаем текущий контекст с помощью context.active(). Если активный контекст существует, спан будет создан в нём. Нам нужно начать спан, добавить нужный код и затем завершить спан.

Чтобы распространять контекст между разными функциями (для добавления в спан атрибутов или создания дочернего спана), нужно заключить вызовы в context.with, а еще задать желаемый спан в текущем контексте с помощью setSpan. В следующем коде мы создаем спан Router GET, а затем задаем его как текущий спан и вызываем функцию getPerson. В результате будет создан спан getPerson как дочерний для спана Router GET.

Если провести несколько тестов, результат будет таким:

const tracer  = require('../telemetry/tracer')
const { context, setSpan, SpanStatusCode } = require('@opentelemetry/api');

router.get('/sayHello/:name', async (req,res,next) => {
    const span = tracer.startSpan("Router GET",{},context.active())
    const name = req.params.name;

    try {
        await context.with(setSpan(context.active(),span), async () => {
            const person = await getPerson(name)
            if(!person.data.name) {
                span.end()
                res.status(404).send("The name is not registered on the database.")
            } else {
                const response = await formatGreeting(person.data)
                span.end()
                res.status(response.status).send(response.data)
            }         
        })
    } catch(err) {
        logger.error(err)
        span.addEvent(err)
        span.setAttribute("error",true)
        span.setStatus(SpanStatusCode.ERROR)
        span.end()
        res.status(500).send(err)
    }
})

function getPerson(name) {
    return new Promise((resolve,reject) => {
        const span = tracer.startSpan("getPerson",{},context.active())
        logger.info("Requesting person information")
        context.with(setSpan(context.active(),span), () => {
            axios.get(encodeURI("http://" + process.env.PEOPLE_SERVICE_HOST + "/getPerson/" + name),{
            }).then((response) => {
                span.end()
                resolve(response)
            }).catch((err) => {
                logger.error(err)
                span.addEvent(err.stack)
                span.setStatus(SpanStatusCode.ERROR)
                span.end()
                reject(err)
            })
        })
    })
}

Если провести несколько тестов, результат будет таким:

image

На этот раз у нас есть автоматически созданные спаны HTTP, MySQL и Express Framework и кастомные спаны, созданные вручную из функций в коде с помощью OpenTelemetry.


Заключение

Распределённая трассировка может показаться сложной, но стоит взяться за дело, и все становится понятно. Сообщество OpenTelemetry постоянно работает над упрощением и улучшением проекта. Вы увидели, как легко комбинировать автоматическое и ручное инструментирование, чтобы максимально эффективно использовать данные трассировки сервисов.

Ссылки:
https://www.jaegertracing.io/docs/1.22/
https://github.com/jaegertracing/helm-charts
https://opentelemetry.io/docs/js/getting_started/nodejs/
https://istio.io/latest/docs/tasks/observability/distributed-tracing/overview/

© Habrahabr.ru