1. Descripción general
En este codelab, aprenderemos sobre el proyecto Spring Native, compilaremos una app que lo use y la implementaremos en Google Cloud.
Analizaremos sus componentes, la historia reciente del proyecto, algunos casos de uso y, por supuesto, los pasos necesarios para que lo uses en tus proyectos.
Actualmente, el proyecto Spring Native se encuentra en una fase experimental, por lo que requerirá una configuración específica para comenzar. Sin embargo, como se anunció en SpringOne 2021, Spring Native se integrará en Spring Framework 6.0 y Spring Boot 3.0 con compatibilidad de primera clase, por lo que este es el momento perfecto para analizar el proyecto con mayor detalle unos meses antes de su lanzamiento.
Si bien la compilación just in time se optimizó muy bien para elementos como los procesos de larga duración, existen ciertos casos de uso en los que las aplicaciones compiladas con anticipación funcionan aún mejor, lo que analizaremos durante el codelab.
En un próximo lab,
- Uso de Cloud Shell
- Habilita la API de Cloud Run
- Crea e implementa una app nativa de Spring
- Implementa una app de este tipo en Cloud Run
Requisitos
- Un proyecto de Google Cloud Platform con una cuenta de facturación activa de GCP
- CLI de gcloud instalada o acceso a Cloud Shell
- Habilidades básicas de Java y XML
- Conocimiento práctico de los comandos comunes de Linux
Encuesta
¿Cómo usarás este instructivo?
¿Cómo calificarías tu experiencia con Java?
¿Cómo calificarías tu experiencia en el uso de los servicios de Google Cloud?
2. Segundo plano
El proyecto Spring Native usa varias tecnologías para ofrecer a los desarrolladores un rendimiento de aplicación nativo.
Para comprender completamente Spring Native, es útil entender algunas de estas tecnologías de componentes, lo que nos permiten y cómo funcionan en conjunto.
Compilación AOT
Cuando los desarrolladores ejecutan javac de forma normal en el tiempo de compilación, nuestro código fuente .java se compila en archivos .class que están escritos en código de bytes. Este código de bytes solo está diseñado para que lo comprenda la máquina virtual de Java, por lo que la JVM tendrá que interpretar este código en otras máquinas para que podamos ejecutarlo.
Este proceso nos brinda la portabilidad de la firma de Java, lo que nos permite “escribir una vez y ejecutar en todas partes”, pero es costoso en comparación con la ejecución de código nativo.
Afortunadamente, la mayoría de las implementaciones de la JVM usan la compilación just in time para mitigar este costo de interpretación. Esto se logra contando las invocaciones de una función y, si se invoca con la frecuencia suficiente para pasar un umbral ( 10,000 de forma predeterminada), se compila en código nativo durante el tiempo de ejecución para evitar una interpretación más costosa.
La compilación por adelantado adopta el enfoque opuesto, ya que compila todo el código accesible en un ejecutable nativo en el momento de la compilación. Esto cambia la portabilidad por la eficiencia de la memoria y otras mejoras de rendimiento en el tiempo de ejecución.
Por supuesto, esta es una compensación y no siempre vale la pena realizarla. Sin embargo, la compilación AOT puede destacarse en ciertos casos de uso, como los siguientes:
- Aplicaciones de corta duración en las que el tiempo de inicio es importante
- Entornos con limitaciones de memoria en los que JIT puede ser demasiado costoso
Como dato curioso, la compilación AOT se introdujo como una función experimental en JDK 9, aunque esta implementación era costosa de mantener y nunca se popularizó, por lo que se quitó en silencio en Java 17 en favor de los desarrolladores que solo usaban GraalVM.
GraalVM
GraalVM es una distribución de JDK de código abierto altamente optimizada que cuenta con tiempos de inicio extremadamente rápidos, compilación de imágenes nativas de AOT y capacidades poliglotas que permiten a los desarrolladores mezclar varios lenguajes en una sola aplicación.
GraalVM está en desarrollo activo, gana nuevas capacidades y mejora las existentes todo el tiempo, por lo que recomiendo a los desarrolladores que estén atentos.
Estos son algunos hitos recientes:
- Un nuevo resultado de compilación de imágenes nativas fácil de usar ( 18/01/2021)
- Compatibilidad con Java 17 ( 18/01/2022)
- Se habilitó la compilación de varios niveles de forma predeterminada para mejorar los tiempos de compilación de Polyglot ( 2021-04-20).
Spring Native
En pocas palabras, Spring Native permite el uso del compilador de imágenes nativas de GraalVM para convertir aplicaciones de Spring en ejecutables nativos.
Este proceso implica realizar un análisis estático de tu aplicación en el tiempo de compilación para encontrar todos los métodos de la aplicación a los que se puede acceder desde el punto de entrada.
Esto, en esencia, crea una concepción de “mundo cerrado” de tu aplicación, en la que se supone que todo el código es conocido en el tiempo de compilación y no se permite cargar ningún código nuevo durante el tiempo de ejecución.
Es importante tener en cuenta que la generación de imágenes nativas es un proceso intensivo de memoria que tarda más que compilar una aplicación normal y que impone limitaciones en ciertos aspectos de Java.
En algunos casos, no se requieren cambios en el código para que una aplicación funcione con Spring Native. Sin embargo, algunas situaciones requieren una configuración nativa específica para funcionar correctamente. En esas situaciones, Spring Native suele proporcionar sugerencias nativas para simplificar este proceso.
3. Configurar/trabajo previo
Antes de comenzar a implementar Spring Native, deberemos crear e implementar nuestra app para establecer un modelo de referencia de rendimiento que podamos comparar con la versión nativa más adelante.
1. Cómo crear el proyecto
Comenzaremos por obtener nuestra app de start.spring.io:
curl https://start.spring.io/starter.zip -d dependencies=web \ -d javaVersion=11 \ -d bootVersion=2.6.4 -o io-native-starter.zip
Esta app de partida usa Spring Boot 2.6.4, que es la versión más reciente que admite el proyecto spring-native en el momento de escribir este artículo.
Ten en cuenta que, desde el lanzamiento de GraalVM 21.0.3, también puedes usar Java 17 para este ejemplo. Seguiremos usando Java 11 para este instructivo para minimizar la configuración involucrada.
Una vez que tengamos nuestro archivo ZIP en la línea de comandos, podemos crear un subdirectorio para nuestro proyecto y descomprimir la carpeta allí:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. Cambios en el código
Una vez que tengamos el proyecto abierto, agregaremos rápidamente un signo de vida y mostraremos el rendimiento de Spring Native una vez que lo ejecutemos.
Edita DemoApplication.java para que coincida con lo siguiente:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
@SpringBootApplication
public class DemoApplication {
private static Instant startTime;
private static Instant readyTime;
public static void main(String[] args) {
startTime = Instant.now();
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String index() {
return "Time between start and ApplicationReadyEvent: "
+ Duration.between(startTime, readyTime).toMillis()
+ "ms";
}
@EventListener(ApplicationReadyEvent.class)
public void ready() {
readyTime = Instant.now();
}
}
En este punto, nuestra app de referencia está lista para usarse, así que no dudes en compilar una imagen y ejecutarla de forma local para obtener una idea del tiempo de inicio antes de convertirla en una aplicación nativa.
Para compilar nuestra imagen, haz lo siguiente:
mvn spring-boot:build-image
También puedes usar docker images demo
para obtener una idea del tamaño de la imagen de referencia:
Para ejecutar nuestra app, sigue estos pasos:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. Implementa la app de modelo de referencia
Ahora que tenemos nuestra app, la implementaremos y tomaremos nota de los tiempos, que compararemos con los tiempos de inicio de nuestra app nativa más adelante.
Según el tipo de aplicación que compilas, existen varias opciones de hosting para tu contenido.
Sin embargo, como nuestro ejemplo es una aplicación web muy simple y directa, podemos simplificar el proceso y confiar en Cloud Run.
Si estás siguiendo los pasos en tu propia máquina, asegúrate de tener instalada y actualizada la herramienta gcloud CLI.
Si estás en Cloud Shell, se encargará de todo y solo tendrás que ejecutar lo siguiente en el directorio de origen:
gcloud run deploy
4. Configuración de la aplicación
1. Configura nuestros repositorios de Maven
Dado que este proyecto aún se encuentra en la fase experimental, tendremos que configurar nuestra app para que pueda encontrar artefactos experimentales, que no están disponibles en el repositorio central de Maven.
Esto implicará agregar los siguientes elementos a nuestro pom.xml, lo que puedes hacer en el editor que elijas.
Agrega las siguientes secciones repositories y pluginRepositories a nuestro pom:
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
2. Agregamos nuestras dependencias
A continuación, agrega la dependencia spring-native, que es necesaria para ejecutar una aplicación de Spring como una imagen nativa. Nota: Este paso no es necesario si usas Gradle.
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. Agrega o habilita nuestros complementos
Ahora, agrega el complemento AOT para mejorar la compatibilidad y el espacio en disco de las imágenes nativas ( Obtén más información):
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Ahora, actualizaremos el complemento spring-boot-maven-plugin para habilitar la compatibilidad con imágenes nativas y usaremos el compilador de paketo para compilar nuestra imagen nativa:
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
Ten en cuenta que la imagen del compilador pequeño es solo una de varias opciones. Es una buena opción para nuestro caso de uso porque tiene muy pocas bibliotecas y utilidades adicionales, lo que ayuda a minimizar nuestra superficie de ataque.
Si, por ejemplo, compilabas una app que necesitaba acceso a algunas bibliotecas C comunes o aún no estabas seguro de los requisitos de tu app, es posible que el completador sea una mejor opción.
5. Compila y ejecuta la app nativa
Una vez que todo esté en su lugar, deberíamos poder compilar nuestra imagen y ejecutar nuestra app nativa compilada.
Antes de ejecutar la compilación, ten en cuenta lo siguiente:
- Esto tardará más tiempo que una compilación normal (unos minutos)
- Este proceso de compilación puede consumir mucha memoria (algunos gigabytes).
- Este proceso de compilación requiere que se pueda acceder al daemon de Docker.
- Si bien en este ejemplo seguimos el proceso de forma manual, también puedes configurar tus fases de compilación para activar automáticamente un perfil de compilación nativo.
Para compilar nuestra imagen, haz lo siguiente:
mvn spring-boot:build-image
Una vez que se compile, todo estará listo para ver la app nativa en acción.
Para ejecutar nuestra app, sigue estos pasos:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
En este punto, estamos en una excelente posición para ver ambos lados de la ecuación de la aplicación nativa.
Dimos un poco de tiempo y un uso de memoria adicional en el tiempo de compilación, pero a cambio obtenemos una aplicación que puede iniciarse mucho más rápido y consumir mucha menos memoria (según la carga de trabajo).
Si ejecutamos docker images demo
para comparar el tamaño de la imagen nativa con la original, podemos ver una reducción significativa:
También debemos tener en cuenta que, en casos de uso más complejos, se necesitan modificaciones adicionales para informar al compilador de AOT lo que hará tu app durante el tiempo de ejecución. Por esa razón, es posible que ciertas cargas de trabajo predecibles (como los trabajos por lotes) sean muy adecuadas para esto, mientras que otras pueden ser más eficaces.
6. Implementamos nuestra app nativa
Para implementar nuestra app en Cloud Run, necesitaremos que nuestra imagen nativa esté en un administrador de paquetes, como Artifact Registry.
1. Preparamos nuestro repositorio de Docker
Para comenzar este proceso, crearemos un repositorio:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
A continuación, nos aseguraremos de que se nos autentique para enviar el contenido a nuestro nuevo registro.
La CLI de gcloud puede simplificar bastante ese proceso:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Enviamos nuestra imagen a Artifact Registry
A continuación, etiquetaremos nuestra imagen:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Luego, podemos usar docker push
para enviarlo a Artifact Registry:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. Implementa en Cloud Run
Ya está todo listo para implementar la imagen que almacenamos en Artifact Registry en Cloud Run:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Como compilamos y, luego, implementamos nuestra app como una imagen nativa, podemos estar seguros de que nuestra aplicación aprovecha de forma excelente los costos de nuestra infraestructura mientras se ejecuta.
No dudes en comparar los tiempos de inicio de nuestra app de referencia con esta nueva app nativa.
7. Resumen/Limpieza
¡Felicitaciones por compilar e implementar una aplicación nativa de Spring en Google Cloud!
Esperamos que este instructivo te anime a familiarizarte más con el proyecto Spring Native y que lo tengas en cuenta si satisface tus necesidades en el futuro.
Opcional: Limpia o inhabilita el servicio
Ya sea que hayas creado un proyecto de Google Cloud para este codelab o que reutilices uno existente, ten cuidado para evitar cargos innecesarios de los recursos que usamos.
Puedes borrar o inhabilitar los servicios de Cloud Run que creamos, borrar la imagen que alojamos o cerrar todo el proyecto.
8. Recursos adicionales
Si bien el proyecto de Spring Native es actualmente un proyecto nuevo y experimental, ya hay una gran cantidad de buenos recursos para ayudar a los usuarios pioneros a solucionar problemas y participar:
Recursos adicionales
A continuación, se incluyen recursos en línea que pueden ser relevantes para este instructivo:
- Más información sobre los indicadores nativos
- Más información sobre GraalVM
- Cómo participar
- Error de falta de memoria al compilar imágenes nativas
- Error de inicio de la aplicación
Licencia
Este trabajo cuenta con una licencia Atribución 2.0 Genérica de Creative Commons.