- PVSM.RU - https://www.pvsm.ru -

Jaeger v2

Появилась у меня задача по мониторингу и оценке производительности проекта на микросервисной архитектуре. Для решения был выбран 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 [1] следующего содержания:

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 является надёжным и качественным решением задачи трейсинга распределённых систем.

Ссылки

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

Jaeger [3]

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

Поиск рейсов

Поиск рейсов
Трейс детально

Трейс детально
Данные мониторинга

Данные мониторинга
Последовательность работы сервисов

Последовательность работы сервисов

Автор: not_sure

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/423960

Ссылки в тексте:

[1] jaeger.py: http://jaeger.py

[2] Код тестового проекта : https://github.com/zrc198/Jaeger_redis

[3] Jaeger : https://www.jaegertracing.io

[4] Источник: https://habr.com/ru/articles/923212/?utm_source=habrahabr&utm_medium=rss&utm_campaign=923212