Spring Native en Google Cloud

1. Descripción general

En este codelab, aprenderemos sobre el proyecto Spring Native, crearemos una app que lo use y, luego, la implementaremos en Google Cloud.

Repasaremos 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 está configurado para integrarse en el framework Spring 6.0 y Spring Boot 3.0 con compatibilidad de primera clase, por lo que este es el momento perfecto para examinar con más detalle el proyecto unos meses antes de su lanzamiento.

Si bien la compilación justo a tiempo está muy bien optimizada para procesos como procesos de larga duración, hay ciertos casos de uso en los que las aplicaciones compiladas con anticipación tienen un rendimiento aún mejor, 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
  • Implementar 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. Información general

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

Para entender completamente Spring Native, es útil comprender algunos de estos componentes de las tecnologías, lo que nos permiten y cómo funcionan juntos.

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 código de bytes. Este código de bytes solo debe ser comprendido por la máquina virtual Java, por lo que la JVM tendrá que interpretarlo en otras máquinas para que podamos ejecutar nuestro código.

Este proceso es lo que nos proporciona la portabilidad de firmas de Java, ya que nos permite “escribir una vez y ejecutarlo en todas partes”, pero es costoso en comparación con ejecutar código nativo.

Afortunadamente, la mayoría de las implementaciones de JVM usan 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 suficiente frecuencia como para pasar un umbral ( 10,000 de forma predeterminada), se compila en código nativo en el tiempo de ejecución para evitar interpretaciones más costosas.

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

5042e8e62a05a27.png

Esto es, por supuesto, una compensación y no siempre vale la pena aceptarlo. 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 el mantenimiento de esta implementación era costoso y nunca se puso al día, por lo que se quitó de manera silenciosa en Java 17 para favorecer a los desarrolladores que solo usaban 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 mezclar varios idiomas en una sola aplicación.

GraalVM está en desarrollo activo, y adquiere capacidades nuevas y mejora las existentes todo el tiempo, por lo que les recomiendo a los desarrolladores que se mantengan atentos.

Estos son algunos de los eventos importantes más recientes:

  • Un resultado de compilación de imagen nativa nuevo y fácil de usar ( 18/1/2021)
  • Compatibilidad con Java 17 ( 18/1/2022)
  • Habilitación de la compilación de varios niveles de forma predeterminada para mejorar los tiempos de compilación de los políglotas ( 20/4/2021)

nativo de primavera

En pocas palabras, Spring Native habilita 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 tu aplicación a los que se puede acceder desde el punto de entrada.

Esto básicamente crea un "mundo cerrado" la concepción de tu aplicación, en la que se supone que todo el código se conoce en el tiempo de compilación y no se permite cargar ningún 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, tarda más que compilar una aplicación normal y, además, 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 a menudo proporciona sugerencias nativas para simplificar este proceso.

3. Configurar/trabajo previo

Antes de comenzar a implementar Spring Native, necesitaremos 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 desde 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 en que se escribe.

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 en este instructivo para minimizar la configuración involucrada.

Una vez que tenemos el 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 de código

Una vez que el proyecto esté 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 siéntete libre de 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 la 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 la app, haz lo siguiente:

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 luego compararemos con los tiempos de inicio de la app nativa.

Según el tipo de aplicación que estés compilando, hay varios alojamientos diferentes para tu contenido.

Sin embargo, debido a que nuestro ejemplo es una aplicación web muy simple y directa, podemos mantener todo simple y confiar en Cloud Run.

Si estás trabajando en tu propia máquina, asegúrate de tener instalada y actualizada la herramienta gcloud CLI.

Si está en Cloud Shell, ya se solucionará. Solo debe ejecutar lo siguiente en el directorio del código fuente:

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 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 prefieras.

Agrega las siguientes secciones de repositorios 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 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 AOT para mejorar la compatibilidad y la huella de las imágenes nativas ( 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 tiny 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 todavía no estabas seguro de los requisitos de tu app, el compilador completo podría ser una mejor opción.

5. Cómo compilar y ejecutar una app nativa

Una vez que todo esté listo, deberíamos poder compilar la imagen y ejecutar la app nativa compilada.

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

  • Esto llevará más tiempo que una compilación normal (unos minutos) d420322893640701.png
  • Este proceso de compilación puede requerir 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 revisamos el proceso manualmente, también puedes configurar las fases de compilación para activar automáticamente un perfil de compilación nativo.

Para compilar la imagen, haz lo siguiente:

mvn spring-boot:build-image

Una vez que se haya realizado la compilación, estará todo listo para ver la aplicación nativa en acción.

Para ejecutar la 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.

En el tiempo de compilación, renunciamos a un poco de tiempo y al uso de memoria adicional, pero, a cambio, obtenemos una aplicación que puede iniciarse mucho más rápido y consumir mucho 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 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 esa razón, ciertas cargas de trabajo predecibles (como los trabajos por lotes) pueden ser muy adecuadas para esto, mientras que otras pueden tener una efectividad mayor.

6. Implementa nuestra app nativa

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

1. Preparar el repositorio de Docker

Podemos comenzar este proceso creando 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, debemos asegurarnos de estar autenticados para enviar a nuestro registro nuevo.

La CLI de gcloud puede simplificar ese proceso bastante:

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

2. Enviamos la imagen a Artifact Registry

Luego 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 estamos listos 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

Ya que compilamos e implementamos nuestra app como una imagen nativa, podemos estar seguros de que nuestra aplicación hace un uso excelente de los costos de infraestructura mientras se ejecuta.

No dudes en comparar por tu cuenta 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 incentive a familiarizarte más con el proyecto Spring Native y a tenerlo en cuenta si, en el futuro, podría satisfacer tus necesidades.

Opcional: Limpia o inhabilita el servicio

Ya sea que hayas creado un proyecto de Google Cloud para este codelab o que estés reutilizando uno existente, ten cuidado de evitar cargos innecesarios de los recursos que utilizamos.

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 actualmente es un proyecto nuevo y experimental, ya existe una gran cantidad de buenos recursos para ayudar a los usuarios pioneros a solucionar problemas y participar:

Recursos adicionales

A continuación, hay recursos en línea que pueden ser relevantes para este tutorial:

Licencia

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