Spring Native en Google Cloud

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, el historial 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á cierta 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 más de cerca unos meses antes de su lanzamiento.

Si bien la compilación justo a tiempo se optimizó muy bien para 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 de Spring Native
  • Implementa una app de este tipo en Cloud Run

Requisitos

Encuesta

¿Cómo usarás este instructivo?

Ler Leer y completar los ejercicios

¿Cómo calificarías tu experiencia con Java?

Principiante Intermedio Avanzado

¿Cómo calificarías tu experiencia en el uso de los servicios de Google Cloud?

Principiante Intermedio Avanzado

2. Fondo

El proyecto Spring Native usa varias tecnologías para ofrecer a los desarrolladores el rendimiento de las aplicaciones nativas.

Para comprender completamente Spring Native, es útil conocer algunas de estas tecnologías de componentes, lo que nos permiten y cómo funcionan en conjunto aquí.

Compilación AOT

Cuando los desarrolladores ejecutan javac normalmente en el tiempo de compilación, nuestro código fuente .java se compila en archivos .class que se escriben en bytecode. 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 ejecutar nuestro código.

Este proceso es lo que le da a Java su portabilidad característica, lo que nos permite "escribir una vez y ejecutar en cualquier lugar", pero es costoso en comparación con la ejecución de código nativo.

Afortunadamente, la mayoría de las implementaciones de la JVM utilizan la compilación justo a tiempo 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 superar un umbral ( 10,000 de forma predeterminada), se compila en código nativo en el tiempo de ejecución para evitar una interpretación costosa adicional.

La compilación anticipada adopta el enfoque opuesto, ya que compila todo el código accesible en un ejecutable nativo en el momento de la compilación. Esto intercambia la portabilidad por la eficiencia de la memoria y otras mejoras de rendimiento en el tiempo de ejecución.

5042e8e62a05a27.png

Por supuesto, esto es una compensación y no siempre vale la pena. 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 restricciones de memoria significativas en los que el 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ó silenciosamente en Java 17 a favor de que los desarrolladores usaran GraalVM.

GraalVM

GraalVM es una distribución de JDK de código abierto altamente optimizada que ofrece tiempos de inicio extremadamente rápidos, compilación de imágenes nativas AOT y capacidades políglotas que permiten a los desarrolladores combinar varios lenguajes en una sola aplicación.

GraalVM se encuentra en desarrollo activo, adquiere nuevas capacidades y mejora las existentes todo el tiempo, por lo que recomiendo a los desarrolladores que se mantengan al tanto.

Estos son algunos hitos recientes:

  • Se agregó un nuevo resultado de compilación de imágenes nativas fácil de usar ( 2021-01-18)
  • 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 políglota ( 2021-04-20).

Spring Native

En pocas palabras, Spring Native permite usar el compilador native-image de GraalVM para convertir las aplicaciones de Spring en ejecutables nativos.

Este proceso implica realizar un análisis estático de tu aplicación en el momento de la compilación para encontrar todos los métodos de tu aplicación a los que se puede acceder desde el punto de entrada.

Esto crea esencialmente una concepción de "mundo cerrado" de tu aplicación, en la que se supone que todo el código se conoce en el momento de la compilación y no se permite cargar código nuevo en el tiempo de ejecución.

Es importante tener en cuenta que la generación de imágenes nativas es un proceso que requiere mucha memoria y que lleva más tiempo que compilar una aplicación normal, y que impone limitaciones en ciertos aspectos de Java.

En algunos casos, no se requieren cambios de 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 valor de referencia del 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 inicio usa Spring Boot 2.6.4, que es la versión más reciente que admite el proyecto spring-native en el momento de la redacción.

Ten en cuenta que, desde el lanzamiento de GraalVM 21.0.3, también puedes usar Java 17 para esta muestra. Seguiremos usando Java 11 en 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 una señal de vida y mostraremos el rendimiento de Spring Native cuando 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 puedes compilar una imagen y ejecutarla de forma local para tener 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 tener una idea del tamaño de la imagen de referencia: 6ecb403e9af1475e.png

Para ejecutar nuestra app, sigue estos pasos:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3. Implementa la app 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 compiles, existen varias formas diferentes de alojar tus cosas.

Sin embargo, como nuestro ejemplo es una aplicación web muy simple y directa, podemos mantener las cosas sencillas y confiar en Cloud Run.

Si sigues los pasos en tu propia máquina, asegúrate de tener instalada y actualizada la herramienta de la CLI de gcloud.

Si estás en Cloud Shell, todo se solucionará y podrás ejecutar lo siguiente en el directorio de origen:

gcloud run deploy

4. Configuración de la aplicación

1. Cómo configurar 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. Cómo agregar nuestras dependencias

A continuación, agrega la dependencia spring-native, que se requiere 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. Cómo agregar o habilitar nuestros complementos

Ahora agrega el complemento de AOT para mejorar la compatibilidad y el tamaño de la imagen nativa ( 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 usar 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 las 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, estabas compilando una app que necesitaba acceso a algunas bibliotecas de C comunes o aún no conocías los requisitos de tu app, es posible que el full-builder 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 compilada nativa.

Antes de ejecutar la compilación, ten en cuenta lo siguiente:

  • Este proceso tardará más que una compilación normal (unos minutos). d420322893640701.png
  • Este proceso de compilación puede consumir mucha memoria (algunos gigabytes). cda24e1eb11fdbea.png
  • Este proceso de compilación requiere que se pueda acceder al daemon de Docker
  • Si bien en este ejemplo realizamos 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, estará todo listo para ver la app nativa en acción.

Para ejecutar nuestra app, haz lo siguiente:

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.

Sacrificamos un poco de tiempo y uso de memoria adicional durante la 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 el original, podemos ver una reducción drástica:

e667f65a011c1328.png

También debemos tener en cuenta que, en casos de uso más complejos, se necesitan modificaciones adicionales para informar al compilador AOT lo que hará tu app en el tiempo de ejecución. Por ese motivo, ciertas cargas de trabajo predecibles (como los trabajos por lotes) pueden ser muy adecuadas para esto, mientras que otras pueden requerir más esfuerzo.

6. Implementa nuestra app nativa

Para implementar nuestra app en Cloud Run, tendremos que colocar nuestra imagen nativa en un administrador de paquetes como Artifact Registry.

1. Cómo preparar 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 estemos autenticados para enviar a nuestro nuevo registro.

La CLI de gcloud puede simplificar bastante ese proceso:

gcloud auth configure-docker us-central1-docker.pkg.dev

2. Envía 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

Ahora podemos implementar en Cloud Run la imagen que almacenamos en Artifact Registry:

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

Dado que compilamos e implementamos nuestra app como una imagen nativa, podemos tener la certeza de que nuestra aplicación está aprovechando al máximo los costos de infraestructura mientras se ejecuta.

No dudes en comparar los tiempos de inicio de nuestra app de referencia con esta nueva app nativa.

6dde63d35959b1bb.png

7. Resumen/Limpieza

¡Felicitaciones por compilar e implementar una aplicación de Spring Native en Google Cloud!

Esperamos que este instructivo te anime a familiarizarte más con el proyecto Spring Native y a tenerlo 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 por 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 Spring Native es actualmente un proyecto nuevo y experimental, ya hay una gran cantidad de recursos útiles para ayudar a los primeros usuarios a solucionar problemas y participar:

Recursos adicionales

A continuación, se incluyen recursos en línea que pueden ser pertinentes para este instructivo:

Licencia

Este trabajo cuenta con una licencia Atribución 2.0 Genérica de Creative Commons.