Spring.io tiene publicado desde marzo la versión experimental de Spring Native, toolsuite para dar soporte a la construcción de proyectos springboot compilables y ejecutables sobre GraalVM.

Pero primero: ¿Qué es GraalVM y qué problema se supone que nos soluciona?

Antecedentes: Cuando mezclas contenedores y JREs.

Desde que se popularizaron los primeros despliegues basados en contenedores, ha surgido el debate sobre si Java como tal podría llegar a tener los días contados.

En mi opinión, Java como tal es un lenguaje demasiado extendido y utilizado en demasiados ámbitos para llegar a suponer tal cosa. Hay demasiado buen código escrito bajo este lenguaje.

Por otro lado, el lenguaje de programación Java y sobre todo la máquina virtual Java, es una solución de mediados de los 90 que venía a dar respuesta al problema del desarrollo y ejecución de software sobre diferentes plataformas.

Básicamente, JDK te permite eso: Es un programa sobre el cual se ejecuta tu software de forma virtual. Gracias a este avance tú podías desentenderte del tipo de hardware o sistema operativo donde se ejecutaría tu software. Únicamente debías tener instalada una versión de JRE compatible en su servidor.

Esta ganancia, como todos bien sabemos no es gratis. La maquina virtual java, al ser una capa más entre su software y tu hardware, tiene un coste en memoria y rendimiento. Pero como no había una solución mejor, y porque además, funciona extremadamente bien, estos costes se aceptaron sin reservas.

spring_native_beta_1

Pero ahora, bien entrados en el siglo XXI, el paradigma cambia. El modelo de despliegue de contenedores, aparte de muchas otras más cosas, nos aporta el poder definir mediante un modelo de capas un entorno de ejecución completamente virtualizado, otra forma de dar respuesta al mismo problema.

spring_native_beta_2

Pero, ¿Qué ocurre si mezclamos estos dos conceptos? Pues nos encontramos con lo siguiente:

spring_native_beta_3

Como veis, por cada servicio, se añade dentro de los contenedores una capa pesada de virtualización (JRE) sobre la propia capa de virtualización de Docker, incrementando significativa e innecesariamente el consumo de recursos.

Por todas estas razones a día de hoy en entornos de muy alta escalabilidad, no es recomendable el desarrollo de microservicios sobre una JDK “tradicional”, en detrimento de otras plataformas de ejecución relativamente más directas como node.js, python, GoLang, etc.

GraalVM: ¿Una solución al problema?

logoGraalVM

GraalVM es un entorno de ejecución de alto rendimiento para Java, JavaScript, y lenguajes basados en LLVM como C y C ++, entre otros.

Principalmente GraalVM hace de “capa traductora” entre los lenguajes de programación o el código objeto de Java y tu S.O. (o en el caso de contenedores, la siguiente capa de virtualización). Además, permite la anticipación en ejecutables nativos para acelerar el tiempo de inicio y reducir la sobrecarga de memoria.

O, dicho de otro modo, es una forma de ejecutar aplicaciones Java sin necesidad de levantar una JRE y que permite acelerara su arranque si defines una precarga de clases configurable.

Al no seguir la estructura estándar de JDK, GrallVM no ejecuta de forma directa cualquier empaquetado jar. En general, GraalVM parece tener sus propias reglas de empaquetado y configuración que daría para otra entrada.

Por suerte para nosotros, ya existen frameworks como Micronaut (https://micronaut.io/) o Quarkus (https://quarkus.io/) y experimentalmente Spring Native que nos abstraen de todo ello.

Ok, GraalVM está muy bien, ¿Pero porque SpringBoot Native?

Respuesta corta: Por su historia, su comunidad y todo lo que lo rodea.

SpringBoot, y por asociación Spring, es uno de los frameworks de desarrollo Java más exitosos de todos los tiempos que cuenta con una de las comunidades más longevas y activas dentro del mundo Java. Hay mucho expertise acumulado sobre Springboot que sería una pena perder.

 

Por otro lado, Micronaut es un framework relativamente nuevo (2018) que está ganando mucha fuerza en estos últimos años y ya cuenta con una buena comunidad que lo apoya. Su framework si se considera más estable y en el caso de despliegues sobre GraalVM si se puede considerar la solución de referencia.

También en la competición está Quarkus, aún más reciente (Marzo 2019). Quarkus está pensado para ejecución de java nativo en entornos Kubernetes, siguiendo la filosofía container-first y, como no, también apoyado en GraalVM para ello.

 

Y aunque no descarto futuros post hablando de Quarkus y Micronaut, por los años que personalmente llevo trabajando con Spring y los buenos resultados que me ha aportado, Spring Boot Native debía ser el primero en ser examinado.

 

Construcción de piloto

Como piloto vamos a montar un servicio CRUD típico contra una base de datos PostgreSQL, aunque valdría cualquier otra con soporte r2dbc.

 

No nos complicamos y nos vamos a https://start.spring.io/:

spring_native_beta_4

Con esta configuración pulsamos sobre GENERATE, y ya tenemos un zip con el proyecto punto de partida.

 

Añadiendo drivers y persistencia

Abrimos pom.xml y añadimos a las dependencias los drivers reactivos para PostgreSQL:

spring_native_beta_5

Dado que el proyecto spring native está en estado experimental y que spring-data ya nos ofrece una buena base para construir CRUDS, vamos a evitar en esta demo incluir librerías no imprescindibles como Apache Lombok o Hibernate JPA que nos puedan generar conflictos innecesarios.

Ahora hay que añadir la persistencia.

Para ello, nos apoyamos en spring-data y creamos una entidad Libro, y su respectivo R2dbcRepository:

Libro.java

LibroRepository.java

Ya tenemos la persistencia, ahora a montar los servicios.

Servicios

Crearemos dos clases java: LibroServiceImpl.java (y su correspondiente interfaz con las operaciones de negocio) y LibroController.java, encargado de dar forma a la fachada REST  del CRUD.

LibroService.java

spring_native_beta_8

LibroServiceImpl.java

spring_native_beta_9

LibroController.java

spring_native_beta_10

Ya tenemos en esencia todo el código picado. Sólo faltarían un par de retoques:

  1. En la clase DemoSpringNativeApplication.java, añadimos la anotación @EnableR2dbcRepositories.
  2. Creamos una base de datos postgreSQL y le añadimos la tabla para libros:
    spring_native_beta_11
  3. En el archivo application.properties, incluimos las propiedades de conexión a dicha base de datos.
    spring_native_beta_12

Y ya tenemos el código fuente listo para empaquetar.

Empaquetando para GraalVM

La mejor forma de construir la imagen Docker con GraalVM, es utilizar el plugin spring-boot:build-image

$  mvn clean package spring-boot:build-image

Nota: El empaquetado consume tanto una gran cantidad de memoria (oficialmente recomiendan 16GB disponibles) como una gran cantidad de tiempo (no baja de más de 5 minutos).

Una vez completado el proceso, nos debe aparecer por consola un mensaje como el siguiente:

[INFO] Successfully built image ‘docker.io/library/demo-spring-native-graalvm:1.0.0’

 

Esto significa que, en nuestro Docker local, existe una nueva imagen con este tag que puede ser desplegada.

$  docker run -p 8080:8080 -e R2DBC_URL=… -e R2DBC_USER=… -e R2DBC_PASS=… docker.io/library/demo-spring-native-graalvm:1.0.0

 

Lo primero que os va a sorprender es el tiempo de arranque, alrededor de 0,12 segundos (Si parpadeas, te lo pierdes).

e.s.r.d.DemoSpringNativeApplication      : Started DemoSpringNativeApplication in 0.116 seconds (JVM running for 0.119)

 

Para probarlo se puede hacer uso de Postman o ejecutar peticiones curl manualmente:

GET

$ curl --location --request GET 'http://localhost:8080/libros/'

PUT (Insertar nuevo elemento)

$ curl --location --request PUT 'http:// localhost:8080/libros' \
--header 'Content-Type: application/json' \
--data-raw '{
"codigo": "9780007141326",
"titulo": "El señor de los anillos: Libro Uno, Dos y Tres",
"autor": "J. R R. Tolkien",
"sinopsis": "Este libro narra las aventuras de Frodo bolson y sus alegres compinches "
}
'

POST  (Modificar un elemento)

$ curl --location --request POST 'http://localhost:8080/libros' \
--header 'Content-Type: application/json' \
--data-raw '{
"codigo": "9780007141326",
"titulo": "El señor de los anillos: La trilogía completa",
"autor": "J. R R. Tolkien",
"sinopsis": "Este libro narra las desventuras de Frodo Bolson, y de como un recado simple como llevar un anillo a Bree se te puede ir de las manos"
}
'

DELETE

$ curl --location --request DELETE 'http://localhost:8080/libros/9780007141326'

 

Comparando Consumos

Ahora para poder estar seguros, vamos en primer lugar a utilizar el mismo código fuente para montar un contendor con el empaquetado JRE “clásico” y así comparar que métricas arrojan cada uno.

Además, para que las reglas sean lo más justas posibles, vamos simplemente a montarlo y lanzarlo sobre contenedor basado en una imagen muy eficiente en recursos: registry.access.redhat.com/ubi8/openjdk-11-runtime.

De entrada, el tiempo de arranque ha pasado de milisegundos a segundos completos: 4 ~ 5s aprox:

e.s.r.d.DemoSpringNativeApplication: Started DemoSpringNativeApplication in 4.057 seconds (JVM running for 4.826)

Para un vistazo a métricas más orientadas al consumo de recursos, en mi caso, ya que tengo portainer montado en local para otros menesteres, me voy a limitar a comparar gráficas.

 

Contenedor con openjdk-runtime:

spring_native_beta_14

Contenedor con GraalVM:

spring_native_beta_15
Como veis, el consumo de RAM es bastante menos de la mitad. No he apreciado diferencias de rendimiento entre ellos, aunque es cierto que al ser proyectos pequeños y además con servicios reactivos, es difícil medir diferencias sin hacer auténticas pruebas de carga.

Además, las imágenes con GraalVM pesan casi 4 veces menos (112MB vs 400MB aprox).

CONCLUSIONES

 

Spring Native es un inicio prometedor al que aún le faltan muchas aristas por pulir.

El proceso de construcción es delicado. En mi caso, para el ejemplo, tuve que volver a montar el proyecto base un par de veces.

No todo es compatible hasta la fecha. Por ejemplo, la propia configuración para swagger, @OpenAPIDefinition, dió problemas de arranque, aunque es posible que fuera por falta de algún detalle de la configuración del proceso de construcción.

Además, como ya he mencionado, la ejecución build-image consume mucha memoria (su documentación oficial recomienda 16GB), y tarda demasiado en completarse (~6 minutos en el caso del ejemplo). Si entramos en el contexto del desarrollo local y la integración continua, son tiempos de espera excesivos.

En resumen, Spring native es un proyecto muy en fase beta al que no debemos perderle la pista, aunque por las dificultades encontradas no lo considero lo suficientemente maduro como para utilizarlo a día de hoy profesionalmente.
Puedes descargar el fuente de este articulo accediendo a su página de GitHub

En SOLTEL tenemos una actitud innovadora hacia la tecnología porque nos apasiona. Nuestra trayectoria trabajando con distintas tecnologías a lo largo de tantos años, nos ayuda a afrontar los proyectos con muchísima perspectiva y a convertirnos en auténticos partners tecnológicos de nuestros clientes.

 

Articulo elaborado por Ramón Tur Vázquez, Solutions Architect en SOLTEL Group