paint-brush
Наблюдаемость Java Backend с помощью трассировок OpenTelemetry и минимального кодак@apanasevich
292 чтения

Наблюдаемость Java Backend с помощью трассировок OpenTelemetry и минимального кода

к Dmitriy Apanasevich10m2024/11/15
Read on Terminal Reader
Read this story w/o Javascript

Слишком долго; Читать

Как мы интегрировали фреймворк OpenTelemetry в наш Java-бэкэнд, получив трассировку с минимальным кодированием.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Наблюдаемость Java Backend с помощью трассировок OpenTelemetry и минимального кода
Dmitriy Apanasevich HackerNoon profile picture

Всем привет! Я Дмитрий Апанасевич, разработчик Java в MY.GAMES, работаю над игрой Rush Royale, и хотел бы поделиться нашим опытом интеграции фреймворка OpenTelemetry в наш Java-бэкенд. Здесь есть что рассказать: мы рассмотрим необходимые изменения кода, необходимые для его внедрения, а также новые компоненты, которые нам нужно было установить и настроить, и, конечно же, поделимся некоторыми нашими результатами.

Наша цель: достижение наблюдаемости системы

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


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



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


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


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


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


Но менее распространенным инструментом для получения и анализа такого рода системных данных являются следы .


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


В этом обсуждении мы сосредоточимся на аспекте трассировки OpenTelemetry.

Еще немного информации об OpenTelemetry

Давайте также прольем свет на проект OpenTelemetry, который появился в результате слияния OpenTracing и OpenCensus проекты.


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


При этом OpenTelemetry не предлагает бэкэнд для хранения данных или инструментов визуализации.


Поскольку нас интересовала только трассировка, мы изучили наиболее популярные решения с открытым исходным кодом для хранения и визуализации трассировок:

  • Егерь
  • Зипкин
  • Графана Темп


В конечном итоге мы выбрали Grafana Tempo из-за его впечатляющих возможностей визуализации, быстрого темпа разработки и интеграции с нашей существующей настройкой Grafana для визуализации метрик. Наличие единого, унифицированного инструмента также было существенным преимуществом.

Компоненты OpenTelemetry

Давайте также немного разберем компоненты OpenTelemetry.


Спецификация:

  • API — типы данных, операции, перечисления

  • SDK — реализация спецификации, API на разных языках программирования. Другой язык означает другое состояние SDK, от альфа до стабильного.

  • Протокол данных (OTLP) и семантические соглашения


Java API SDK:

  • Библиотеки инструментирования кода
  • Экспортеры — инструменты для экспорта сгенерированных трассировок в бэкэнд
  • Cross Service Propagators — инструмент для передачи контекста выполнения за пределы процесса (JVM)


Коллектор OpenTelemetry — важный компонент, прокси-сервер, который получает данные, обрабатывает их и передает дальше — давайте рассмотрим его подробнее.

Сборщик OpenTelemetry

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


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

Вот два основных метода отбора проб, которые используются для определения того, какие трассы следует сохранить, а какие — отбросить:

  • Отбор проб — в начале трассировки принимается решение, сохранять ее или нет
  • Выборка хвоста — принимает решение только после того, как будет доступна полная трасса. Это необходимо, когда решение зависит от данных, которые появляются позже в трассе. Например, данные, включающие интервалы ошибок. Эти случаи не могут быть обработаны выборкой головы, поскольку они требуют сначала анализа всей трассы


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

Инструментарий с нулевым кодом

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


-javaagent:/opentelemetry-javaagent-1.29.0.jar

-Dotel.javaagent.configuration-file=/otel-config.properties


OpenTelemetry поддерживает огромное количество библиотеки и фреймворки , поэтому после запуска приложения с агентом мы сразу получали трейсы с данными по этапам обработки запросов между сервисами, в СУБД и т. д.


В конфигурации нашего агента мы отключили используемые нами библиотеки, чьи области мы не хотели видеть в трассировках, и чтобы получить данные о том, как работает наш код, мы пометили его как аннотации :


 @WithSpan("acquire locks") public CompletableFuture<Lock> acquire(SortedSet<Object> source) { var traceLocks = source.stream().map(Object::toString).collect(joining(", ")); Span.current().setAttribute("locks", traceLocks); return CompletableFuture.supplyAsync(() -> /* async job */); }


В этом примере для метода используется аннотация @WithSpan , которая сигнализирует о необходимости создания нового диапазона с именем « acquire locks », а атрибут « locks » добавляется к созданному диапазону в теле метода.


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

Наша настройка сбора следов

Теперь поговорим о том, как настроить всю систему сбора трассировок. Все наши приложения JVM запускаются с помощью Java-агента, который отправляет данные в коллектор OpenTelemetry.


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


Здесь, а коллектор настроен как на помощь приходит балансир.


В итоге получаем следующую систему: Каждое JVM-приложение отправляет данные одному и тому же сборщику-балансировщику, единственная задача которого — распределить данные, полученные от разных приложений, но относящиеся к заданной трассе, по одному и тому же сборщику-процессору. Затем сборщик-процессор отправляет данные в Grafana Tempo.



Давайте подробнее рассмотрим конфигурацию компонентов этой системы.

Коллектор балансировки нагрузки

В конфигурации коллектор-балансир мы сконфигурировали следующие основные части:


 receivers:  otlp:    protocols:      grpc: exporters:  loadbalancing:    protocol:      otlp:        tls:          insecure: true    resolver:      static:        hostnames:          - collector-1.example.com:4317          - collector-2.example.com:4317          - collector-3.example.com:4317 service:  pipelines:    traces:      receivers: [otlp]      exporters: [loadbalancing]


  • Receivers — где настраиваются методы (через которые данные могут быть получены сборщиком). Мы настроили прием данных исключительно в формате OTLP. (Возможна настройка приема данных через многие другие протоколы (например, Зипкин, Йегер.)
  • Экспортеры — часть конфигурации, в которой настраивается балансировка данных. Среди указанных в этом разделе сборщиков-обработчиков данные распределяются в зависимости от хеша, вычисленного из идентификатора трассировки.
  • В разделе «Сервис» указывается конфигурация работы сервиса: только с трассировками, с использованием настроенного сверху приемника OTLP и передачей данных в качестве балансировщика, т.е. без обработки.

Сборщик с обработкой данных

Конфигурация сборщиков-процессоров более сложная, поэтому давайте рассмотрим ее:


 receivers:  otlp:    protocols:      grpc:        endpoint: 0.0.0.0:14317 processors:  tail_sampling:    decision_wait: 10s    num_traces: 100    expected_new_traces_per_sec: 10    policies:      [          {            name: latency500-policy,            type: latency,            latency: {threshold_ms: 500}          },          {            name: error-policy,            type: string_attribute,            string_attribute: {key: error, values: [true, True]}          },          {            name: probabilistic10-policy,            type: probabilistic,            probabilistic: {sampling_percentage: 10}          }      ]  resource/delete:    attributes:      - key: process.command_line        action: delete      - key: process.executable.path        action: delete      - key: process.pid        action: delete      - key: process.runtime.description        action: delete      - key: process.runtime.name        action: delete      - key: process.runtime.version        action: delete exporters:  otlp:    endpoint: tempo:4317    tls:      insecure: true service:  pipelines:    traces:      receivers: [otlp]      exporters: [otlp]


Подобно конфигурации коллектор-балансир, конфигурация обработки состоит из разделов Receivers, Exporters и Service. Однако мы сосредоточимся на разделе Processors, который объясняет, как обрабатываются данные.


Во-первых, раздел tail_sampling демонстрирует конфигурация что позволяет фильтровать данные, необходимые для хранения и анализа:


  • latency500-policy : это правило выбирает трассировки с задержкой, превышающей 500 миллисекунд.

  • error-policy : это правило выбирает трассировки, в которых возникли ошибки во время обработки. Оно ищет атрибут строки с именем "error" со значениями "true" или "True" в диапазонах трассировки.

  • probabilistic10-policy : это правило случайным образом выбирает 10% всех трассировок, чтобы предоставить информацию о нормальной работе приложения, ошибках и длительной обработке запросов.


Помимо tail_sampling, в этом примере показан раздел resource/delete для удаления ненужных атрибутов, не требуемых для анализа и хранения данных.

Результаты

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


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


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


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



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



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


Подведение итогов

Как мы увидели, работа с трассировкой OpenTelemetry значительно улучшила наши возможности наблюдения. С минимальными изменениями кода и хорошо структурированной настройкой коллектора мы получили глубокие знания – плюс мы увидели, как возможности визуализации Grafana Tempo дополнительно дополнили нашу настройку. Спасибо за чтение!