Как стать автором
Обновить

Jaeger v2

Время на прочтение7 мин
Количество просмотров1K

Появилась у меня задача по мониторингу и оценке производительности проекта на микросервисной архитектуре. Для решения был выбран Jaeger. Он давно на рынке, активно развивается (не так давно вышла версия 2, в которой упростилось развертывание и появилась интеграция OpenTelemetry). На мой взгляд, Jaeger – отличное решение для трейсинга, но документация ощущается как не до конца собранный пазл: важные вещи разбросаны, а для понимания приходится обращаться к исходному коду или искать примеры в GitHub-репозиториях.

Цель данной статьи показать на практике, как внедрить Jaeger в продукт на микросервисах.

Ссылку на код всего, о чем пойдет речь дальше, можно найти в конце статьи.

Основные понятия

Спан(span) - единица работы, которую выполняет система.

Спан содержит:

  • одну операцию, запрос или обработку

  • временные метками начала и конца

  • контекст (сюда можно погрузить любую информацию)

Спан может входить в состав трейса (trace), который описывает всю цепочку вызовов между сервисами.

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

Подопытный проект

Для примера используются три сервиса и очередь, упакованные в docker контейнеры.

Исходная архитектура тестового проекта
Исходная архитектура тестового проекта
  • task_receiver – получает от пользователя 2 числа и создает задачу для worker в очереди 

  • worker – получает задачу из очереди, делит одно число на другое, создает задачу в очереди для printer

  • printer – получает задачу из очереди и выводит результат в консоль

Интеграция Jaeger

Добавляем необходимые контейнеры:

  • Jaeger – содержит сам Jaeger

  • Elasticksearch – хранилище спанов

  • Prometheus – сервис сбора и хранения метрик

  • spark_job – сервис построения графов зависимостей спанов (запускается периодически, например через cron)

Финальная архитектура тестового проекта
Финальная архитектура тестового проекта

Начиная с версии 2 все компоненты Jaeger упакованы в один бинарный файл.

Также понадобится хранилище спанов. Из коробки Jaeger дружит с Elasticsearch и Cassandra, но обещают расширить список. В примере используется Elasticsearch. 

Prometheus будет хранить метрики. Без этого в JaegerUI не будет работать вкладка Monitor.

Чтобы строить диаграммы зависимости сервисов необходимо периодически сканировать хранилище спанов и строить граф зависимостей. Для этого используется spark_job. Без построения графа зависимостей в JaegerUI не будет работать вкладка Dependencies.

Настройка

Настраивается Jaeger через конфиг в формате yaml.  Далее описаны основные параметры конфига

Сервис (service)

  • Extensions: используемые расширения, такие как jaeger_storage, jaeger_query и healthcheckv2.

  • Pipelines:

    Traces: Настраивает обработку трассировок с использованием приёмников, процессоров и экспортеров.

    Metrics/Spanmetrics: Настраивает обработку метрик спанов с использованием приёмника  и экспортера.

  • Telemetry:

    Resource: Указывает имя сервиса как jaeger.

    Metrics: Включает детализированные метрики для.

    Logs: Устанавливает уровень логирования.

Расширения (extensions)

  • healthcheckv2: Включает проверку состояния сервиса.

  • jaeger_query: Настраивает хранилища для трассировок и метрик. Определяет параметры UI и gRPC/HTTP эндпоинты.

  • jaeger_storage: Настраивает хранилище трассировок с использованием Elasticsearch. Определяет параметры индексов (префикс, частота ротации, количество шардов и реплик). Настраивает Prometheus как хранилище метрик.

Коннекторы (connectors)

  • spanmetrics: Используется для обработки метрик спанов.

Приёмники (receivers)

  • otlp: Принимает трассировки через gRPC и HTTP.

  • jaeger: Принимает трассировки через протокол Thrift Compact.

Процессоры (processors)

  • batch: Обрабатывает трассировки пакетами для повышения производительности.

Экспортеры (exporters)

  • jaeger_storage_exporter: Экспортирует трассировки в Elasticsearch.

  • prometheus: Экспортирует метрики в Prometeus.

Также необходимо создать конфиг JaegerUI (jaeger_config_ui.json) и включить в нем Monitor (по умолчанию выключен). Prometeus тоже требует конфиг с указанными портами для сбора метрик. Все конфиги доступны в тестовом проекте по ссылке в конце статьи.

Код

Теперь необходимо добавить создание и обработку спанов в код сервисов проекта.

Создаём jaeger.py следующего содержания:

from jaeger_client import Config
from opentracing.scope_managers.contextvars import ContextVarsScopeManager
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.propagate import inject,extract
from opentelemetry.trace import Status, StatusCode


def init_otel_tracer(service_name='unnamed_service'):
    resource = Resource(attributes={
        "service.name": service_name
    })
    provider = TracerProvider(resource=resource)
    processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://jaeger:4317", insecure=True))
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)
    return trace.get_tracer(service_name)


def otel_tracer(tracer, span_name='unnamed_span'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            data = args[0]
            context = extract(data.get('jaeger_context', {}))
            with tracer.start_as_current_span(span_name, context=context) as span:
                service_name = trace.get_tracer_provider().resource.attributes.get("service.name")
                span.set_attribute('peer.service', service_name)
                span.set_attribute('component', span_name)
                span.set_attribute('jaeger.trace_id', str(span.context.trace_id))
                span.set_attribute('payload', str(args[0]))
                for key, value in kwargs.items():
                    span.set_attribute(f'kwargs.{key}', str(value))
                jaeger_context = {}
                inject(jaeger_context)
                args[0]['jaeger_context'] = jaeger_context
                return func(*args, **kwargs)
        return wrapper
    return decorator


def mark_span_as_error(exc):
    current_span = trace.get_current_span()
    current_span.set_status(Status(StatusCode.ERROR, str(exc)))
    current_span.set_attribute("error", True)
    current_span.set_attribute("error.message", str(exc))

Далее в коде каждого сервиса нужно инициализировать tracer, указав имя сервиса и декорировать функцию с помощью декоратора otel_tracer, указав имя функции.

Чтобы можно было построить зависимости между спанами нужно передавать контекст спана. В Jaeger есть методы extract и inject, которые работают с запросами, но в примере используется очередь Redis и контекст надо погружать в задания Redis, чтобы следующий сервис мог извлечь этот контекст и обновить его своими данными. Тогда спаны всех сервисов приобретут зависимости и будут отображаться в UI корректно (цепочкой). В примере это происходит в декораторе otel_tracer. При обработке возникающих ошибок спан тоже нужно помечать “ошибочным”. Для этого используем метод mark_span_as_error, передавая в него само исключение.

Например код сервиса worker выглядит следующим образом:

import redis
import json
from jaeger import init_otel_tracer, otel_tracer, mark_span_as_error
from opentelemetry.propagate import inject
from opentelemetry.trace import Status, StatusCode


r = redis.Redis(host='redis', port=6379, decode_responses=True)
tracer = init_otel_tracer(service_name='worker')


@otel_tracer(tracer, span_name='process_task')
def process_task(task):
    a = task["a"]
    b = task["b"]
    try:
        result = a / b
    except ZeroDivisionError as exc:
        result = "Error: Division by zero"
        mark_span_as_error(exc)
    task["result"] = result
    updated_task = json.dumps(task)
    print(f"Processed task: {a} / {b} = {result}")
    r.rpush("results", updated_task)

if name == '__main__':
    print('worker started')
    while True:
        task_json = r.blpop("tasks")[1]
        task = json.loads(task_json)
        process_task(task)

JaegerUI

По умолчанию вебинтерфейс Jaeger сидит на порту 16686 и доступ к нему никак и ничем не ограничен. Существует множество вариантов ограничения доступа. В данном примере ограничение доступа к вебинтерфейсу реализовано с помощью Flask и jwt. Приложение на Flask принимает входящие подключения и при отсутствии токена отправляет на страницу авторизации. Настройки вынесены в .env файл.

Заключение

Тема мониторинга в общем и трейсинга в частности очень широка и здесь можно было бы много еще написать о стандартах и тонкостях настройки. Цель этой статьи помочь сделать первые шаги в этой области, что позволит находить узкие места в работе микросервисной архитектуры, упростит разбор инцидентов и анализ взаимодействия сервисов между собой. Jaeger является надёжным и качественным решением задачи трейсинга распределённых систем.

Ссылки

Код тестового проекта

Jaeger

Немного скринов

Поиск рейсов
Поиск рейсов
Трейс детально
Трейс детально
Данные мониторинга
Данные мониторинга
Последовательность работы сервисов
Последовательность работы сервисов
Теги:
Хабы:
+1
Комментарии0

Публикации

Ближайшие события