Spring Native on Google Cloud

1. Présentation

Dans cet atelier de programmation, nous allons découvrir le projet Spring Native, créer une application qui l'utilise et la déployer sur Google Cloud.

Nous passerons en revue ses composants, l'historique récent du projet, quelques cas d'utilisation et, bien sûr, les étapes requises pour l'utiliser dans vos projets.

Le projet Spring Native est actuellement en phase de test. Il nécessite donc une configuration spécifique pour commencer. Toutefois, comme nous l'avons annoncé lors de l'événement SpringOne 2021, la version Spring Native sera intégrée au framework Spring Framework 6.0 et à Spring Boot 3.0 avec une compatibilité de premier ordre. C'est donc le moment idéal pour examiner de plus près le projet quelques mois avant sa sortie.

Bien que la compilation juste-à-temps ait été très bien optimisée pour des éléments tels que les processus de longue durée, il existe des cas d'utilisation dans lesquels les applications compilées à l'avance sont encore plus performantes. Nous en parlerons dans cet atelier de programmation.

Vous apprendrez à

  • Utiliser Cloud Shell
  • Activer l'API Cloud Run
  • Créer et déployer une application Spring Native
  • Déployer une telle application sur Cloud Run

Prérequis

Enquête

Comment allez-vous utiliser ce tutoriel ?

Je vais le lire uniquement Je vais le lire et effectuer les exercices

Comment évalueriez-vous votre expérience avec Java ?

Débutant Intermédiaire Expert

Quel est votre niveau d'expérience avec les services Google Cloud ?

<ph type="x-smartling-placeholder"></ph> Débutant Intermédiaire Expert
.

2. Contexte

Le projet Spring Native utilise plusieurs technologies pour fournir aux développeurs les performances des applications natives.

Pour bien comprendre le fonctionnement de Spring Native, il est utile de comprendre quelques-unes de ces technologies de composants, ce qu'elles nous apportent et comment elles fonctionnent ensemble.

Compilation anticipée (ou "compilation AOT")

Lorsque les développeurs exécutent normalement javac au moment de la compilation, notre code source .java est compilé dans des fichiers .class écrits en bytecode. Ce bytecode est destiné uniquement à être compris par la machine virtuelle Java. La JVM devra donc interpréter ce code sur d'autres machines pour que nous puissions l'exécuter.

C'est ce processus qui nous permet de bénéficier de la portabilité de signature de Java, qui nous permet d'écrire une seule fois et de s'exécuter n'importe où, mais cela coûte cher par rapport à l'exécution de code natif.

Heureusement, la plupart des implémentations de la JVM utilisent la compilation juste-à-temps pour réduire les coûts d'interprétation. Pour ce faire, vous devez compter les appels d'une fonction. Si elle est appelée suffisamment souvent pour dépasser un seuil ( 10 000 par défaut), elle est compilée en code natif au moment de l'exécution afin d'éviter toute interprétation coûteuse.

La compilation anticipée (ou compilation anticipée) adopte l'approche inverse, en compilant tout le code accessible dans un exécutable natif au moment de la compilation. Vous troquez la portabilité au détriment de l'efficacité de la mémoire et d'autres gains de performances au moment de l'exécution.

5042e8e62a05a27.png

Il s'agit bien sûr d'un compromis et cela n'en vaut pas toujours la peine. Cependant, la compilation AOT peut briller dans certains cas d'utilisation, par exemple:

  • Applications de courte durée pour lesquelles le temps de démarrage est important
  • Environnements fortement limités en mémoire où le JIT peut être trop coûteux

La compilation AOT a été introduite en tant que fonctionnalité expérimentale dans JDK 9, bien que cette implémentation soit coûteuse à gérer et qu'elle n'ait jamais été adoptée. Elle a donc été suffisamment supprimée dans Java 17 au profit des développeurs qui n'utilisaient que GraalVM.

GraalVM

GraalVM est une distribution JDK Open Source hautement optimisée qui offre des temps de démarrage ultrarapides, la compilation d'images native AOT et des fonctionnalités polyglottes qui permettent aux développeurs de combiner plusieurs langues dans une seule application.

GraalVM est en cours de développement afin de développer de nouvelles fonctionnalités et d'améliorer les fonctionnalités existantes en permanence. J'encourage donc les développeurs à rester à l'écoute.

Voici quelques-unes des étapes récentes:

  • Nouvelle sortie de compilation d'image native conviviale ( 18/01/2021)
  • Compatibilité avec Java 17 ( 18/01/2022)
  • Activation par défaut de la compilation à plusieurs niveaux pour améliorer les temps de compilation des polyglottes ( 20/04/2021)

Spring Native

En termes simples, Spring Native permet d'utiliser le compilateur d'images natives de GraalVM pour transformer les applications Spring en exécutables natifs.

Ce processus implique l'exécution d'une analyse statique de votre application au moment de la compilation afin de trouver toutes les méthodes de votre application accessibles à partir du point d'entrée.

Cela crée essentiellement un « monde fermé » de votre application, où tout le code est supposé être connu au moment de la compilation et où aucun nouveau code n'est autorisé à être chargé au moment de l'exécution.

Il est important de noter que la génération d'images natives est un processus gourmand en mémoire qui prend plus de temps que la compilation d'une application standard et impose des limites sur certains aspects de Java.

Dans certains cas, aucune modification de code n'est nécessaire pour qu'une application fonctionne avec Spring Native. Cependant, certaines situations nécessitent une configuration native spécifique pour fonctionner correctement. Dans ce cas, Spring Native fournit souvent des indicateurs natifs pour simplifier ce processus.

3. Configuration/Préparation

Avant de commencer à implémenter Spring Native, nous devons créer et déployer notre application afin d'établir une référence des performances que nous pourrons comparer à la version native ultérieurement.

1. Créer le projet

Pour commencer, nous allons télécharger notre application à partir 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

Cette application de démarrage utilise Spring Boot 2.6.4, qui est la dernière version compatible avec le projet natif printan au moment de la rédaction.

Notez que depuis le lancement de GraalVM 21.0.3, vous pouvez également utiliser Java 17 pour cet exemple. Dans ce tutoriel, nous utiliserons tout de même Java 11 afin de minimiser la configuration requise.

Une fois le fichier ZIP dans la ligne de commande, nous pouvons créer un sous-répertoire pour notre projet et y décompresser le dossier:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2. Modifications du code

Une fois le projet ouvert, nous ajouterons rapidement un signe de vie et présenterons les performances du Spring Native une fois que nous l'aurons lancé.

Modifiez le fichier DemoApplication.java pour qu'il corresponde à celui-ci:

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();
    }
}

À ce stade, notre application de base est prête à l'emploi. Par conséquent, n'hésitez pas à créer une image et à l'exécuter localement afin d'avoir une idée du temps de démarrage avant de la convertir en application native.

Pour créer l'image:

mvn spring-boot:build-image

Vous pouvez également utiliser docker images demo pour avoir une idée de la taille de l'image de référence: 6ecb403e9af1475e.png

Pour exécuter l'application:

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

3. Déployer une application de référence

Maintenant que nous avons notre application, nous allons la déployer et prendre note des durées, que nous comparerons plus tard aux temps de démarrage de notre application native.

Selon le type d'application que vous développez, plusieurs options s'offrent à vous pour héberger vos contenus.

Cependant, comme notre exemple est une application Web très simple, nous pouvons simplifier les choses et utiliser Cloud Run.

Si vous suivez les étapes sur votre propre machine, assurez-vous que l'outil gcloud CLI est installé et mis à jour.

Si vous êtes dans Cloud Shell, tout va être pris en charge. Il vous suffit d'exécuter la commande suivante dans le répertoire source:

gcloud run deploy

4. Configuration de l'application

1. Configurer nos dépôts Maven

Ce projet étant encore en phase expérimentale, nous devrons configurer notre application pour pouvoir trouver des artefacts expérimentaux qui ne sont pas disponibles dans le dépôt central de Maven.

Pour cela, vous allez ajouter les éléments suivants à notre fichier pom.xml, ce que vous pouvez faire dans l'éditeur de votre choix.

Ajoutez les sections "Repository" et "pluginRepositories" suivantes à notre 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. Ajouter nos dépendances

Ajoutez ensuite la dépendance Spring-native, nécessaire pour exécuter une application Spring en tant qu'image native. Remarque: Cette étape n'est pas nécessaire si vous utilisez Gradle.

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3. Ajouter et activer nos plug-ins

Ajoutez maintenant le plug-in AOT pour améliorer la compatibilité et l'empreinte des images natives ( en savoir plus):

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

Nous allons maintenant mettre à jour le plug-in Spring-boot-maven-plugin pour activer la prise en charge des images natives et utiliser le compilateur paketo pour créer notre image native:

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

Notez que l'image du compilateur minuscule n'est qu'une des options disponibles. C'est un bon choix pour notre cas d'utilisation, car il comporte très peu de bibliothèques et d'utilitaires supplémentaires, ce qui permet de minimiser notre surface d'attaque.

Par exemple, si vous conceviez une application nécessitant un accès à certaines bibliothèques C courantes ou si vous n'étiez pas encore sûr des exigences de votre application, l'outil full-builder est peut-être mieux adapté.

5. Créer et exécuter une application native

Une fois que tout cela est en place, nous devrions pouvoir créer notre image et exécuter notre application native compilée.

Avant d'exécuter la compilation, gardez à l'esprit les points suivants:

  • Cela prendra plus de temps qu'un build standard (quelques minutes) d420322893640701.png
  • Ce processus de compilation peut utiliser beaucoup de mémoire (quelques gigaoctets) cda24e1eb11fdbea.png
  • Ce processus de compilation nécessite que le daemon Docker soit accessible
  • Dans cet exemple, nous suivons le processus manuellement, mais vous pouvez également configurer vos phases de compilation pour déclencher automatiquement un profil de compilation natif.

Pour créer l'image:

mvn spring-boot:build-image

Une fois que tout cela sera fait, nous pourrons voir l'application native en action.

Pour exécuter l'application:

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

À ce stade, nous sommes idéalement placés pour étudier les deux aspects de l'équation des applications natives.

Nous avons perdu un peu de temps et d'utilisation de mémoire supplémentaire au moment de la compilation, mais en échange, nous obtenons une application qui peut démarrer beaucoup plus rapidement et consommer beaucoup moins de mémoire (en fonction de la charge de travail).

Si nous exécutons docker images demo pour comparer la taille de l'image native à celle d'origine, nous pouvons constater une réduction considérable:

e667f65a011c1328.png

Notez également que dans des cas d'utilisation plus complexes, des modifications supplémentaires sont nécessaires pour indiquer au compilateur AOT ce que fera votre application au moment de l'exécution. C'est pourquoi certaines charges de travail prévisibles (telles que les jobs par lot) peuvent s'avérer très adaptées, tandis que d'autres peuvent représenter un impact plus important.

6. Déployer notre application native

Pour déployer notre application sur Cloud Run, nous devons transférer l'image native dans un gestionnaire de packages tel qu'Artifact Registry.

1. Préparer le dépôt Docker

Nous pouvons démarrer ce processus en créant un dépôt:

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

Nous devons maintenant nous assurer que nous sommes authentifiés pour transférer les données vers notre nouveau registre.

La gcloud CLI peut simplifier considérablement ce processus:

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

2. Transférer l'image vers Artifact Registry

Nous allons ensuite ajouter des tags à notre image:

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

Nous pouvons ensuite utiliser docker push pour l'envoyer à Artifact Registry:

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

3. Déployer sur Cloud Run

Nous sommes maintenant prêts à déployer dans Cloud Run l'image que nous avons stockée dans Artifact Registry:

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

Comme nous avons créé et déployé notre application en tant qu'image native, nous pouvons être sûrs que notre application utilise à bon escient nos coûts d'infrastructure lors de son exécution.

N'hésitez pas à comparer vous-même les temps de démarrage de notre application de référence à celle de cette nouvelle application native.

6dde63d35959b1bb.png

7. Résumé/Nettoyage

Félicitations ! Vous avez créé et déployé une application Spring Native sur Google Cloud.

Nous espérons que ce tutoriel vous aidera à vous familiariser avec le projet Spring Native et à le garder à l'esprit si cela répond à vos besoins à l'avenir.

Facultatif: Nettoyer et/ou désactiver le service

Que vous ayez créé un projet Google Cloud pour cet atelier de programmation ou que vous réutilisiez un projet existant, veillez à ne pas vous facturer inutilement des frais liés aux ressources que nous avons utilisées.

Vous pouvez supprimer ou désactiver les services Cloud Run que nous avons créés, supprimer l'image que nous avons hébergée ou arrêter l'ensemble du projet.

8. Ressources supplémentaires

Bien que le projet Spring Native soit actuellement un nouveau projet expérimental, il existe déjà de nombreuses ressources de qualité pour aider les utilisateurs de la première heure à résoudre les problèmes et à s'impliquer:

Ressources supplémentaires

Vous trouverez ci-dessous des ressources en ligne qui peuvent vous être utiles pour ce tutoriel:

Licence

Ce document est publié sous une licence Creative Commons Attribution 2.0 Generic.