Spring Native en Google Cloud

1. Resumen

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

Repasaremos los componentes, el historial reciente del proyecto, algunos casos de uso y, por supuesto, los pasos necesarios para que puedas usarlo en tus proyectos.

El proyecto Spring Native se encuentra en una fase experimental, por lo que necesitará una configuración específica para comenzar. Sin embargo, como se anunció en SpringOne 2021, Spring Native está integrado en Spring Framework 6.0 y Spring Boot 3.0 con compatibilidad de primera clase, por lo que es el momento perfecto para analizar el proyecto en detalle. unos meses antes de su lanzamiento.

Si bien la compilación justo a tiempo se optimizó para aspectos como los procesos de ejecución prolongada, hay ciertos casos de uso en los que las aplicaciones compiladas con anticipación funcionan aún mejor, que analizaremos durante el codelab.

Obtendrás información sobre cómo hacer las siguientes acciones:

  • Uso de Cloud Shell
  • Habilitar la API de Cloud Run
  • Crea e implementa una app de Spring Native
  • Implementa una app de este tipo en Cloud Run

Requisitos

Survey

¿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 con los servicios de Google Cloud?

Principiante Intermedio Avanzado

2. Información general

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

Para comprender completamente a Spring Native, es útil comprender algunas de estas tecnologías de componentes, qué es lo que nos habilitan y cómo funcionan juntas.

Compilación AOT

Cuando los desarrolladores ejecutan javac normalmente en el momento de la compilación, nuestro código fuente .java se compila en archivos .class escritos en código de bytes. Solo la máquina virtual Java debe interpretar este código de bytes, por lo que la JVM deberá interpretarlo en otras máquinas para poder ejecutar nuestro código.

Este proceso es lo que nos brinda la portabilidad de la firma de Java, ya que nos permite "escribir una vez y ejecutar en todas partes", pero es costoso en comparación con ejecutar código nativo.

Afortunadamente, la mayoría de las implementaciones de la JVM utilizan una compilación justo a tiempo para mitigar este costo de interpretación. Para lograr esto, se cuentan las invocaciones de una función. Si se la invoca con la frecuencia suficiente para superar un umbral ( 10,000 de forma predeterminada), se compila en el código nativo durante el tiempo de ejecución a fin de evitar una interpretación más costosa.

La compilación anticipada tiene el enfoque opuesto, que compila todo el código alcanzable en un ejecutable nativo durante la compilación. Esto cambia la portabilidad para la eficiencia de la memoria y otras mejoras de rendimiento en el momento de la ejecución.

5042e8e62a05a27.png

Esta es una compensación, y no siempre vale la pena tomarla. Sin embargo, la compilación AOT puede brillar en ciertos casos de uso, como los siguientes:

  • Aplicaciones de corta duración en las que el tiempo de inicio es importante
  • Entornos con alta restricción de memoria en los que JIT podría 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 y nunca se llegó a recuperar, por lo que se quitó de manera discreta en Java 17 a favor de los desarrolladores que solo usan GraalVM.

GraalVM;

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

GraalVM está en desarrollo activo, ganando nuevas capacidades y mejorando las existentes todo el tiempo, por eso alento a los desarrolladores a mantenerse al tanto.

Estos son algunos de los logros más recientes:

  • Resultado de compilación de una imagen nativa nueva y fácil de usar ( 18/01/2021)
  • Compatibilidad con Java 17 ( 18/01/2022)
  • Habilita la compilación de varios niveles de forma predeterminada para mejorar los tiempos de compilación de Polyglot ( 20/04/2021)

Nativo de la primavera

En pocas palabras, Spring Native permite usar el compilador de imágenes nativas de GraalVM para convertir aplicaciones de Spring en archivos 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 en tu aplicación a los que se puede acceder desde el punto de entrada.

Básicamente, esto crea una concepción de "mundo cerrado" de tu aplicación, en la que se supone que se conoce todo el código 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 que requiere mucha memoria y que lleva más tiempo que la compilación de una aplicación regular e impone limitaciones para 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, tendremos que 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 inicio usa Spring Boot 2.6.4, que es la última versión que admite el proyecto nativo de primavera cuando se escribe.

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 el archivo ZIP en la línea de comandos, podremos 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 abierto el proyecto, rápidamente agregaremos un letrero de vida y mostraremos el rendimiento de Spring Native una vez que lo ejecutemos.

Edite la aplicación DemoApplication.java para que coincida:

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 que la uses, así que no dudes en compilar una imagen y ejecutarla de manera local a fin de tener una idea del tiempo de inicio antes de convertirla en una aplicación nativa.

Para compilar la imagen, sigue estos pasos:

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:

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

3. Implemente la aplicación de referencia

Ahora que tenemos nuestra app, la implementaremos y anotaremos 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 distintas opciones de hosting para tu contenido.

Sin embargo, dado que nuestro ejemplo es una aplicación web muy simple y directa, podemos hacer que las cosas sean simples y confiar en Cloud Run.

Si realiza el seguimiento en su propia máquina, asegúrese de tener instalada y actualizada la herramienta gcloud CLI.

Si está en Cloud Shell, se encargará de todo y simplemente podrá 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

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

Para ello, deberá agregar los siguientes elementos a nuestro archivo pom.xml. Puede hacerlo en el editor que prefiera.

Agregue las siguientes secciones y los complementos de 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

Luego, agregue la dependencia nativa de resorte, que es necesaria 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 y habilitar nuestros complementos

Ahora, agrega el complemento AOT para mejorar la huella y la compatibilidad 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 para habilitar la compatibilidad con imágenes nativas y usaremos el compilador 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 tiny compilador es solo una de varias opciones. Es una buena opción para nuestro caso de uso, ya que tiene muy pocas bibliotecas y utilidades adicionales, lo que ayuda a minimizar nuestra superficie de ataque.

Por ejemplo, si compilaste una app que necesitaba acceso a algunas bibliotecas comunes de C o si no estabas seguro de los requisitos de tu app, el compilador completo podría ser la mejor opción.

5. Cómo compilar y ejecutar una app nativa

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

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

  • Esto tardará más que una compilación normal (algunos 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
  • En este ejemplo, se realiza el proceso de forma manual, pero también puedes configurar tus fases de compilación para activar automáticamente un perfil de compilación nativo.

Para compilar la imagen, sigue estos pasos:

mvn spring-boot:build-image

Una vez que esté compilado, ya podemos ver a la aplicación nativa en acción.

Para ejecutar nuestra app:

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

En este punto, nos encontramos en una excelente posición para ver ambos lados de la ecuación de la aplicación nativa.

Dedicamos un poco de tiempo y uso adicional de memoria 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 observar una reducción drástica:

e667f65a011c1328.png

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

6. Cómo implementar nuestra app nativa

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

1. Cómo preparar nuestro repositorio de Docker

Para iniciar este proceso, podemos crear un repositorio:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

Luego, debemos asegurarnos de que estamos autenticados para enviar a nuestro nuevo registro.

La CLI de gcloud puede simplificar ese proceso un poco:

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

Ya estamos listos para 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 imagen nativa, podemos tener la seguridad de que nuestra aplicación hace un uso excelente de nuestros costos de infraestructura mientras se ejecuta.

Puedes comparar el tiempo de inicio de nuestra app de referencia con el de esta nueva para ti mismo.

6dde63d35959b1bb.png

7. Resumen/Limpieza

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

Esperamos que este instructivo te aliente a familiarizarte con el proyecto Spring Native y tenerlo en cuenta si satisface tus necesidades en el futuro.

Opcional: Cómo inhabilitar o limpiar el servicio

Ya sea que hayas creado un proyecto de Google Cloud para este codelab o que vuelvas a usar uno existente, asegúrate de evitar cargos innecesarios en 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 este proyecto es actualmente un proyecto nuevo y experimental, hay muchos recursos útiles que ayudan a los usuarios pioneros a solucionar problemas y participar:

Recursos adicionales

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

Licencia

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