Not One Spring Boot: una descripción general de alternativas



Actualmente no hay escasez de marcos para crear microservicios en Java y Kotlin. El artículo discute lo siguiente:
TituloVersiónAño de primer lanzamientoDesarrollador
Helidon se1.1.12019Oráculo
Ktor1.2.12018Jetbrains
Micronaut1.1.32018Informática de objetos
Bota de primavera2.1.52014Pivotal

En base a ellos, se han creado cuatro servicios que pueden interactuar entre sí a través de la API HTTP utilizando el patrón de descubrimiento de servicios implementado mediante Consul . Por lo tanto, forman una arquitectura de microservicio heterogénea (a nivel de marco) (en lo sucesivo, ISA):



Defina un conjunto de requisitos para cada servicio:

  • pila de tecnología:
    • JDK 12;
    • Kotlin
    • Gradle (Kotlin DSL);
    • JUEGO 5.

  • funcionalidad (API HTTP):
    • GET /application-info{?request-to=some-service-name}
      Devuelve información básica sobre el microservicio (nombre, marco, año de lanzamiento del marco); al especificar el nombre de uno de los cuatro microservicios en el parámetro de request-to a su API HTTP, se ejecuta una solicitud similar que devuelve información básica;
    • GET /application-info/logo
      Devuelve la imagen.

  • implementación:
    • configuración utilizando el archivo de configuración;
    • Usando inyección de dependencia
    • pruebas que verifican la funcionalidad de la API HTTP.

  • ISA
    • usando el patrón de Descubrimiento de Servicio (registrándose con Consul, accediendo a la API HTTP de otro microservicio por su nombre usando el equilibrio de carga del cliente);
    • Formación de artefactos uber-jar.


A continuación, consideramos la implementación de un microservicio en cada uno de los marcos y comparamos los parámetros de las aplicaciones recibidas.

Servicio de Helidon


El marco de desarrollo se creó en Oracle para uso interno, y luego se convirtió en código abierto. Hay dos modelos de desarrollo basados ​​en este marco: Standard Edition (SE) y MicroProfile (MP). En ambos casos, el servicio será un programa Java SE normal. Obtenga más información sobre las diferencias en esta página.

En resumen, Helidon MP es una de las implementaciones de Eclipse MicroProfile , que permite utilizar muchas API, ambas conocidas anteriormente por los desarrolladores de Java EE (por ejemplo, JAX-RS, CDI) y las más nuevas (Health Check, Metrics, Fault Tolerance). etc.) En la variante Helidon SE, los desarrolladores se guiaron por el principio de "Sin magia", que se expresa, en particular, en menos o ninguna anotación necesaria para crear la aplicación.

Helidon SE fue seleccionado para el desarrollo de microservicios. Entre otras cosas, carece de herramientas para implementar la inyección de dependencias, por lo que Koin se usa para implementar dependencias. La siguiente es una clase que contiene el método principal. Para implementar la inyección de dependencia, la clase hereda de KoinComponent . Koin comienza primero, luego se inicializan las dependencias requeridas y se llama al método startServer() , donde se crea un objeto del tipo WebServer , al que se transfieren previamente la configuración de la aplicación y la configuración de enrutamiento; después de comenzar la aplicación se registra en 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 } 

El enrutamiento se configura de la siguiente manera:

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

La aplicación usa la configuración en el formato HOCON :

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

También es posible usar archivos en JSON, YAML y formatos de propiedades para la configuración (más detalles aquí ).

Servicio de Ktor


El marco está escrito en Kotlin. Se puede crear un nuevo proyecto de varias maneras: usando el sistema de compilación, start.ktor.io o el complemento para IntelliJ IDEA (más aquí ).

Al igual que Helidon SE, Ktor no tiene una DI lista para usar, por lo que las dependencias se implementan utilizando Koin antes de iniciar el servidor:

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

Los módulos requeridos por la aplicación se especifican en el archivo de configuración (es posible usar solo el formato HOCON; más sobre la configuración del servidor Ktor aquí ), cuyo contenido se presenta a continuación:

 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 y Koin usan el término "módulo", que tiene diferentes significados. En Koin, un módulo es un análogo del contexto de la aplicación en Spring Framework. El módulo Ktor es una función definida por el usuario que acepta un objeto de tipo Aplicación y puede configurar una tubería, establecer características, registrar rutas, procesar
solicitudes, etc .:

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

En este fragmento de código, el enrutamiento de solicitudes se configura, en particular, el recurso estático logo.png .

El servicio de Ktor puede contener funciones. Una característica es una funcionalidad que está incrustada en una tubería de solicitud-respuesta ( DefaultHeaders, Compression y otros en el ejemplo de código anterior). Es posible implementar sus propias características, por ejemplo, el siguiente código implementa el patrón de Descubrimiento de Servicio en combinación con el equilibrio de carga del cliente basado en el algoritmo 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 } } } } 

La lógica principal está en el método de install : durante la fase de solicitud de Render (que se ejecuta antes de la fase de envío ), primero se determina el nombre del servicio que se llama, luego se solicita una lista de instancias de este servicio a consulClient , después de lo cual se determina la instancia utilizando el algoritmo Round-robin. Por lo tanto, se hace posible la siguiente llamada:

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


Servicio de Micronaut


Micronaut está desarrollado por los creadores del marco Grails y está inspirado en la experiencia de crear servicios utilizando Spring, Spring Boot y Grails. El marco es un políglota que admite Java, Kotlin y Groovy; tal vez habrá soporte para Scala. La inyección de dependencia se lleva a cabo en la etapa de compilación, lo que conduce a un menor consumo de memoria y un inicio de aplicación más rápido en comparación con Spring Boot.

La clase principal tiene la siguiente forma:

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

Algunos componentes de una aplicación basada en Micronaut son similares a sus contrapartes en una aplicación Spring Boot, por ejemplo, el código del controlador es el siguiente:

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

El soporte de Kotlin en Micronaut se basa en el complemento del compilador kapt (más información aquí ). El script de ensamblaje se configura de la siguiente manera:

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

El siguiente es el contenido del archivo de configuración:

 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 

La configuración de microservicio también es posible con JSON, propiedades y formatos de archivo Groovy (más detalles aquí ).

Servicio de arranque de primavera


El marco fue creado para simplificar el desarrollo de aplicaciones utilizando el ecosistema Spring Framework. Esto se logra a través de mecanismos de autoconfiguración al conectar bibliotecas. El siguiente es el código del controlador:

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

El microservicio está configurado con un archivo YAML:

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

También es posible usar archivos de formato de propiedades para la configuración (más detalles aquí ).

Lanzamiento


El proyecto funciona en JDK 12, aunque es probable que también esté en la versión 11, solo necesita cambiar el parámetro jvmTarget en los scripts de ensamblaje en jvmTarget :

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

Antes de iniciar microservicios, debe instalar Consul e iniciar el agente, por ejemplo, así: consul agent -dev .

Es posible iniciar microservicios desde:

  • IDE
    Los usuarios de IntelliJ IDEA pueden ver algo como lo siguiente:

  • la consola
    Para hacer esto, vaya a la carpeta del proyecto y ejecute secuencialmente:

     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 


Después de iniciar todos los microservicios en http://localhost:8500/ui/dc1/services verá:



Prueba de API


Los resultados de probar la API del servicio Helidon se dan como ejemplo:

  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

    Devuelve la imagen.

Puede probar una API de microservicio arbitraria utilizando Postman (una colección de solicitudes), el cliente IntelliJ IDEA HTTP (una colección de solicitudes), un navegador u otra herramienta. Si usa los dos primeros clientes, debe especificar el puerto del microservicio llamado en la variable correspondiente (en Postman está en el menú de colección -> Editar -> Variables , y en el Cliente HTTP está en la variable de entorno especificada en este archivo), y al probar el método 2) La API también necesita especificar el nombre del microservicio "bajo el capó" solicitado. Las respuestas serán similares a las dadas anteriormente.

Comparación de configuraciones de aplicaciones



Tamaño del artefacto


Para preservar la simplicidad de configurar y ejecutar aplicaciones en scripts de ensamblaje, no se excluyeron las dependencias transitivas, por lo que el tamaño del servicio uber-JAR en Spring Boot excede significativamente el tamaño de los análogos en otros marcos (porque cuando se usan iniciadores, no solo se importan las dependencias necesarias; si lo desea, el tamaño puede reducirse significativamente):
MicroservicioTamaño de artefacto, MB
Servicio de Helidon16,6
Servicio de Ktor20,9
Servicio de Micronaut16,5
Servicio de arranque de primavera42,7


Tiempo de lanzamiento


El tiempo de inicio de cada aplicación es inconsistente y cae en alguna "ventana"; La siguiente tabla muestra el tiempo de lanzamiento del artefacto sin especificar ningún parámetro adicional:
MicroservicioHora de inicio, segundos
Servicio de Helidon2.2 2.2
Servicio de Ktor1.4
Servicio de Micronaut4.0 4.0
Servicio de arranque de primavera10,2

Vale la pena señalar que si "limpia" la aplicación Spring Boot de dependencias innecesarias y presta atención a la configuración de la aplicación para que se inicie (por ejemplo, escanee solo los paquetes necesarios y use la inicialización de bin diferido), puede reducir significativamente el tiempo de inicio.

Prueba de carga


Para las pruebas, se utilizaron Gatling y un script Scala. El generador de carga y el servicio bajo prueba se ejecutaron en la misma máquina (Windows 10, un procesador quad-core de 3.2 GHz, 24 GB de RAM, SSD). El puerto de este servicio se indica en el script Scala.

Para cada microservicio se determina:

  • la cantidad mínima de memoria de almacenamiento -Xmx ( -Xmx ) requerida para ejecutar un microservicio de trabajo (que responde a las solicitudes)
  • memoria mínima necesaria para pasar la prueba de carga 50 usuarios * 1000 solicitudes
  • memoria de almacenamiento dinámico mínima requerida para pasar la prueba de carga 500 usuarios * 1000 solicitudes

Pasar una prueba de carga significa que el microservicio respondió a todas las solicitudes en cualquier momento.
MicroservicioLa cantidad mínima de memoria de almacenamiento dinámico, MB
Para iniciar el servicioPara carga 50 * 1000Para carga 500 * 1000
Servicio de Helidon9 99 911
Servicio de Ktor111113
Servicio de Micronaut131317
Servicio de arranque de primavera222325

Vale la pena señalar que todos los microservicios usan el servidor Netty HTTP.

Conclusión


La tarea, la creación de un servicio simple con HTTP API y la capacidad de funcionar en el ISA, se pudo completar en todos los marcos en cuestión. Es hora de hacer un balance y considerar sus ventajas y desventajas.

Helidon

Edición estándar
  • ventajas
    • configuración de la aplicación
      En todos los aspectos mostró buenos resultados;
    • "Sin magia"
      El marco justificó el principio establecido por los desarrolladores: solo se necesitó una anotación para crear la aplicación ( @JvmStatic - para la interoperación Java-Kotlin).
  • contras
    • microframework
      Faltan algunos componentes necesarios para el desarrollo industrial, por ejemplo, la inyección de dependencias y la implementación de Service Discovery.

Microperfil
El microservicio no se implementó en este marco, por lo que solo notaré un par de puntos que conozco:

  • ventajas
    • Implementación de Eclipse MicroProfile
      En esencia, MicroProfile es Java EE optimizado para ISA. Por lo tanto, en primer lugar, obtiene acceso a toda la variedad de API Java EE, incluidas las diseñadas específicamente para el ISA, y en segundo lugar, puede cambiar la implementación de MicroProfile a cualquier otra (Open Liberty, WildFly Swarm, etc.) .
  • adicionalmente
    • en MicroProfile Starter puede crear un proyecto desde cero con los parámetros necesarios por analogía con herramientas similares para otros marcos (por ejemplo, Spring Initializr ). En el momento de la publicación del artículo, Helidon implementa MicroProfile 1.2, mientras que la última versión de la especificación es 3.0.


Ktor

  • ventajas
    • ligereza
      Le permite conectar solo aquellas funciones que se necesitan directamente para completar la tarea;
    • configuración de la aplicación
      Buenos resultados en todos los aspectos.
  • contras
    • "Afilado" bajo Kotlin, es decir, es posible, pero no necesario, desarrollarse en Java;
    • microframework (ver artículo similar para Helidon SE).
  • adicionalmente
    Por un lado, el concepto de desarrollo de marco no está incluido en los dos modelos de desarrollo de Java más populares (Spring-like (Spring Boot / Micronaut) y Java EE / MicroProfile), lo que puede conducir a:

    • un problema con encontrar especialistas;
    • más tiempo para completar tareas en comparación con Spring Boot debido a la necesidad de configurar explícitamente la funcionalidad requerida.

    Por otro lado, la diferencia con respecto a los "clásicos" Spring y Java EE le permite ver el proceso de desarrollo desde un ángulo diferente, tal vez de manera más consciente.


Micronaut

  • ventajas
    • Mucho
      Como se señaló anteriormente, AOT le permite reducir el tiempo de inicio y la memoria consumida por la aplicación en comparación con su contraparte en Spring Boot;
    • Modelo de desarrollo primaveral
      Los programadores con experiencia en desarrollo en primavera no tardarán mucho en dominar este marco;
    • configuración de la aplicación
      Buenos resultados en todos los aspectos;
    • políglota
      Apoyo ciudadano de primera clase para Java, Kotlin, Groovy; tal vez habrá soporte para Scala. En mi opinión, esto puede afectar positivamente el crecimiento de la comunidad. Por cierto, en junio de 2019 Groovy en el ranking de la popularidad de los lenguajes de programación, TIOBE ocupa el 14 ° lugar, despegando del 60 ° año anterior, por lo que se encuentra en un honorable segundo lugar entre los idiomas JVM;
    • El proyecto Micronaut for Spring también le permite cambiar el tiempo de ejecución de su aplicación Spring Boot existente a Micronaut (con restricciones).


Bota de primavera

  • ventajas
    • madurez de la plataforma y ecosistema
      El marco "todos los días". Para la mayoría de las tareas cotidianas, ya existe una solución en el paradigma de programación Spring, es decir, de una manera familiar para muchos programadores. El desarrollo se simplifica por los conceptos de iniciadores y configuraciones automáticas;
    • la presencia de una gran cantidad de especialistas en el mercado laboral, así como una importante base de conocimiento (que incluye documentación y respuestas a Stack Overflow);
    • perspectiva
      Creo que muchos estarán de acuerdo en que en el futuro cercano, Spring seguirá siendo el marco de desarrollo líder.
  • contras
    • configuración de la aplicación
      La aplicación en este marco no estaba entre los líderes, sin embargo, algunos parámetros, como se señaló anteriormente, se pueden optimizar de forma independiente. También vale la pena recordar la presencia del proyecto Spring Fu , que se encuentra en desarrollo activo, cuyo uso permite reducir estos parámetros.

También puede resaltar los problemas generales asociados con los nuevos marcos que faltan en Spring Boot:

  • ecosistema menos desarrollado;
  • un pequeño número de especialistas con experiencia en estas tecnologías;
  • mayor tiempo para completar tareas;
  • perspectivas oscuras

Los marcos considerados pertenecen a diferentes categorías de peso: Helidon SE y Ktor son microframes , Spring Boot es un marco de pila completa, Micronaut es más probable que también sea de pila completa; otra categoría es MicroProfile (por ejemplo, Helidon MP). En microframes, la funcionalidad es limitada, lo que puede ralentizar la ejecución de tareas; Para aclarar la posibilidad de implementar esta o aquella funcionalidad sobre la base de cualquier marco de desarrollo, le recomiendo que se familiarice con su documentación.

No me atrevo a juzgar si este o aquel marco se "disparará" en el futuro cercano, por lo tanto, en mi opinión, es mejor continuar monitoreando el desarrollo de eventos usando el marco de desarrollo existente para resolver tareas de trabajo.

Al mismo tiempo, como se mostró en el artículo, los nuevos marcos superan a Spring Boot en los parámetros considerados de las aplicaciones recibidas. Si alguno de estos parámetros es crítico para cualquiera de sus microservicios, es posible que deba prestar atención a los marcos que mostraron los mejores resultados en ellos. Sin embargo, no olvide que Spring Boot, en primer lugar, continúa mejorando y, en segundo lugar, tiene un ecosistema enorme y un número significativo de programadores de Java están familiarizados con él. Hay otros marcos que no están cubiertos en este artículo: Javalin, Quarkus, etc.

Puede ver el código del proyecto en GitHub . Gracias por su atencion!

PD: Gracias a artglorin por ayudarme con este artículo.

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


All Articles