1. Présentation
Dans cet atelier de programmation, vous allez 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, certains cas d'utilisation et, bien sûr, les étapes à suivre 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 assistance de premier plan. 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 certains cas d'utilisation dans lesquels les applications compilées à l'avance fonctionnent encore mieux. 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
- Un projet Google Cloud Platform avec un compte de facturation GCP actif
- CLI gcloud installée ou accès à Cloud Shell
- Compétences de base en Java et XML
- Une connaissance correcte des commandes Linux courantes
Enquête
Comment allez-vous utiliser ce tutoriel ?
Comment évalueriez-vous votre niveau d'expérience avec Java ?
Quel est votre niveau d'expérience avec les services Google Cloud ?
2. Contexte
Le projet Spring Native utilise plusieurs technologies pour fournir aux développeurs des performances d'application natives.
Pour comprendre pleinement Spring Native, il est utile de comprendre certaines de ces technologies de composants, ce qu'elles nous permettent et comment elles fonctionnent ensemble.
Compilation anticipée (ou "compilation AOT")
Lorsque les développeurs exécutent javac normalement au moment de la compilation, le code source .java est compilé en fichiers .class écrits en bytecode. Ce bytecode n'est destiné qu'à être compris 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.
C'est ce processus qui nous offre la portabilité de signature Java, qui nous permet d'écrire une fois et d'exécuter partout, mais il est coûteux par rapport à l'exécution du code natif.
Heureusement, la plupart des implémentations de la JVM utilisent la compilation juste-à-temps pour atténuer ce coût d'interprétation. Pour ce faire, le nombre d'appels d'une fonction est compté. 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 pour éviter toute autre interprétation coûteuse.
La compilation anticipée adopte l'approche inverse, en compilant tout le code accessible dans un exécutable natif au moment de la compilation. Cela échange la portabilité contre l'efficacité de la mémoire et d'autres gains de performances au moment de l'exécution.
Il s'agit bien sûr d'un compromis qui n'est pas toujours intéressant. Toutefois, la compilation AOT peut être utile dans certains cas d'utilisation, par exemple:
- Applications de courte durée où le temps de démarrage est important
- Environnements très limités en mémoire où le JIT peut être trop coûteux
À titre d'information, 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 n'ait jamais vraiment pris, elle a été silencieusement supprimée dans Java 17 au profit des développeurs qui n'utilisent que GraalVM.
GraalVM
GraalVM est une distribution JDK Open Source hautement optimisée qui offre des temps de démarrage extrêmement rapides, une compilation d'images natives 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 actif, et gagne en permanence de nouvelles fonctionnalités et améliore les existantes. Je vous invite donc à rester à l'affût.
Voici quelques jalons récents:
- Nouvelle sortie de compilation d'images natives conviviale ( 18/01/2021)
- Prise en charge de Java 17 ( 18/01/2022)
- Activation de la compilation multicouche par défaut pour améliorer les temps de compilation polyglotte ( 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 consiste à effectuer 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 une conception "monde fermé" de votre application, où tout le code est supposé être connu au moment de la compilation et qu'aucun nouveau code n'est autorisé à être chargé au moment de l'exécution.
Notez 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 qui impose des limites à certains aspects de Java.
Dans certains cas, aucune modification de code n'est nécessaire pour qu'une application fonctionne avec Spring Native. Toutefois, dans certains cas, une configuration native spécifique est nécessaire pour que le service fonctionne correctement. Dans ce cas, Spring Native fournit souvent des indices 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 de performances que nous pourrons comparer à la version native plus tard.
1. Créer le projet
Nous allons commencer par récupérer notre application sur 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 réduire la configuration requise.
Une fois le 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 de 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'exécuterons.
Modifiez DemoApplication.java pour obtenir le code suivant:
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 donc 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:
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, que nous comparerons plus tard aux temps de démarrage de notre application native.
En fonction du type d'application que vous créez, il existe plusieurs options d'hébergement.
Toutefois, comme notre exemple est une application Web très simple et directe, nous pouvons simplifier les choses et nous appuyer sur Cloud Run.
Si vous suivez la procédure sur votre propre ordinateur, assurez-vous d'avoir installé et mis à jour l'outil gcloud CLI.
Si vous utilisez Cloud Shell, tout sera géré et 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
Étant donné que ce projet est encore en phase expérimentale, nous devrons configurer notre application pour qu'elle puisse trouver des artefacts expérimentaux, qui ne sont pas disponibles dans le dépôt central de Maven.
Pour ce faire, vous devez 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
Ajoutez ensuite la dépendance spring-native, qui est requise 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 du petit compilateur 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 réduire notre surface d'attaque.
Par exemple, si vous créez une application qui a besoin d'accéder à certaines bibliothèques C courantes ou si vous n'êtes pas encore sûr des exigences de votre application, le compilateur complet peut être plus adapté.
5. Créer et exécuter une application native
Une fois tout 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:
- Cette opération prend plus de temps qu'une compilation normale (quelques minutes).
- Ce processus de compilation peut utiliser beaucoup de mémoire (quelques gigaoctets)
- 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 notre image:
mvn spring-boot:build-image
Une fois cette étape terminée, nous sommes prêts à voir l'application native en action.
Pour exécuter notre application:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
À ce stade, nous sommes en mesure de voir les deux côtés de l'équation des applications natives.
Nous avons sacrifié un peu de temps et une utilisation supplémentaire de la mémoire 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 (selon la charge de travail).
Si nous exécutons docker images demo
pour comparer la taille de l'image native à l'original, nous pouvons constater une réduction spectaculaire:
Notez également 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 (telles que les tâches par lot) peuvent être très bien adaptées à cette approche, tandis que d'autres peuvent nécessiter un effort plus important.
6. Déployer notre application native
Pour déployer notre application sur Cloud Run, nous devons placer notre image native dans un gestionnaire de paquets tel qu'Artifact Registry.
1. Préparation de 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 des notifications push vers notre nouveau registre.
La CLI gcloud peut simplifier considérablement ce processus:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Transférer notre image vers Artifact Registry
Nous allons ensuite ajouter un tag à 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 dans Cloud Run:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Étant donné que 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 pendant son exécution.
N'hésitez pas à comparer vous-même les temps de démarrage de notre application de référence à ceux de cette nouvelle application native.
7. Résumé/Nettoyage
Félicitations pour avoir créé et déployé une application Spring Native sur Google Cloud.
Nous espérons que ce tutoriel vous encouragera à vous familiariser avec le projet Spring Native et à le garder à l'esprit si vous en avez besoin à 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 à é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 nouveau et expérimental, de nombreuses ressources de qualité sont déjà disponibles 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:
- En savoir plus sur les hints natifs
- En savoir plus sur GraalVM
- Comment participer
- Erreur de mémoire insuffisante lors de la création d'images natives
- Erreur de démarrage de l'application
Licence
Ce document est publié sous une licence Creative Commons Attribution 2.0 Generic.