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 allons passer en revue ses composants, l'historique récent du projet, quelques cas d'utilisation et, bien sûr, les étapes nécessaires pour l'utiliser dans vos projets.

Le projet Spring Native est actuellement en phase expérimentale. Vous devrez donc effectuer une configuration spécifique pour commencer. Toutefois, comme annoncé lors de SpringOne 2021, Spring Native devrait être intégré à Spring Framework 6.0 et Spring Boot 3.0 avec une prise en charge 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 à la volée ait été très bien optimisée pour les processus de longue durée, il existe certains cas d'utilisation dans lesquels les applications compilées à l'avance sont encore plus performantes. Nous en parlerons lors de l'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

Quel est votre niveau d'expérience avec Java ?

Débutant Intermédiaire Expert

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

Débutant Intermédiaire Expert

2. Arrière-plan

Le projet Spring Native utilise plusieurs technologies pour offrir aux développeurs des performances d'application natives.

Pour bien comprendre Spring Native, il est utile de connaître certaines de ces technologies de composants, ce qu'elles nous permettent de faire et comment elles fonctionnent ensemble.

Compilation anticipée (ou "compilation AOT")

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

Ce processus est à l'origine de la portabilité caractéristique de Java, qui permet d'écrire du code une seule fois et de l'exécuter partout. Toutefois, il est coûteux par rapport à l'exécution de code natif.

Heureusement, la plupart des implémentations de la JVM utilisent la compilation à la volée pour atténuer ce coût d'interprétation. Pour ce faire, le nombre d'appels d'une fonction est comptabilisé. Si ce nombre est suffisamment élevé pour dépasser un seuil ( 10 000 par défaut), la fonction est compilée en code natif au moment de l'exécution pour éviter toute interprétation coûteuse.

La compilation AOT adopte l'approche inverse en compilant tout le code accessible dans un exécutable natif au moment de la compilation. Cela permet de gagner en efficacité de mémoire et d'améliorer les performances lors de l'exécution, mais au détriment de la portabilité.

5042e8e62a05a27.png

Il s'agit bien sûr d'un compromis qui ne vaut pas toujours la peine d'être fait. Toutefois, la compilation AOT peut être très utile dans certains cas d'utilisation, par exemple :

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

Pour l'anecdote, la compilation AOT a été introduite en tant que fonctionnalité expérimentale dans JDK 9. Toutefois, cette implémentation était coûteuse à maintenir et n'a jamais vraiment pris de l'ampleur. Elle a donc été discrètement supprimée dans Java 17 au profit de l'utilisation de GraalVM par les développeurs.

GraalVM

GraalVM est une distribution JDK Open Source hautement optimisée qui offre des temps de démarrage extrêmement rapides, une compilation d'image native AOT et des capacités polyglottes permettant aux développeurs de combiner plusieurs langages dans une même application.

GraalVM est en cours de développement. De nouvelles fonctionnalités sont ajoutées et les fonctionnalités existantes sont améliorées en permanence. J'encourage donc les développeurs à rester à l'écoute.

Voici quelques-uns des jalons récents :

  • Nouvelle sortie de compilation d'images natives conviviale ( 18/01/2021)
  • Prise en charge de Java 17 ( 18/01/2022)
  • Activation par défaut de la compilation multicouche pour améliorer les temps de compilation polyglottes ( 2021-04-20)

Spring Native

En d'autres termes, Spring Native permet d'utiliser le compilateur native-image de GraalVM pour transformer les applications Spring en exécutables natifs.

Ce processus consiste à effectuer une analyse statique de votre application au moment de la compilation pour trouver toutes les méthodes de votre application qui sont accessibles à partir du point d'entrée.

Cela crée essentiellement une conception "en univers clos" de votre application, où tout le code est supposé être connu au moment de la compilation et où aucun nouveau code ne peut ê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 classique et qui impose des limites sur certains aspects de Java.

Dans certains cas, aucune modification du code n'est requise pour qu'une application fonctionne avec Spring Native. Toutefois, certaines situations nécessitent une configuration native spécifique pour fonctionner correctement. Dans ce cas, Spring Native fournit souvent des conseils 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 pour établir une référence de performances à laquelle nous pourrons comparer la version native ultérieurement.

1. Créer le projet

Nous allons commencer par obtenir notre application depuis 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 spring-native au moment de la rédaction.

Notez que depuis la sortie de GraalVM 21.0.3, vous pouvez également utiliser Java 17 pour cet exemple. Nous allons toujours utiliser Java 11 pour ce tutoriel afin de minimiser la configuration requise.

Une fois que nous avons notre fichier ZIP sur 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 allons rapidement ajouter un signe de vie et présenter les performances de Spring Native une fois que nous l'aurons exécuté.

Modifiez DemoApplication.java pour qu'il corresponde à ce qui suit :

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 référence est prête à l'emploi. N'hésitez pas à créer une image et à l'exécuter localement pour vous faire une idée du temps de démarrage avant de la convertir en application native.

Pour créer notre image :

mvn spring-boot:build-image

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

Pour exécuter notre application :

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

3. Déployer l'application de référence

Maintenant que nous avons notre application, nous allons la déployer et noter les temps de démarrage, que nous comparerons plus tard à ceux de notre application native.

Selon le type d'application que vous créez, il existe plusieurs options d'hébergement pour vos données.

Toutefois, comme notre exemple est une application Web très simple, nous pouvons nous en tenir à Cloud Run.

Si vous suivez ce tutoriel sur votre propre machine, assurez-vous d'avoir installé et mis à jour l'outil gcloud CLI.

Si vous utilisez Cloud Shell, tout sera pris en charge. Vous pouvez simplement 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

Comme ce projet est encore en phase expérimentale, nous devrons configurer notre application pour qu'elle puisse trouver les artefacts expérimentaux, qui ne sont pas disponibles dans le dépôt central de Maven.

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

Ajoutez les sections "repositories" et "pluginRepositories" suivantes à notre fichier 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

Ensuite, ajoutez la dépendance spring-native, qui est 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/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 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 tiny builder n'est qu'une des nombreuses 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 développez une application qui a besoin d'accéder à des bibliothèques C courantes ou si vous n'êtes pas encore sûr des exigences de votre application, le full-builder peut être plus adapté.

5. Compiler et exécuter l'application native

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

Avant d'exécuter la compilation, voici quelques points à garder à l'esprit :

  • Cette opération prendra plus de temps qu'une compilation normale (quelques minutes) d420322893640701.png
  • Ce processus de compilation peut consommer beaucoup de mémoire (quelques gigaoctets). cda24e1eb11fdbea.png
  • Ce processus de compilation nécessite que le daemon Docker soit accessible.
  • Dans cet exemple, nous allons passer en revue le processus manuellement, mais vous pouvez également configurer vos phases de compilation pour déclencher automatiquement un profil de compilation natif.

Pour créer notre image :

mvn spring-boot:build-image

Une fois l'application native créée, nous sommes prêts à la voir en action.

Pour exécuter notre application :

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

À ce stade, nous sommes bien placés pour voir les deux côtés de l'équation de l'application native.

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

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

e667f65a011c1328.png

Il convient également de noter que, dans les cas d'utilisation plus complexes, des modifications supplémentaires sont nécessaires pour informer le compilateur AOT de ce que votre application fera au moment de l'exécution. C'est pourquoi certaines charges de travail prévisibles (comme les jobs par lot) peuvent être très bien adaptées à cette fonctionnalité, tandis que d'autres peuvent être plus difficiles à migrer.

6. Déployer notre application native

Pour déployer notre application sur Cloud Run, nous devons placer notre image native dans un gestionnaire de packages tel que Artifact Registry.

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

Nous pouvons commencer 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"

Ensuite, nous devons nous assurer que nous sommes authentifiés pour envoyer à notre nouveau registre.

La gcloud CLI peut simplifier considérablement ce processus :

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

2. Transférer notre image vers Artifact Registry

Ensuite, nous allons taguer 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 l'image que nous avons stockée dans Artifact Registry sur Cloud Run :

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 qu'elle utilise parfaitement nos coûts d'infrastructure lors de son exécution.

N'hésitez pas à comparer les temps de démarrage de notre application de référence à ceux de cette nouvelle application native !

6dde63d35959b1bb.png

7. Résumé/Nettoyage

Félicitations pour la création et le déploiement d'une application Spring Native sur Google Cloud !

Nous espérons que ce tutoriel vous encouragera à vous familiariser davantage avec le projet Spring Native et à le garder à l'esprit s'il 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 en réutilisiez un existant, veillez à éviter les frais inutiles 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 projet nouveau et expérimental, il existe déjà de nombreuses ressources utiles pour aider les premiers utilisateurs à résoudre les problèmes et à s'impliquer :

Ressources supplémentaires

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

Licence

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