📜 ⬆️ ⬇️

Not a single spring boot: review alternatives



Currently, there is no shortage of frameworks for creating microservices in Java and Kotlin.

The article discusses the following:
TitleVersionYear of first releaseDeveloper
Helidon SE1.1.12019Oracle
Ktor1.2.12018JetBrains
Micronaut1.1.32018Object Computing
Spring boot2.1.52014Pivotal

Based on them, four services have been created that can interact with each other using the HTTP API using the Service Discovery pattern implemented using Consul . Thus, they form a heterogeneous (at the level of frameworks) microservice architecture (hereafter referred to as ISA):
')


Define a set of requirements for each service:


Next, we consider the implementation of microservice on each of the frameworks and compare the parameters of the resulting applications.

Helidon service


The development framework was created in Oracle for internal use, subsequently becoming open-source. There are two development models based on this framework: Standard Edition (SE) and MicroProfile (MP). In both cases, the service will be a regular Java SE program. Learn more about the differences on this page.

In short, Helidon MP is one of the implementations of Eclipse MicroProfile , which makes it possible to use multiple APIs, as previously known to Java EE developers (for example, JAX-RS, CDI), and newer ones (Health Check, Metrics, Fault Tolerance etc.). In the variant Helidon SE, the developers were guided by the principle of “No magic”, which is expressed, in particular, in the smaller number or complete absence of annotations necessary to create the application.

Helidon SE is selected for microservice development. Among other things, it lacks the means to implement Dependency Injection, so Koin was used to implement dependencies. The following is the class containing the main method. To implement Dependency Injection, the class is inherited from KoinComponent . First, Koin starts, then the required dependencies are initialized and the startServer() method is called, where an object of the WebServer type is created, to which the application configuration and routing setup are passed; after start the application is registered in 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 } 

Routing is configured as follows:

 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() 

The application uses the config in HOCON format:

 webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } } 

For configuration, it is also possible to use files in the formats JSON, YAML and properties (more here ).

Ktor service


The framework is written in Kotlin. A new project can be created in several ways: using the build system, start.ktor.io or the plug-in to IntelliJ IDEA (more here ).

As in Helidon SE, Ktor is missing the DI “out of the box”, so before starting the server using Koin, dependencies are introduced:

 val koinModule = module { single { ApplicationInfoService(get(), get()) } single { ApplicationInfoProperties() } single { MicronautServiceClient(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) } 

The modules necessary for the application are specified in the configuration file (only the HOCON format is possible; for more information on configuring the Ktor server here ), the contents of which are presented below:

 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 } 

In Ktor and Koin, the term “module” is used, with different meanings. In Koin, a module is an analogue of the application context in the Spring Framework. The Ktor module is a user-defined function that accepts an object of type Application and can configure pipeline, set features ( features ), register routes, process
requests and so on:

 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") } } } } 

This code snippet configures the routing of requests, in particular, the static resource logo.png . Ktor-service may contain features. A feature is a functionality that is embedded in the request-response pipeline ( DefaultHeaders, Compression, and others in the example code above). You can implement your own features, for example, below is the code implementing the Service Discovery pattern in combination with client load balancing based on the Round-robin algorithm:

 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 } } } } 

The main logic is in the install method: during the Render request phase (which runs before the Send phase), the name of the called service is first determined, then the consulClient queried for a list of instances of this service, after which the instance defined using the Round-robin algorithm is called. Thus, the following call becomes possible:

 fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get<ApplicationInfo>("http://$serviceName/application-info") } 

Micronaut service


Micronaut is developed by the creators of the Grails framework and is inspired by the experience of building services using Spring, Spring Boot and Grails. The framework is a polyglot, supporting the languages ​​of Java, Kotlin and Groovy; may be Scala support. Dependencies are introduced at compile time, which results in less memory consumption and faster application launch compared to Spring Boot.

The main class has the following form:

 object MicronautServiceApplication { @JvmStatic fun main(args: Array<String>) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice") .mainClass(MicronautServiceApplication.javaClass) .start() } } 

Some components of the application based on Micronaut are similar to their counterparts in the application on Spring Boot, for example, below is the controller code:

 @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 support in Micronaut is based on the kapt compiler plugin (more here ). The assembly script is configured as follows:

 plugins { ... kotlin("kapt") ... } dependencies { kapt("io.micronaut:micronaut-inject-java") ... kaptTest("io.micronaut:micronaut-inject-java") ... } 

The following shows the contents of the configuration file:

 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 

Configuring microservice is also possible with JSON, properties and Groovy file formats (more details here ).

Spring Boot service


The framework was created to simplify the development of applications using the Spring Framework ecosystem. This is achieved through auto-configuration mechanisms when libraries are connected. Below is the controller code:

 @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() } 

Microservice is configured with a YAML format file:

 spring: application: name: spring-boot-service server: port: 8084 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014 

It is also possible to use properties files for configuration (more here ).

Launch


The project runs on JDK 12, although, probably, on the 11th version, too, you only need to change the jvmTarget parameter in the jvmTarget scripts:

 withType<KotlinCompile> { kotlinOptions { jvmTarget = "12" ... } } 

Before launching microservices, you need to install Consul and start an agent - for example, like this: consul agent -dev .

Microservice launch is possible from:


After starting all microservices on http://localhost:8500/ui/dc1/services you will see:



API Testing


As an example, the results of testing the Helidon service API are given:

  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

    Returns an image.

You can test an arbitrary microservice API using Postman ( collection of requests), IntelliJ IDEA HTTP client ( collection of requests), browser or another tool. In the case of using the first two clients, you must specify the port of the called microservice in the corresponding variable (in Postman it is in the menu of the collection -> Edit -> Variables , and in the HTTP Client - in the environment variable specified in this file), and when testing method 2) The API also needs to specify the name of the microservice requested “under the hood”. The answers will be similar to those given above.

Comparison of application parameters


Artifact size
In order to preserve the simplicity of setting up and launching applications in assembly scripts, no transitive dependencies were excluded, therefore the size of the Spring Boot uber-JAR service significantly exceeds the size of its counterparts in other frameworks (because when using starters, not only the necessary dependencies are imported; if desired, the size can be significantly reduced):
MicroserviceArtifact size, MB
Helidon service16.6
Ktor service20.9
Micronaut service16.5
Spring Boot service42.7

Start time
The launch time of each application is intermittent and falls into a certain “window”; The table below shows the start time of the artifact without any additional parameters:
MicroserviceStart time, seconds
Helidon service2.2
Ktor service1.4
Micronaut service4.0
Spring Boot service10.2

It is worth noting that if you “clean” an application on Spring Boot from unnecessary dependencies and pay attention to setting up the application launch (for example, scan only the necessary packages and use lazy initialization of bins), you can significantly reduce the launch time.

Stress Testing
Gatling and Scala script were used for testing. The load generator and the service being tested were run on the same machine (Windows 10, 3.2 GHz quad-core processor, 24 GB RAM, SSD). The port of this service is specified in the Scala script.

For each microservice is determined:


Under the passage of the load test is understood that the microservice answered all requests for any time.
MicroserviceThe minimum amount of heap-memory, MB
To start the serviceFor load
50 * 1000
For load
500 * 1000
Helidon service99eleven
Ktor serviceeleveneleven13
Micronaut service131317
Spring Boot service222325

It is worth noting that all microservices use the HTTP server Netty.

Conclusion


The task was to create a simple service with the HTTP API and the ability to function in ISA - it was possible to perform on all the frameworks under consideration. It's time to take stock and consider their pros and cons.

Helidoun

Standard Edition

Microprofile
Microservice was not implemented on this framework, so I’ll note only a couple of points I know:


Ktor


Micronaut


Spring boot


You can also highlight common problems associated with new frameworks that Spring Boot lacks:


The frameworks considered belong to different weight categories: Helidon SE and Ktor are microframes , Spring Boot is a full-stack framework, Micronaut, rather, also full-stack; another category is MicroProfile (for example, Helidon MP). Microfrim functionality is limited, which can slow down the execution of tasks; To clarify the possibility of implementing a particular functionality based on a development framework, I recommend that you familiarize yourself with its documentation.

I don’t dare to judge whether this or that framework will “shoot” in the near future, so in my opinion, for now, it’s best to continue to monitor developments using the existing development framework for solving work tasks.

At the same time, as was shown in the article, new frameworks win Spring Boot on the considered parameters of the received applications. If any of these parameters are critically important for one of your microservices, then perhaps you should pay attention to the frameworks that showed the best results. However, we should not forget that Spring Boot, firstly, continues to improve, and secondly, it has a huge ecosystem and a significant number of Java programmers are familiar with it. There are other frameworks that are not covered in this article: Javalin, Quarkus, etc.

You can view the project code on GitHub . Thank you for attention!

PS: Thank you artglorin for help in preparing the article.

Source: https://habr.com/ru/post/456376/


All Articles