En el actual mundo tecnológico, donde existe una tendencia clara hacia aplicaciones cloud native, necesitamos aplicaciones que sean capaces de inicializarse en el menor tiempo posible. Los microservicios pertenecen a este tipo de aplicaciones y, para que su escalado sea eficiente, es necesario minimizar su tiempo de arranque.
Para conseguirlo, hoy os voy a hablar de dos herramientas que, trabajando de forma conjunta, nos dan muy buenos resultados. Dichas herramientas son GraalVM y Micronaut.
Pre-requisitos
A lo largo de este post vamos a utilizar:
- Sistema operativo Windows o basado en Linux.
- Docker.
- IDE de desarrollo.
- Maven.
- GraalVM Community Edition. Es necesaria únicamente en tiempo de compilación por lo que la agregaremos en nuestro IDE como la JVM seleccionada con la que se compilarán nuestros proyectos.
- Micronaut CLI. Tenemos que agregar la ruta de sus binarios a nuestro PATH para disponer de sus opciones desde la línea de comandos.
$ mn --version
| Micronaut Version: 1.2.3
| JVM Version: 1.8.0_212
$ java -version
java version "1.8.0_212"
Java(TM) SE Runtime Environment (build 1.8.0_212-b31)
Java HotSpot(TM) 64-Bit GraalVM EE 19.1.0 (build 25.212-b31-jvmci-20-b04, mixed mode)
Vistazo a las herramientas
GraalVM es una máquina virtual que permite interactuar con distintos lenguajes de programación (JavaScript, Python, Ruby, R, JVM-based languages like Java, Scala, Groovy, Kotlin, Clojure, and LLVM-based languages such as C and C++). A pesar de los múltiples lenguajes que nos ofrece, me voy a centrar en este artículo en lenguaje Java.
Micronaut es framework que permite, entre otros, desarrollar microservicios basados en Java y configurar Netty como servidor embebido.
Crear aplicación
Para crear la aplicación utilizaremos Micronaut CLI que nos facilitará la construcción del proyecto.
$mn create-app mi-primer-microservicio -f graal-native-image -b maven
Utilizando la opción -f (--features)
directamente nos prepara el proyecto para poder ser compilado como imagen nativa.
Para ver todas las características que nos proporciona, en el momento de la creación ejecutamos lo siguiente:
$mn profile-info service
Una vez creada la aplicación podemos importarla con nuestro IDE de preferencia como un proyecto Maven (al utilizar la opción -b, --build=BUILD-TOOL
como argumento del comando create-app
le hemos indicado que será un proyecto Maven en lugar de Gradle, que es la opción por omisión).
La aplicación
La estructura de la aplicación es la siguiente:
/mi-primer-microservicio
|-src
|-main
|-java
|-mi.primer.microservicio
|-Application.java
Las anotaciones básicas de Micronaut que vamos a utilizar son:
- @Controller - permite indicar el punto de entrada a nivel de clase de nuestro servicio.
- @Get - indica el tipo de peticiones que atenderá el servicio y el formato de las mismas.
- @Introspected - es importante escribirla a nivel de clase en los POJOs que queremos se sirvan en el contrato del servicio (DTOs).
Agregamos la dependencia de Lombok en el pom, para acelerar el desarrollo y configuramos nuestro IDE para que procese las anotaciones:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>${lombok.version}</version>
</dependency>
Configuramos el proyecto en el pom, para que procese las anotaciones de Lombok. Para ello, estas anotaciones deben ir antes que las de Micronaut en el tag de annotationProcessorPaths:
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
...
</annotationProcessorPaths>
Las anotaciones de Lombok utilizadas son @Data
y @Builder
.
Ahora sí, generamos nuestro código para exponer nuestro servicio:
Gretting.java
@Introspected
@Builder
@Data
public class Greeting {
private String action;
}
HelloController.java
@Controller("/api/v1/micronaut")
public class HelloController {
@Get(value = "/hello", produces = MediaType.APPLICATION_JSON)
public HttpResponse<Greeting> hello() {
return HttpResponse.ok(Greeting.builder().action("Hello").build());
}
}
Generamos el jar que contiene el código Java compilado con las dependencias necesarias para ejecutarse en cualquier JVM.
$ mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< mi.primer.microservicio:mi-primer-microservicio >-----------
[INFO] Building mi-primer-microservicio 0.1
[INFO] --------------------------------[ jar ]---------------------------------
...
...
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.534 s
[INFO] Finished at: 2019-10-02T23:16:05+02:00
[INFO] ------------------------------------------------------------------------
Comprobamos el arranque de nuestra aplicación:
$ java -jar target/mi-primer-microservicio-0.1.jar
23:18:02.899 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 2626ms. Server Running: http://localhost:8080
Comprobamos el funcionamiento de nuestro servicio:
$ curl -XGET http://localhost:8080/api/v1/micronaut/hello
{"action":"Hello"}
Como resultado de la primera ejecución podemos ver que el tiempo de arranque del microservicio que ha sido 2626ms.
Podemos ver que, al tratarse de un servicio simple, ha sido bastante sencillo implementarlo y ejecutarlo. Para aquellos desarrolladores con experiencia en el desarrollo de microservicios con el framework de Spring boot os resultará familiar el código.
¡Vamos a la parte más interesante! Es decir, optimizar el tiempo de arranque de nuestro microservicio.
Si os habéis fijado en la estructura generada por Micronaut CLI, entre los ficheros han aparecido Dockerfile y docker-build.sh.
/mi-primer-microservicio
|-src
|-main
|-java
|-mi.primer.microservicio
|-Application.java
-Dockerfile
-docker-build.sh
Seguimos las instrucciones que vienen dentro de docker-build.sh o podemos ejecutarlo como un batch.
$ cat docker-build.sh
#!/bin/sh
docker build . -t mi-primer-microservicio
echo
echo
echo "To run the docker container execute:"
echo " $ docker run -p 8080:8080 mi-primer-microservicio"
Mediante docker build
generamos la imagen docker de nuestro microservicio:
$docker build . -t mi-primer-microservicio
....
Con esto lo que hacemos es compilar nuestro microservicio con native-image, una herramienta de GraalVM. Durante este proceso se realizan optimizaciones dando como resultado una imagen docker.
Las mejoras en tiempo de ejecución proporcionadas por GraalVM están basadas en AOT (ahead-of-time), es decir, adelantar la creación de objetos del contexto de arranque de la aplicación. Normalmente, la compilación que se realiza con el compilador de Java es JIT (just-in-time), es decir, delega la creación de todos los objetos del contexto en el momento de arranque.
Durante el proceso de generación de la imagen nativa, se hace un análisis estático pormenorizado de aquellas clases y métodos que son alcanzables, y serán estos los que serán tenidos en cuenta para la compilación AOT. Esto implica que, por ahora, también nos encontremos con alguna limitación como el hecho de que no está soportada la carga dinámica de clases (dynamic class loading).
Para completar la foto, Micronaut nos brinda un framework optimizado por encima de GraalVM ya que, también se podría utilizar GraalVM para compilar aplicaciones que no se vayan a optimizar y que emplean, por ejemplo, el framework Spring 5. Hay que mencionar que, en la actualidad, el equipo del proyecto Spring trabaja con el equipo de GraalVM para liberar una versión optimizada del proyecto.
...
Sending build context to Docker daemon 14.78MB
Step 1/9 : FROM oracle/graalvm-ce:19.2.0.1 as graalvm
...
Downloading: Component catalog from www.graalvm.org
Processing component archive: Native Image
Downloading: Component native-image: Native Image from github.com
Installing new component: Native Image (org.graalvm.native-image, version 19.2.0.1)
...
Successfully built 0f93e1021c35
Successfully tagged mi-primer-microservicio:latest
Ahora que tenemos la imagen docker preparada, la usamos de la siguiente forma:
$ docker run -p 8080:8080 mi-primer-microservicio
19:27:35.009 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 105ms. Server Running: http://d418c61e52b8:8080
Y volvemos a verificar que la invocación funciona perfectamente, obteniendo el mismo resultado:
$ curl -XGET http://localhost:8080/api/v1/micronaut/hello
{"action":"Hello"}
Como podéis observar el tiempo de arranque obtenido es de 105ms lo cual supone una mejora evidente en comparación con el primer arranque que fue 2626ms.
Conclusiones
- Micronaut es un framework sencillo de aprender con un gran número de funcionalidades, aunque aún está en desarrollo y con la posibilidad de encontrar errores.
- GraalVM es capaz de generar aplicaciones en imágenes nativas (native-image), lo que proporciona una arranque más rápido de las mismas.
- Se presenta una alternativa para acelerar el arranque de nuestros microservicios y explotar sus capacidades en la nube.
Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevas entregas!