Pull to refresh
74.77
Райффайзен Банк
Развеиваем мифы об IT в банках

Не Spring Boot’ом единым: обзор альтернатив

Reading time13 min
Views26K


В настоящее время нет недостатка во фреймворках для создания микросервисов на 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 (например, JAX-RS, CDI), так и более новых (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(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 { ServiceClient(get()) }
   single { Consul.builder().withUrl("http://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. Внедрение зависимостей осуществляется на этапе компиляции, что приводит к меньшему потреблению памяти и более быстрому запуску приложения по сравнению со 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:

  1. GET http://localhost:8081/application-info

    {
      "name": "helidon-service",
      "framework": {
    	"name": "Helidon SE",
    	"releaseYear": 2019
      },
      "requestedService": null
    }
  2. GET http://localhost:8081/application-info?request-to=ktor-service

    {
      "name": "helidon-service",
      "framework": {
    	"name": "Helidon SE",
    	"releaseYear": 2019
      },
      "requestedService": {
    	"name": "ktor-service",
    	"framework": {
    		"name": "Ktor",
      		"releaseYear": 2018
    	},
    	"requestedService": null
      }
    }
  3. 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 и т. д.).
  • дополнительно
    • на MicroProfile Starter вы можете с нуля создать проект с нужными параметрами по аналогии с похожими инструментами для других фреймворков (например, Spring Initializr). На момент публикации статьи Helidon реализует MicroProfile 1.2, тогда как последняя версия спецификации — 3.0.


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 не займёт много времени освоение этого фреймворка;
    • параметры приложения
      Хорошие результаты по всем параметрам;
    • полиглот
      Поддержка на уровне first-class citizen языков Java, Kotlin, Groovy; возможно, будет поддержка Scala. На мой взгляд, это может положительно повлиять на рост сообщества. К слову, на июнь 2019 Groovy в рейтинге популярности языков программирования TIOBE занимает 14-е место, взлетев с 60-го годом ранее, таким образом, находясь на почётном втором месте среди JVM-языков;
    • проект Micronaut for Spring позволяет в том числе изменить среду выполнения имеющегося Spring Boot приложения на Micronaut (с ограничениями).


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 за помощь в подготовке статьи.
Tags:
Hubs:
+22
Comments14

Articles

Information

Website
www.raiffeisen.ru
Registered
Founded
1996
Employees
5,001–10,000 employees
Location
Россия