Maison >Java >javaDidacticiel >Java peut également être sans serveur : utiliser GraalVM pour des démarrages rapides à froid
Une approche assez courante lorsqu'il s'agit de travailler avec du code sans serveur consiste à l'écrire en tant qu'application Python, Node ou Go étant donné leur réputation de démarrages à froid très rapides.
Mais que se passe-t-il si nous sommes confrontés à des applications Java préexistantes ciblant des environnements sans serveur tels qu'AWS Lambda ? Peut-être qu'une bonne majorité de notre base de code héberge Java et nous avons développé un riche écosystème d'outils et de bibliothèques que nous aimerions réutiliser. Réécrire l'ensemble du parc de ces applications dans un langage différent coûte cher, sans compter que nous abandonnons des fonctionnalités telles que la sécurité des types statiques et l'optimisation du temps de compilation.
Il y a quelque temps, j'ai été confronté à ce scénario précis : 9 applications AWS Lambda écrites en Java qui seraient très lentes lors des démarrages à froid au point que certaines d'entre elles expireraient occasionnellement.
Les Lambdas en question ont été placés derrière API Gateway et utilisés pour les tâches d'administration en appelant les API REST correspondantes. Cette fonctionnalité n'était pas très utilisée et les démarrages à froid étaient donc inévitables ; cependant, comme il ne s'agissait pas d'un service critique, c'était une opportunité parfaite pour expérimenter : déterminer si ces Lambda pouvaient être récupérées.
Il ne m'a pas fallu longtemps avant de tomber sur plusieurs autres articles de blog sur des développeurs utilisant avec succès GraalVM et des frameworks tels que Quarkus pour résoudre ce problème précis. J’ai donc décidé de l’essayer par moi-même.
Mais au fait, c'est quoi ces outils ?
En bref, GraalVM est une machine virtuelle Java livrée avec un ensemble d'outils capables de compiler Java vers une image native et de l'exécuter à l'aide de la JVM Graal.
Normalement, Java utilise le compilateur « Just In Time » (JIT), qui, comme son nom l'indique, effectue des optimisations et une compilation lors de l'exécution de notre code. Les applications de longue durée en bénéficient étant donné que les optimiseurs JVM surveillent en permanence l'exécution du programme et effectuent des réglages précis qui, au fil du temps, se traduisent par de meilleures performances.
C'est génial si une application est instanciée une fois et devrait s'exécuter pendant plusieurs heures ou plus, mais pas si génial si nous avons affaire à Kubernetes, AWS Lambdas et aux tâches par lots qui espèrent démarrer rapidement des applications Java, effectuer opérations urgentes et échelle en fonction de la demande - en parlant de turbo lag pour les passionnés de voitures.
Et c’est là que la fonctionnalité Native Image de GraalVM intervient pour vous aider. Au lieu d'utiliser le compilateur JIT, il opte pour une approche très différente consistant à compiler notre code à l'avance (AOT). Il prépare notre tarte à l'aide d'une analyse de code statique et pré-initialise même certaines classes pendant la construction afin qu'elles soient prêtes à être déclenchées à chaque fois que notre code d'application est exécuté.
Le résultat ? Démarrages à froid très rapides, ce qui rend les images natives très performantes dans les domaines sans serveur où les applications sont de courte durée et doivent démarrer rapidement.
Une chose à noter est que même si GraalVM est capable d'AOT, il peut également remplacer la JVM existante offrant de meilleures performances grâce au nouveau compilateur JIT de GraalVM écrit en Java.
Mais attendez, il y a plus ! Étant donné que Native Image inclut uniquement le code qui se trouve sur le chemin d'exécution connu, nous réduisons le gras et toutes les classes Java qui n'ont pas été explicitement déclarées à conserver ne seront pas disponibles. Parce que nous ne conservons que les bits censés s’exécuter, nous augmentons la sécurité de notre application.
Prenons par exemple la tristement célèbre vulnérabilité Log4J qui utilisait l'exécution de code à distance comme moyen de compromettre l'hôte. Avec Native Images, il est très peu probable que le chaînage de gadgets réussisse, car les morceaux de code de bibliothèque requis pour transmettre l'attaque ne sont même pas accessibles.
Quarkus, d'autre part, est un framework Java optimisé pour les applications sans serveur qui est livré avec une boîte à outils qui facilite la création d'images natives en proposant une extension pour configurer et créer spécifiquement des AWS Lambda en tant qu'exécutables natifs.
Au cours de mon parcours d'optimisation Lambda, j'ai également rencontré des techniques d'optimisation alternatives. L'une de ces optimisations était l'utilisation exclusive proposée d'un compilateur C1 lors de l'exécution de Lambda, ce qui promettait des démarrages à froid plus rapides. Normalement, les applications Java qui s'exécutent dans une JVM utilisent une compilation à plusieurs niveaux qui consiste en un C1 plus rapide, mais moins optimal, suivi d'un C2 qui est plus lent, mais offre des performances plus optimales pour les applications Java qui s'exécutent pendant une longue période. Étant donné que les Lambdas sont de courte durée, les avantages de la compilation C2 sont négligeables.
Un guide expliquant le processus de configuration de la compilation C1 pour AWS Lambdas est disponible ici.
Bien sûr, je voulais savoir dans quelle mesure cette technique pouvait offrir une amélioration par rapport à mon plan directeur GraalVM en place, et je l'ai donc également inclus dans mes conclusions ci-dessous.
De plus amples détails sur la compilation hiérarchisée de JVM ainsi que sur le tout nouveau compilateur JIT de GraalVM peuvent être trouvés dans cet article de Baeldung.
Assez ironiquement, quelques mois après avoir envoyé mes modifications en production, AWS a proposé sa dernière fonctionnalité SnapStart, qui prend un instantané d'un Lambda en cours d'exécution et au lieu de le réinitialiser à nouveau, il utilise des images instantanées comme un point de restauration promettant des démarrages à froid plus rapides. J'ai dû essayer pour savoir si l'utilisation de GraalVM était un effort inutile et je l'ai également inclus dans mes conclusions.
Il convient de noter que pour tirer le meilleur parti de SnapStart, une refactorisation de code aurait été nécessaire afin d'utiliser les hooks beforeCheckpoint et afterRestore (plus de détails ici). Étant donné que je voulais éviter si possible toute modification majeure du code, j'ai utilisé cette fonctionnalité « telle quelle », sans implémenter ces méthodes ni réorganiser le code.
Revenons maintenant à GraalVM ! À ma grande surprise, après avoir incorporé cette solution, aucune modification du code Java n'a été requise, à part l'ajout et l'ajustement des fichiers de configuration de build et certaines métadonnées requises.
Cela semble trop beau pour être vrai ?
Peut-être un peu. Étant donné que nous utilisons la compilation AOT, dans le monde de Java, cela pose un certain défi s'il s'agit d'utiliser des fonctionnalités de langage telles que les réflexions, les proxys, les interfaces et les registres de services sur lesquels s'appuient de nombreuses bibliothèques. C'est pourquoi le compilateur GraalVM nécessite la déclaration de métadonnées de configuration supplémentaires qui enregistrent explicitement certaines classes et services afin qu'ils puissent être inclus dans l'artefact final. GraalVM fournit un soi-disant agent qui peut être utilisé pour s'exécuter avec votre exécutable afin d'identifier automatiquement la configuration requise, ce qui peut faciliter ce processus.
Quarkus fournit plusieurs extensions pour les bibliothèques bien connues afin de les rendre « compatibles avec les images natives », mais étant donné que je travaillais avec une base de code existante, et que mon objectif était d'éviter toute refactorisation majeure (ou toute modification de code d'ailleurs ), je me suis contenté de créer les fichiers de configuration requis par les bibliothèques existantes pour produire des images natives avec succès.
Sachez que la compilation d'images natives nécessite beaucoup de ressources et prend beaucoup plus de temps que la compilation de bytecode ciblant un runtime JVM standard. Il y a de fortes chances que vous deviez allouer plus de RAM à un nœud de build pour éviter les problèmes de mémoire insuffisante, ce qui ne devrait pas être un facteur décisif, mais c'est certainement quelque chose à garder à l'esprit.
Maintenant que mes Native Image Lambdas ont été compilées et empaquetées, il était temps de les déployer dans un environnement de test. Normalement, Java Lambdas utilise les Java Runtimes d'AWS pour s'exécuter ; Cependant, étant donné que nous essayons d'utiliser une image native qui est un artefact binaire contenant le code de notre application enveloppé dans la JVM Graal, nous devons sélectionner l'un des environnements Amazon Linux « personnalisés » proposés par AWS.
J'ai utilisé une collection d'API Postman pour envoyer des requêtes aux 9 Lambdas et mesurer les temps de réponse au démarrage à froid pour chaque technique mentionnée ci-dessus. Pour m'assurer que je rencontrais toujours un démarrage à froid, j'ai rechargé la configuration de la cible Lambda, ce qui garantit que le prochain appel n'utilisera pas une instance qui pourrait être déjà chaude. Tous les Lambda ont été configurés avec 1 Go de RAM. J'ai également mesuré un seul appel pour chaque configuration étant donné que le processus prenait du temps ; cependant, les temps de réponse observés dressent un tableau assez clair.
Alors ça a marché ? Absolument! Voici les résultats :
Et le grand gagnant est : GraalVM Native Images - en moyenne, cela a entraîné une accélération 3x par rapport aux Java Lambdas inchangés - plus de délais d'attente et des temps de réponse bien meilleurs, ce qui est exactement ce que je voulais réaliser.
SnapStart n'a pas fonctionné aussi bien que je le pensais sans aucune modification de code. Lorsque le compilateur C1 a été utilisé en plus de la fonctionnalité SnapStart, il a encore réduit les temps de démarrage à froid, mais n'a toujours pas battu l'image native de GraalVM. Cela ne veut pas dire que ce n’est pas une option viable en tant qu’amélioration rapide et facile à mettre en œuvre ; cependant, si nous voulons optimiser notre Lambda autant que possible et que nous disposons de temps et de ressources pour ajuster la configuration et notre processus de construction, GraalVM est définitivement supérieur en termes de performances et de sécurité.
Comme le prétend GraalVM, les images natives nécessitent moins de ressources pour fonctionner efficacement par rapport à leurs homologues JVM classiques. Je voulais voir comment les performances de démarrage à froid et de démarrage à chaud résisteraient si je devais réduire la quantité de RAM avec laquelle ces Lambda devaient fonctionner. Cette fois, je n'ai sélectionné qu'une seule application Lambda pour effectuer ce test. Voici les résultats :
Et ils ont tenu leur promesse ! Les Lambdas JVM standard ont manqué de mémoire lors d'une tentative de configuration de 256 Mo et moins, alors que l'image native semblait ne pas être en phase et continuer à s'exécuter. Si 128 Mo n’étaient pas l’option de mémoire disponible la plus basse, je me demande jusqu’où nous aurions pu descendre. Les images natives sont non seulement plus rapides lors des démarrages à froid, mais offrent également des performances constantes lorsque vous travaillez avec des ressources limitées, ce qui se traduit par des coûts d'exploitation inférieurs.
L'écosystème Java est riche et vaste avec de nombreuses nouvelles technologies et améliorations émergeant chaque jour qui maintiennent Java dans le jeu en matière d'applications sans serveur. L'une de ces technologies émergentes est GraalVM. Ce qui a commencé comme un projet de recherche est maintenant lentement adopté et se présente comme une alternative viable à une JVM standard telle que HotSpot. Dans cet article de blog, j'ai à peine effleuré la surface de ce que GraalVM a à offrir et j'encourage les lecteurs à l'explorer davantage. Il existe plusieurs réussites d'entreprises telles que Adyen (lien de l'article) ou Facebook (lien de l'article) qui ont pu utiliser GraalVM pour économiser du temps et de l'argent.
Donc, la prochaine fois que vous êtes sur le point de supprimer Java en option, essayez GraalVM. Et maintenant que Spring Boot 3 prend en charge les images natives GraalVM, il est plus facile que jamais de les utiliser pour vos charges de travail sans serveur afin de capitaliser sur les performances, la faible consommation de ressources et la sécurité supplémentaire que GraalVM a à offrir.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!