Всем привет! Я Дмитрий Апанасевич, разработчик Java в MY.GAMES, работаю над игрой Rush Royale, и хотел бы поделиться нашим опытом интеграции фреймворка OpenTelemetry в наш Java-бэкенд. Здесь есть что рассказать: мы рассмотрим необходимые изменения кода, необходимые для его внедрения, а также новые компоненты, которые нам нужно было установить и настроить, и, конечно же, поделимся некоторыми нашими результатами.
Наша цель: достижение наблюдаемости системы
Давайте дадим больше контекста нашему случаю. Как разработчики, мы хотим создать программное обеспечение, которое легко контролировать, оценивать и понимать (и это как раз и есть цель внедрения OpenTelemetry — максимизировать системные
Традиционные методы сбора данных о производительности приложений часто подразумевают ручную регистрацию событий, показателей и ошибок:
Конечно, существует множество фреймворков, позволяющих нам работать с логами, и я уверен, что у каждого, читающего эту статью, есть настроенная система для сбора, хранения и анализа логов.
Логирование также было полностью настроено для нас, поэтому мы не использовали возможности, предоставляемые OpenTelemetry для работы с логами.
Другим распространенным способом мониторинга системы является использование показателей:
У нас также была полностью настроенная система сбора и визуализации метрик, поэтому и здесь мы проигнорировали возможности OpenTelemetry в части работы с метриками.
Но менее распространенным инструментом для получения и анализа такого рода системных данных являются
Трассировка представляет собой путь, который запрос проходит через нашу систему в течение своего жизненного цикла, и обычно начинается, когда система получает запрос, и заканчивается ответом. Трассировки состоят из нескольких
В этом обсуждении мы сосредоточимся на аспекте трассировки OpenTelemetry.
Еще немного информации об OpenTelemetry
Давайте также прольем свет на проект OpenTelemetry, который появился в результате слияния
Теперь 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 дополнительно дополнили нашу настройку. Спасибо за чтение!