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