Как сократить потребление памяти в интеграционных тестах с Kafka с помощью GraalVM
В данной статье я хочу поделиться своим опытом создания нативного образа для EmbeddedKafka с использованием GraalVM. Использование такого образа в интеграционных тестах позволяет увеличить скорость запуска тестовых сценариев и сократить объем потребляемой памяти. Интересно отметить, что в сравнении с использованием confluentinc/cp-kafka
в Testcontainers, разница в скорости и потреблении памяти оказывается заметной — и не в пользу последнего.
EmbeddedKafka, Testcontainers, GraalVM
Кратко о ключевых компонентах, использованных в проекте.
EmbeddedKafka — это инструмент, который позволяет встроить Kafka-сервер непосредственно в JVM-приложение или тестовую среду. Это полезно для интеграционного тестирования приложений, использующих Apache Kafka для обработки потоков данных или в качестве системы обмена сообщениями. Embedded Kafka преимущественно используется для изолированного тестирования взаимодействия с Kafka, упрощая настройку и управление тестами за счет быстрого запуска и остановки. Это обеспечивает воспроизводимость тестов в различных средах и предоставляет контроль над конфигурацией Kafka. Однако, работа Embedded Kafka в JVM-приложении увеличивает потребление памяти из-за ресурсоемкости Kafka и необходимости хранения данных. Это компромисс между удобством разработки и дополнительной нагрузкой на память, делая внешний (по отношению к JVM-приложению) Kafka-сервер более предпочтительным для производственной среды или при ограниченных ресурсах памяти.
Testcontainers — это фреймворк, который используется для поддержки автоматизированных интеграционных тестов с использованием Docker-контейнеров. Он позволяет создавать, управлять и удалять контейнеры во время выполнения тестов. Использование Docker-контейнеров обеспечивает согласованность тестовой среды на различных машинах и платформах, что упрощает локальную разработку и тестирование. Использование Kafka в Testcontainers по сравнению с Embedded Kafka предлагает ряд преимуществ, особенно когда дело касается тестирования в более реалистичной и гибкой среде. Testcontainers запускает экземпляры Kafka на основе официальных образов Confluent OSS Platform.
GraalVM — это платформа для выполнения программ, поддерживающая различные языки программирования и технологии. Она, кроме всего прочего, позволяет компилировать Java-приложения в статически связанные исполняемые файлы (native binaries). Эти нативные исполнимые файлы запускаются быстрее, требуют меньше памяти и не требуют установленной JVM. Недавняя статья по этой теме — Как скомпилировать Spring Boot приложение в native image с помощью GraalVm и развернуть его с помощью Docker.
Тестовый сценарий
Для иллюстрации подходов к написанию тестов я подготовил примеры кода, соответствующие простому тестовому сценарию:
отправляем сообщение
value1
в топикtopic1
;читаем сообщение из топика
topic1
;проверяем значение, оно должно быть равно
value1
.
Примеры можно найти в репозитории проекта:
Заворачиваем EmbeddedKafka в контейнер
Первым делом нужно было реализовать запуск EmbeddedKafka в рамках отдельного контейнера. Для этого я пошел прямым путем:
описал стандартное Spring Boot приложение, используя https://start.spring.io;
в приложении описал запуск экземпляра класса
org.springframework.kafka.test.EmbeddedKafkaZKBroker
с передачей необходимых параметров;описал Dockerfile и собрал образ.
Все описанные выше действия находят свое отражение в коде модуля emk-application в репозитории проекта.
Запускаем EmbeddedKafka в контейнере
В документации Testcontainers представлено руководство к запуску контейнера с Kafka через использование класса KafkaContainer
следующим образом:
KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1"))
Данный класс мне не подошел, так как он предназначен для использования с совместимыми образами confluentinc/cp-kafka
, но знакомство с ним было полезно — в нем можно увидеть любопытную логику вокруг передачи параметра KAFKA_ADVERTISED_LISTENERS:
На старте контейнера осуществляется подмена инструкций ENTRYPOINT/COMMAND.
После старта идет передача в контейнер параметров KAFKA_ADVERTISED_LISTENERS и инструкции по запуску Kafka.
Это подробно описано на прилагаемой схеме.
Зачем это нужно? В процессе работы клиент может обратиться к любому узлу Kafka для получения адреса, по которому необходимо выполнять операции записи/чтения, даже если Kafka представлена одним узлом. Для внешнего пользователя необходим внешний адрес, а для внутреннего — соответственно, внутренний. Указывая KAFKA_ADVERTISED_LISTENERS, мы предоставляем брокеру информацию о его внешнем адресе, которую брокер затем передает клиенту. Клиенты будут внешними по отношению к брокеру, так как брокер запущен в контейнере.
Я реализовал описанную выше логику в новом классе — EmbeddedKafkaContainer.java.
Создаем нативный образ EmbeddedKafka
Самый простой способ начать новый проект на Spring Boot для GraalVM — это перейти на сайт start.spring.io, добавить зависимость «GraalVM Native Support» и сгенерировать проект. В комплекте с проектом поставляется файл HELP.md, который предоставляет полезные подсказки для начала работы.
Сбор меты
Инструмент сборки нативного образа зависит от статического анализа, доступного во время выполнения кода приложения. Однако этот анализ не всегда способен полностью предсказать все случаи использования Java Native Interface (JNI), рефлексии Java, динамических прокси-объектов и т.д. Следовательно, случаи использования этих динамических функций должны быть явно указаны инструменту сборки нативного образа в виде метаданных. Один из способов предоставления таких метаданных — это JSON файлы, размещенные в директории проекта META-INF/native-image/
.
GraalVM предлагает Tracing Agent для удобного сбора метаданных и подготовки файлов конфигурации. Этот агент отслеживает все случаи использования динамических функций во время выполнения приложения на стандартной Java VM.
Мой подход был следующим:
запустил экземпляр Spring приложения с Embedded Kafka под JVM с Tracing Agent.
прогнал большой набор тестов из одного из моих проектов, используя запущенное приложение в качестве основного брокера Kafka.
Полученные в ходе этого процесса файлы были размещены в директории проекта META-INF/native-image.
Запуск и использование
Для демонстрации результата я подготовил следующие артефакты:
Библиотека с классом
EmbeddedKafkaContainer
—pw.avvero:emk-testcontainers:1.0.0
.Docker образы:
avvero/emk
(JVM) иavvero/emk-native
(native, platform=linux/arm64).Пример использования, соответствующий тестовому сценарию, можно найти в модуле example-embedded-kafka-container.
Конфигурация KafkaContainerConfiguration выглядит следующим образом:
@TestConfiguration(proxyBeanMethods = false)
public class KafkaContainerConfiguration {
@Bean
@RestartScope
@ServiceConnection
EmbeddedKafkaContainer kafkaContainer() {
return new EmbeddedKafkaContainer("avvero/emk-native:1.0.0");
}
}
При локальном запуске сценариев в различных конфигурациях, я использовал инструмент мониторинга docker stats
для наблюдения за потреблением памяти. На основе этих наблюдений я заметил следующие тенденции по потреблению памяти:
confluentinc/cp-kafka:5.4.3 | 1.264GiB |
avvero/emk | 677.3MiB |
avvero/emk-native | 126.45MiB |
confluentinc/cp-kafka:7.3.3 | 1.331GiB |
Время запуска также различается, но это становится значимым только в случаях частого перезапуска контейнеров.
Заключение
Использование нативного образа для EmbeddedKafka с GraalVM в интеграционных тестах ускоряет время запуска тестов и снижает потребление памяти, что делает его эффективным решением по сравнению с традиционными методами, например, использованием confluentinc/cp-kafka
в Testcontainers.
Использованием GraalVM открывает новые возможности для разработчиков, стремящихся улучшить производительность и эффективность интеграционных тестов. Этот подход может быть адаптирован и расширен для других подобных задач и технологий, подчеркивая его универсальность и потенциал в области разработки программного обеспечения.
Ссылка на репозиторий проект с демонстрацией тестов — https://github.com/avvero/embedded-kafka.
Спасибо за внимание к статье, и удачи в вашем стремлении к написанию эффективных и быстрых тестов!