В настоящее время нет недостатка во фреймворках для создания микросервисов на Java и Kotlin.
В статье рассматриваются следующие:
Название | Версия | Год первого релиза | Разработчик |
---|---|---|---|
Helidon SE | 1.1.1 | 2019 | Oracle |
Ktor | 1.2.1 | 2018 | JetBrains |
Micronaut | 1.1.3 | 2018 | Object Computing |
Spring Boot | 2.1.5 | 2014 | Pivotal |
На их основе созданы четыре сервиса, которые могут взаимодействовать друг с другом посредством HTTP API с использованием паттерна Service Discovery, реализованного с помощью Consul. Таким образом они формируют гетерогенную (на уровне фреймворков) микросервисную архитектуру (далее МСА):
Определим набор требований к каждому сервису:
- стек технологий:
- JDK 12;
- Kotlin;
- Gradle (Kotlin DSL);
- JUnit 5.
- функциональность (HTTP API):
GET /application-info{?request-to=some-service-name}
Возвращает некоторую базовую информация о микросервисе (название, фреймворк, год релиза фреймворка); при указании в параметре request-to названия одного из четырёх микросервисов к его HTTP API выполняется аналогичный запрос, возвращающий базовую информацию;GET /application-info/logo
Возвращает изображение.
- реализация:
- настройка с использованием конфигурационного файла;
- использование внедрения зависимостей;
- тесты, проверяющие работоспособность HTTP API.
- МСА:
- использование паттерна Service Discovery (регистрация в Consul, обращение к HTTP API другого микросервиса по его названию с использованием клиентской балансировки нагрузки);
- формирование артефакта uber-JAR.
Далее рассматривается реализация микросервиса на каждом из фреймворков и сравниваются параметры полученных приложений.
Helidon service
Каркас разработки был создан в Oracle для внутреннего использования, впоследствии став open-source’ным. Существует две модели разработки на основе этого фреймворка: Standard Edition (SE) и MicroProfile (MP). В обоих случаях сервис будет обычной Java SE программой. Подробнее о различиях можно узнать на этой странице.
Если коротко, то Helidon MP — это одна из реализаций Eclipse MicroProfile, что даёт возможность использования множества API, как ранее известных разработчикам на Java EE, так и более новых (Health Check, Metrics, Fault Tolerance и т. д.). В варианте Helidon SE разработчики руководствовались принципом “No magic”, что выражается, в частности, в меньшем количестве или полном отсутствии аннотаций, необходимых для создания приложения.
Для разработки микросервиса выбран Helidon SE. Помимо прочего в нём отсутствуют средства для реализации Dependency Injection, поэтому для внедрения зависимостей использован Koin. Далее приведён класс, содержащий main-метод. Для реализации Dependency Injection класс наследуется от KoinComponent. Сначала стартует Koin, далее инициализируются требуемые зависимости и вызывается метод startServer()
, где создаётся объект типа WebServer, которому предварительно передаётся конфигурация приложения и настройка роутинга; после старта приложение регистрируется в Consul:
object HelidonServiceApplication : KoinComponent {
@JvmStatic
fun main(args: Array<String>) {
val startTime = System.currentTimeMillis()
startKoin {
modules(koinModule)
}
val applicationInfoService: ApplicationInfoService by inject()
val consulClient: Consul by inject()
val applicationInfoProperties: ApplicationInfoProperties by inject()
val serviceName = applicationInfoProperties.name
startServer(applicationInfoService, consulClient, serviceName, startTime)
}
}
fun startServer(
applicationInfoService: ApplicationInfoService,
consulClient: Consul,
serviceName: String,
startTime: Long
): WebServer {
val serverConfig = ServerConfiguration.create(Config.create().get("webserver"))
val server: WebServer = WebServer
.builder(createRouting(applicationInfoService))
.config(serverConfig)
.build()
server.start().thenAccept { ws ->
val durationInMillis = System.currentTimeMillis() - startTime
log.info("Startup completed in $durationInMillis ms. Service running at: http://localhost:" + ws.port())
// register in Consul
consulClient.agentClient().register(createConsulRegistration(serviceName, ws.port()))
}
return server
}
Роутинг настраивается следующим образом:
private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder()
.register(JacksonSupport.create())
.get("/application-info", Handler { req, res ->
val requestTo: String? = req.queryParams()
.first("request-to")
.orElse(null)
res
.status(Http.ResponseStatus.create(200))
.send(applicationInfoService.get(requestTo))
})
.get("/application-info/logo", Handler { req, res ->
res.headers().contentType(MediaType.create("image", "png"))
res
.status(Http.ResponseStatus.create(200))
.send(applicationInfoService.getLogo())
})
.error(NotFoundException::class.java) { req, res, ex ->
log.error("NotFoundException:", ex)
res.status(Http.Status.BAD_REQUEST_400).send()
}
.error(Exception::class.java) { req, res, ex ->
log.error("Exception:", ex)
res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send()
}
.build()
В приложении используется конфиг в формате HOCON:
webserver {
port: 8081
}
application-info {
name: "helidon-service"
framework {
name: "Helidon SE"
release-year: 2019
}
}
Для конфигурирования возможно также использовать файлы в форматах JSON, YAML и properties (подробнее здесь).
Ktor service
Фреймворк написан на Kotlin. Новый проект можно создать несколькими способами: используя систему сборки, start.ktor.io или плагин к IntelliJ IDEA (подробнее здесь).
Как и в Helidon SE, в Ktor отсутствует DI “из коробки”, поэтому перед стартом сервера с помощью Koin осуществляется внедрение зависимостей:
val koinModule = module {
single { ApplicationInfoService(get(), get()) }
single { ApplicationInfoProperties() }
single { MicronautServiceClient(get()) }
single { Consul.builder().withUrl("https://localhost:8500").build() }
}
fun main(args: Array<String>) {
startKoin {
modules(koinModule)
}
val server = embeddedServer(Netty, commandLineEnvironment(args))
server.start(wait = true)
}
Необходимые приложению модули указываются в конфигурационном файле (возможно использование только формата HOCON; подробнее о конфигурировании Ktor-сервера здесь), содержимое которого представлено ниже:
ktor {
deployment {
host = localhost
port = 8082
watch = [io.heterogeneousmicroservices.ktorservice]
}
application {
modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module]
}
}
application-info {
name: "ktor-service"
framework {
name: "Ktor"
release-year: 2018
}
В Ktor и Koin используется термин “модуль”, обладающий при этом разными значениями. В Koin модуль — это аналог контекста приложения в Spring Framework. Модуль Ktor — это определённая пользователем функция, которая принимает объект типа Application и может осуществлять конфигурирование пайплайна, установку фич (features), регистрацию роутов, обработку
запросов и т. д.:
fun Application.module() {
val applicationInfoService: ApplicationInfoService by inject()
if (!isTest()) {
val consulClient: Consul by inject()
registerInConsul(applicationInfoService.get(null).name, consulClient)
}
install(DefaultHeaders)
install(Compression)
install(CallLogging)
install(ContentNegotiation) {
jackson {}
}
routing {
route("application-info") {
get {
val requestTo: String? = call.parameters["request-to"]
call.respond(applicationInfoService.get(requestTo))
}
static {
resource("/logo", "logo.png")
}
}
}
}
В этом фрагменте кода настраивается роутинг запросов, в частности, статический ресурс logo.png
. Ktor-сервис может содержать фичи. Фича — это функциональность, встраиваемая в пайплайн запрос-ответ (DefaultHeaders, Compression и другие в примере кода выше). Возможна реализация собственных фич, например, ниже приведён код, имплементирующий паттерн Service Discovery в сочетании с клиентской балансировкой нагрузки на основе алгоритма Round-robin:
class ConsulFeature(private val consulClient: Consul) {
class Config {
lateinit var consulClient: Consul
}
companion object Feature : HttpClientFeature<Config, ConsulFeature> {
var serviceInstanceIndex: Int = 0
override val key = AttributeKey<ConsulFeature>("ConsulFeature")
override fun prepare(block: Config.() -> Unit) = ConsulFeature(Config().apply(block).consulClient)
override fun install(feature: ConsulFeature, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.Render) {
val serviceName = context.url.host
val serviceInstances =
feature.consulClient.healthClient().getHealthyServiceInstances(serviceName).response
val selectedInstance = serviceInstances[serviceInstanceIndex]
context.url.apply {
host = selectedInstance.service.address
port = selectedInstance.service.port
}
serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size
}
}
}
}
Основная логика находится в методе install
: во время фазы запроса Render (которая выполняется перед фазой Send) сначала определяется название вызываемого сервиса, далее у consulClient
запрашивается список инстансов этого сервиса, после чего вызывается инстанс, определённый с помощью алгоритма Round-robin. Таким образом становится возможным следующий вызов:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking {
httpClient.get<ApplicationInfo>("http://$serviceName/application-info")
}
Micronaut service
Micronaut разрабатывается создателями фреймворка Grails и вдохновлён опытом построения сервисов с использованием Spring, Spring Boot и Grails. Фреймворк является полиглотом, поддерживая языки Java, Kotlin и Groovy; возможно, будет поддержка Scala. Внедрение зависимостей в Micronaut осуществляется на этапе компиляции, что приводит к меньшему потреблению памяти и более быстрому запуску приложения по сравнению со Spring Boot.
Main-класс имеет следующий вид:
object MicronautServiceApplication {
@JvmStatic
fun main(args: Array<String>) {
Micronaut.build()
.packages("io.heterogeneousmicroservices.micronautservice")
.mainClass(MicronautServiceApplication.javaClass)
.start()
}
}
Некоторые компоненты приложения на основе Micronaut похожи на свои аналоги в приложении на Spring Boot, например, ниже приведён код контроллера:
@Controller(
value = "/application-info",
consumes = [MediaType.APPLICATION_JSON],
produces = [MediaType.APPLICATION_JSON]
)
class ApplicationInfoController(
private val applicationInfoService: ApplicationInfoService
) {
@Get
fun get(requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo)
@Get("/logo", produces = [MediaType.IMAGE_PNG])
fun getLogo(): ByteArray = applicationInfoService.getLogo()
}
Поддержка Kotlin в Micronaut реализована на основе плагина компилятора kapt (подробнее здесь). Сборочный скрипт при этом конфигурируется так:
plugins {
...
kotlin("kapt")
...
}
dependencies {
kapt("io.micronaut:micronaut-inject-java")
...
kaptTest("io.micronaut:micronaut-inject-java")
...
}
Далее показано содержимое конфигурационного файла:
micronaut:
application:
name: micronaut-service
server:
port: 8083
consul:
client:
registration:
enabled: true
application-info:
name: ${micronaut.application.name}
framework:
name: Micronaut
release-year: 2018
Конфигурирование микросервиса возможно также файлами форматов JSON, properties и Groovy (подробнее здесь).
Spring Boot service
Фреймворк был создан с целью упростить разработку приложений, использующих экосистему Spring Framework. Это достигается посредством механизмов автоконфигурации при подключении библиотек. Ниже приведён код контроллера:
@RestController
@RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_UTF8_VALUE])
class ApplicationInfoController(
private val applicationInfoService: ApplicationInfoService
) {
@GetMapping
fun get(@RequestParam("request-to") requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo)
@GetMapping(path = ["/logo"], produces = [MediaType.IMAGE_PNG_VALUE])
fun getLogo(): ByteArray = applicationInfoService.getLogo()
}
Микросервис конфигурируется файлом формата YAML:
spring:
application:
name: spring-boot-service
server:
port: 8084
application-info:
name: ${spring.application.name}
framework:
name: Spring Boot
release-year: 2014
Также для конфигурирования возможно использовать файлы формата properties (подробнее здесь).
Запуск
Проект работает на JDK 12, хотя, вероятно, и на 11-й версии тоже, требуется только соответствующим образом поменять в сборочных скриптах параметр jvmTarget
:
withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "12"
...
}
}
Перед запуском микросервисов нужно установить Consul и запустить агент — например, так: consul agent -dev
.
Запуск микросервисов возможен из:
- IDE
Пользователи IntelliJ IDEA могут увидеть примерно следующее:
- консоли
Для этого нужно перейти в папку проекта и последовательно выполнить:java -jar helidon-service/build/libs/helidon-service-all.jar java -jar ktor-service/build/libs/ktor-service-all.jar java -jar micronaut-service/build/libs/micronaut-service-all.jar java -jar spring-boot-service/build/libs/spring-boot-service.jar
После старта всех микросервисов на http://localhost:8500/ui/dc1/services
вы увидите:
Тестирование API
В качестве примера приведены результаты тестирования API Helidon service:
GET http://localhost:8081/application-info
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": null }
GET http://localhost:8081/application-info?requestTo=ktor-service
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": { "name": "ktor-service", "framework": { "name": "Ktor", "releaseYear": 2018 }, "requestedService": null } }
GET http://localhost:8081/application-info/logo
Возвращает изображение.
Протестировать API произвольного микросервиса можно с помощью Postman (коллекция запросов), IntelliJ IDEA HTTP client (коллекция запросов), браузера или другого инструмента. В случае использования первых двух клиентов требуется указать порт вызываемого микросервиса в соответствующей переменной (в Postman она находится в меню коллекции -> Edit -> Variables, а в HTTP Client — в переменной среды, указываемой в этом файле), а при тестировании метода 2) API также нужно указать название запрашиваемого “под капотом” микросервиса. Ответы при этом будут аналогичны приведённым выше.
Сравнение параметров приложений
Размер артефакта
C целью сохранения простоты настройки и запуска приложений в сборочных скриптах не были исключены какие-либо транзитивные зависимости, поэтому размер uber-JAR сервиса на Spring Boot значительно превышает размеры аналогов на других фреймворках (т. к. при использовании стартеров импортируются не только нужные зависимости; при желании размер можно существенно уменьшить):
Микросервис | Размер артефакта, Мбайт |
---|---|
Helidon service | 16,6 |
Ktor service | 20,9 |
Micronaut service | 16,5 |
Spring Boot service | 42,7 |
Время запуска
Время запуска каждого приложения непостоянно и попадает в некоторое “окно”; в таблице ниже приведено время запуска артефакта без указания каких-либо дополнительных параметров:
Микросервис | Время запуска, секунды |
---|---|
Helidon service | 2,2 |
Ktor service | 1,4 |
Micronaut service | 4,0 |
Spring Boot service | 10,2 |
Стоит отметить, что если “почистить” приложение на Spring Boot от ненужных зависимостей и уделить внимание настройке запуска приложения (например, сканировать только нужные пакеты и использовать ленивую инициализацию бинов), то можно значительно сократить время запуска.
Нагрузочное тестирование
Для проведения тестирования были использованы Gatling и скрипт на Scala. Генератор нагрузки и тестируемый сервис были запущены на одной машине (Windows 10, четырёхъядерный процессор 3,2 ГГц, 24 Гбайт RAM, SSD). Порт этого сервиса указывается в Scala-скрипте.
Для каждого микросервиса определяется:
- минимальный объём heap-памяти (
-Xmx
), необходимый для запуска работоспособного (отвечающего на запросы) микросервиса - минимальный объём heap-памяти, необходимый для прохождения нагрузочного теста 50 пользователей * 1000 запросов
- минимальный объём heap-памяти, необходимый для прохождения нагрузочного теста 500 пользователей * 1000 запросов
Под прохождением нагрузочного теста понимается то, что микросервис ответил на все запросы за любое время.
Микросервис | Минимальный объём heap-памяти, Мбайт | ||
---|---|---|---|
Для запуска сервиса | Для нагрузки 50 * 1000 |
Для нагрузки 500 * 1000 |
|
Helidon service | 9 | 9 | 11 |
Ktor service | 11 | 11 | 13 |
Micronaut service | 13 | 13 | 17 |
Spring Boot service | 22 | 23 | 25 |
Стоит заметить, что все микросервисы используют HTTP-сервер Netty.
Заключение
Поставленную задачу — создание простого сервиса с HTTP API и возможностью функционировать в МСА — удалось выполнить на всех рассматриваемых фреймворках. Пришло время подвести итоги и рассмотреть их плюсы и минусы.
Helidоn
Standard Edition
- плюсы
- параметры приложения
По всем параметрам показал хорошие результаты и отлично справился с нагрузочным тестированием; - “no magic”
Фреймворк оправдал заявленный разработчиками принцип: для создания приложения потребовалась всего одна аннотация (@JvmStatic
— для интеропа Java-Kotlin).
- параметры приложения
- минусы
- микрофреймворк
Отсутствуют “из коробки” некоторые необходимые для промышленной разработки компоненты, например, внедрение зависимостей и реализация Service Discovery.
- микрофреймворк
MicroProfile
Микросервис на этом фреймворке реализован не был, поэтому отмечу лишь пару известных мне пунктов:
- плюсы
- имплементация Eclipse MicroProfile
По сути, MicroProfile — это Java EE, оптимизированная для МСА. Таким образом, во-первых, вы получаете доступ ко всему многообразию Java EE API, в том числе, разработанному специально для МСА, во-вторых, вы можете изменить имплементацию MicroProfile на любую другую (Open Liberty, WildFly Swarm и т. д.).
- имплементация Eclipse MicroProfile
- дополнительно
- на MicroProfile Starter вы можете с нуля создать проект с нужными параметрами по аналогии с похожими инструментами для других фреймворков (например, Spring Initializr). На момент написания статьи Helidon реализует MicroProfile 1.2, тогда как последняя версия спецификации — 2.2.
Ktor
- плюсы
- легковесность
Позволяет подключать только те функции, которые непосредственно нужны для выполнения поставленной задачи;
- параметры приложения
Хорошие результаты по всем параметрам.
- легковесность
- минусы
- “заточен” под Kotlin, то есть, разрабатывать на Java, вроде, можно, но не нужно;
- микрофреймворк (см. аналогичный пункт для Helidon SE).
- дополнительно
С одной стороны, концепция разработки на фреймворке не входит в две наиболее популярных модели разработки на Java (Spring-подобную (Spring Boot/Micronaut) и Java EE/MicroProfile), что может привести к:- проблеме с поиском специалистов;
- увеличению времени на выполнение задач по сравнению со Spring Boot из-за необходимости явного конфигурирования требуемой функциональности.
С другой, непохожесть на “классические” Spring и Java EE позволяет взглянуть на процесс разработки под другим углом, возможно, более осознанно.
Micronaut
- плюсы
- AOT
Как ранее было отмечено, AOT позволяет уменьшить время старта и потребляемую приложением память по сравнению с аналогом на Spring Boot; - Spring-подобная модель разработки
У программистов с опытом разработки на Spring не займёт много времени освоение этого фреймворка; - проект Micronaut for Spring позволяет в том числе изменить среду выполнения имеющегося Spring Boot приложения на Micronaut (с ограничениями);
- параметры приложения
Хорошие результаты по всем параметрам; - полиглот
Поддержка на уровне first-class citizen языков Java, Kotlin, Groovy; возможно, будет поддержка Scala. На мой взгляд, это может положительно повлиять на рост сообщества. К слову, на июнь 2019 Groovy в рейтинге популярности языков программирования TIOBE занимает 14-е место, взлетев с 60-го годом ранее, таким образом, находясь на почётном втором месте среди JVM-языков.
- AOT
Spring Boot
- плюсы
- зрелость платформы и экосистема
Фреймворк “на каждый день”. Для бОльшей части повседневных задач уже есть решение в парадигме программирования Spring, т. е. привычным для многих программистов способом. Разработку упрощают концепции стартеров и автоконфигураций; - наличие большого количества специалистов на рынке труда, а также значительная база знаний (включая документацию и ответы на Stack Overflow);
- перспектива
Думаю, многие согласятся, что в ближайшем будущем Spring останется лидирующим каркасом разработки.
- зрелость платформы и экосистема
- минусы
- параметры приложения
Приложение на этом фреймворке не было в числе лидеров, однако некоторые параметры, как было отмечено ранее, могут быть оптимизированы самостоятельно. Также стоит вспомнить о наличии находящегося в активной разработке проекта Spring Fu, использование которого позволяет уменьшить эти параметры.
- параметры приложения
Также можно выделить общие проблемы, связанные с новыми фреймворками и отсутствующие у Spring Boot:
- менее развитая экосистема;
- малое количество специалистов с опытом работы с этими технологиями;
- большее время выполнения задач;
- неясные перспективы.
Рассмотренные фреймворки принадлежат к разным весовым категориям: Helidon SE и Ktor — это микрофреймворки, Spring Boot — full-stack фреймворк, Micronaut, скорее, тоже full-stack; ещё одна категория — MicroProfile (например, Helidon MP). В микрофреймворках функциональность ограничена, что может замедлить выполнение задач; для уточнения возможности реализации той или иной функциональности на основе какого-либо каркаса разработки рекомендую ознакомиться с его документацией.
Не берусь судить о том, “выстрелит” ли тот или иной фреймворк в ближайшем будущем, поэтому, на мой взгляд, пока лучше продолжить наблюдать за развитием событий, используя имеющийся каркас разработки для решения рабочих задач.
В то же время, как было показано в статье, новые фреймворки выигрывают у Spring Boot по рассмотренным параметрам полученных приложений. Если для какого-то из ваших микросервисов критически важны какие-либо из этих параметров, то, возможно, стоит обратить внимание на фреймворки, показавшие по ним лучшие результаты. Однако, не стоит забывать, что Spring Boot, во-первых, продолжает совершенствоваться, во-вторых, имеет огромную экосистему и с ним знакомы значительное количество Java-программистов. Есть и другие фреймворки, не освещённые в настоящей статье: Javalin, Quarkus и т. д.
С кодом проекта вы можете ознакомиться на GitHub. Благодарю за внимание!
P.S.: Спасибо artglorin за помощь в подготовке статьи.
Автор: Roman Kudryashov