Background Image
TECHNOLOGIE

Java compilé nativement sur Google App Engine

Brendon Anderson

Directeur de la consultation

June 14, 2022 | 6 Lecture minute

Google App Engine est une plateforme en tant que service qui est commercialisée comme un moyen d'intégrer vos applications dans le nuage sans nécessairement connaître tous les éléments d'infrastructure nécessaires pour le faire. Google App Engine existe depuis 2008 et a été l'un des premiers services en nuage proposés par Google. Il a d'abord pris en charge Python, puis s'est rapidement étendu à Java et prend désormais en charge de nombreux langages tels que Go, .Net, Node.js et d'autres. L'environnement standard d'App Engine gère les instances en cours d'exécution pour vous en passant de zéro instance (en cas d'absence de trafic) à une mise à l'échelle rapide pour répondre à n'importe quel niveau de demande. Une application à faible trafic peut être exécutée gratuitement, ce qui est idéal pour le prototypage ou la création d'applications de validation.

Récemment, en lisant sur Java compilé nativement, je suis tombé sur une information qui disait que Java compilé en tant que binaire natif peut être déployé dans l'environnement standard d'App Engine. J'ai donc décidé d'approfondir la question, car je n'avais connaissance que du déploiement d'un artefact de type war ou jar sur App Engine.

Entrez dans Micronaut

Si votre application déployée sur App Engine ne répond à aucune requête, elle est automatiquement réduite à zéro instance. Lorsqu'une requête arrive, App Engine démarre automatiquement une instance pour vous. Vous voulez que votre application démarre rapidement afin que la première requête n'ait pas à attendre trop longtemps avant d'être servie. Même une application Spring de taille modérée peut prendre plusieurs secondes pour démarrer. Micronaut est un autre framework qui vante sa capacité à démarrer rapidement. La rapidité de démarrage de Micronaut est attribuée à la façon dont il effectue toutes ses opérations d'injection de dépendances et de type AOP au moment de la compilation. Spring fait tout cela au moment de l'exécution. Mieux encore, Micronaut a beaucoup de support pour construire des binaires compilés nativement en utilisant GraalVM, ce qui rend le temps de démarrage et la consommation de mémoire encore plus faibles. Un autre avantage de Micronaut est qu'il ressemble beaucoup à Spring lors du développement d'une application, ce qui rend la transition assez facile pour les développeurs Spring.

A noter

Avant d'aller plus loin dans ce billet, je dois préciser que j'ai effectué tout le développement sous Linux. Les binaires compilés sur MacOS ou Windows ne fonctionneront probablement pas sur Google App Engine. Dans un environnement réel, il existe un pipeline CI/CD dans lequel le binaire est construit sous Linux dans un conteneur Docker.

Conditions préalables

Plusieurs prérequis sont nécessaires pour commencer, dont certains existent peut-être déjà pour vous. Je n'entrerai pas dans les détails de leur installation, mais je mettrai des liens à votre disposition pour vous aider à démarrer.

  • SDKMan - Un excellent outil à installer de toute façon !

  • GraalVM - Installer avec SDKMan (la version peut être différente)

  • sdk install java 22.1.0.r17-grl 

  • L'outil native-image de GraalVM

  • gu install native-image 

  • Gradle - Installer avec SDKMan

  • sdk install gradle 

  • Micronaut CLI - Installer avec SDKMan

  • sdk install micronaut 

  • zlib et musl

  • Créez un compte GCP et un projet App Engine. Si vous avez le choix, choisissez l'environnement "standard" (au lieu de "flex"). Il y a quelques instructions de base ici.

  • gcloud SDK CLI

Le code

Le code source complet de mon application d'exemple peut être trouvé sur GitHub, mais je vais le parcourir et signaler quelques problèmes.

Utilisez Micronaut pour créer votre application :

mn create-app com.improving.native-on-app-engine --build=gradle --lang=java --features=graalvm 

A ce stade, vous aurez un shell d'application avec la plupart des éléments dont vous aurez besoin.

La première étape consiste à créer un contrôleur simple. Dans mon code, j'ai injecté une propriété pour indiquer quel profil est lu. Les profils Micronaut fonctionnent de manière similaire à Spring en nommant les fichiers application.yml de manière appropriée.

@Controller("/hello") public class HelloController { @Inject // nécessaire pour les variables privées lors de la compilation native @Property(name = "app.greeting") private String greeting ; private final String baseText ; public HelloController(@Property(name = "app.basetext") String txt) { this.baseText = txt ; } @Get @Produces(MediaType.TEXT_PLAIN) public String index() { return baseText + " " + greeting ; } }

Tout ceci devrait être banal, à l'exception de l'annotation @Inject. Lors de l'injection de variables privées, l'annotation @Inject est nécessaire pour la compilation native (pas nécessaire pour les artefacts de type jar/war). D'autres moyens de contourner ce problème sont d'avoir un setter public ou d'utiliser l'injection de constructeur, comme je l'ai fait ici. Mettre la variable en package-private est une autre option. Je pense que dans la plupart des cas, l'injection de constructeur est la meilleure solution.

Le fichier build.gradle a également besoin de quelques modifications. Il aura besoin de ce plugin :

id("com.google.cloud.tools.appengine") version '2.4.2'

Ces bibliothèques supplémentaires :

annotationProcessor("io.micronaut:micronaut-graal")

nativeImageCompileOnly("com.google.cloud:native-image-support:0.14.1")

annotationProcessor("io.micronaut:micronaut-inject-java")

Cette configuration supplémentaire doit également être ajoutée :

appengine { stage.artifact = "${buildDir}/native/nativeCompile/native" deploy { projectId = "YOUR_PROJECT_ID_HERE" version = "1" } }

graalvmNative { binaries{ main { imageName.set('native') buildArgs.addAll(["--verbose", "--static", "--libc=musl"]) } } }

Les args de compilation dans le fichier graalvmNative lui indiquent de compiler statiquement le binaire en utilisant la bibliothèque musl pour libc. Cela garantit qu'il fonctionnera n'importe où, quelle que soit la version de libc/glibc installée sur la machine hôte (tant qu'il s'agit de Linux). J'ai rencontré des problèmes en ne compilant pas statiquement le code. La version de glibc sur App Engine était différente de la version sur mon ordinateur portable Linux, de sorte qu'il ne démarrait pas sur App Engine. La compilation statique du binaire de cette manière met dans le binaire tout ce qui est nécessaire. Cela crée un binaire plus volumineux, mais augmente également la compatibilité de sorte qu'il fonctionnera dans plus d'endroits.

Voir settings.gradle pour des configurations supplémentaires liées aux plugins.

Créer un fichier src/main/appengine/app.yml avec le contenu suivant :

runtime : java11 point d'entrée : ./native

A ce stade, vérifiez que les choses fonctionnent sans la compilation native. L'application devrait démarrer en lançant ./gradlew run. En utilisant curl ou Postman pour accéder à l'URL http://localhost:8080/hello, le message "Hello World baseà moins que vous n'ayez modifié le profil pour utiliser l'un des autres fichiers de propriétés (en définissant la variable d'environnement MICRONAUT_ENVIRONMENTS=local). Si tout fonctionne comme prévu, passons à la compilation native.

Compilation native

En utilisant la commande ./gradlew nativeCompile devrait construire un binaire. C'est une compilation beaucoup plus longue que celle d'un fichier jar. Le nouveau binaire doit être situé à l'endroit suivant /build/native/nativeCompile/native 

Pour tester le binaire, exécutez la commande ./build/native/nativeCompile/native

Vous devriez remarquer que les temps de démarrage passent de 500-600 ms à moins de 100 ms.

Déploiement dans le nuage

Lorsque vous avez créé votre projet GCP, vous avez reçu un identifiant de projet. Cet identifiant doit être placé dans le fichier appengine dans le fichier build.gradle dans la configuration d'appengine. Si tout est en place, vous pouvez déployer le binaire en utilisant ./gradlew appengineDeploy ce qui prendra quelques minutes.

Lorsque le déploiement est terminé, il vous donnera une URL de base pour votre application dans le terminal. Si vous prenez cette URL et ajoutez /hello à la fin, vous devriez voir le message "Hello Cloud GCP". Le profil correct a été choisi automatiquement en fonction de l'environnement (application-gcp.yml) ! La première fois que vous accédez à votre point de terminaison, cela peut prendre 1500 ms en temps réel, mais les requêtes suivantes devraient prendre ~50 ms. Je pense que la plupart de ces 1500ms sont des frais généraux avec App Engine qui fait ce qu'il doit faire pour que votre application soit opérationnelle. Si vous regardez les logs actuels dans GCP, vous verrez que votre application a démarré en 100-200 ms.

 

Technologie
Application Modernization

Dernières réflexions

Explorez nos articles de blog et laissez-vous inspirer par les leaders d'opinion de nos entreprises.
Blog Image - Unveiling the Future of AI at Google Cloud Next 24 -1
AI/ML

Unveiling the Future of AI at Google Cloud Next ‘24

Get firsthand insights from Improving into the innovation brewing around artificial intelligence and cloud computing at Google Cloud Next '24.