Quarkus, jlink et Application Class Data Sharing (AppCDS)

Quarkus, jlink et Application Class Data Sharing (AppCDS)

Quarkus est optimisé pour démarrer rapidement et avoir une empreinte mémoire très faible. Ceci est vrai en déployant dans une JVM standard mais encore plus en déployant notre application comme un exécutable natif via GraalVM.

Quarkus facilite grandement la création d’exécutable natif, grâce à ça, une application Quarkus démarre en quelques dizaines de millisecondes et avec une empreinte mémoire très faible : quelques dizaines de Mo de RSS (Resident Set Size – totale d’utilisation de mémoire du processus Java vu par l’OS).

Si on reprend la comparaison disponible sur le graphique ci-dessous, on passe, pour une stack classique REST JSON / Hibernate, de 2s de temps de démarrage à 42ms et de 145 Mo de RSS à 28 Mo. Et encore, avec un seul cœur ! À titre d’exemple, mon application bookmark-service de mon Dojo Quarkus bookmarkit démarre en 1,2s sur mon laptop.

Un autre point non mentionné, est la taille de l’image Docker. Une application native ne contiendra que le code utilisé par l’application (grâce au dead code élimination de GraalVM), ainsi qu’une JVM minimaliste (SubstrateVM), et sera donc plus petite qu’une image standard qui embarque l’intégralité des librairies tierces ainsi qu’une JVM complète.

Mais, déployer son application Quarkus comme une application native a quelques inconvénients :

  • A cause du close world assumption de GraalVM, il faut que toutes les librairies utilisées soient compatibles GraalVM. Ceci est garantie pour les extensions Quarkus, mais pour les autres, c’est à vous de vérifier, et il peut parfois y avoir de mauvaises surprises.
  • SubstrateVM est une JVM partielle, elle ne supporte ni JMX ni JVM-TI (et donc pas d’agents Java), ce qui peut grandement complexifier le monitoring et l’administration de votre application.
  • Une application native a des performances en pointe qui sont inférieures à une application déployée dans la JVM. Principalement parce que le Just-In-Time Compiler (le JIT) de Java va profiler votre application lors de son utilisation et pourra donc effectuer des optimisations plus pertinentes que GraalVM qui réalise ces optimisations à la compilation (compilateur Ahead-Of-Time – AOT).

Si vous voulez déployer votre application Quarkus dans une JVM standard, mais optimiser son temps de démarrage et la taille de l’image Docker, alors Jlink et AppCDS peuvent vous aider !

Dans les paragraphes suivants, je vais utiliser l’application bookmark-service comme exemple. Une JVM 11 minimum est nécessaire (j’ai utilisé OpenJDK 11.0.5).

Jlink

Jlink permet de générer une JVM customisée qui ne contiendra que les modules nécessaires au fonctionnement de votre application. Ceci est rendu possible grâce à la modularisation du JDK qui a été faite pour Java 9 (JSR-376 – projet Jigsaw). Qui dit taille réduite de JVM, dit image Docker plus petite !

Jlink prend en entrée la liste des modules à inclure dans votre JVM customisée. Pour connaitre cette liste, il faut utiliser l’outil jdeps.

Tout d’abord, il faut configurer Quarkus pour générer un fat jar (ou uber jar) pour que jdeps puisse analyser tout le code de votre application. Pour ce faire, il faut ajouter la propriété suivante dans votre application.properties :

quarkus.package.uber-jar=true

On package ensuite l’application via mvn clean package.

Puis on lance la commande jdeps avec l’option --list-deps qui permet de lister les modules dont votre application dépend.

Voici le résultat de la commande jdeps avec le uber jar de l’application bookmark-service :

$ jdeps --list-deps target/bookmark-service-1.0-SNAPSHOT-runner.jar 
   JDK removed internal API/org.relaxng.datatype
   java.base/sun.security.util
   java.base/sun.security.x509
   java.compiler
   java.datatransfer
   java.desktop
   java.instrument
   java.logging
   java.management
   java.naming
   java.rmi
   java.security.jgss
   java.security.sasl
   java.sql
   java.transaction.xa
   java.xml
   jdk.jconsole
   jdk.management
   jdk.unsupported

Pour créer une JVM customisée, il faut utiliser jlink en lui donnant la liste des modules résultants de l’appel à la commande jdeps via l’option -add-modules. Petite subtilité, les deux modules java.base/sun.security.util et java.base/sun.security.x509 ne doivent pas être intégrées, mais on doit intégrer le module java.base à la place. Je ne sais pas d’où vient cette incohérence de résultat de la commande jdeps, mais le module java.base est utilisé par toute application Java, donc il doit systématiquement être présent.

Pour l’application bookmark-service, voici la commande jlink que j’ai utilisé :

$ jlink --no-header-files --no-man-pages --output target/customjdk --compress=2 --strip-debug
--module-path $JAVA_HOME/jmods --add-modules java.base,java.base,java.base,java.compiler,
java.datatransfer,java.desktop,java.instrument,java.logging,java.management,java.naming,
java.rmi,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,
java.xml,jdk.jconsole,jdk.management,jdk.unsupported

Comme je voulais avoir une JVM la plus petite possible, j’ai ajouté les options suivantes à la commande jlink :

  • --no-header-files : Ne pas inclure les fichiers header,
  • --no-man-pages : Ne pas inclure les pages de manuel,
  • --compress=2 : Compression de niveau ZIP pour les fichiers compressibles,
  • --strip-debug : Ne pas inclure les informations de debug.

Après cela, on peut utiliser la JVM générée dans le répertoire target/customjdk pour lancer notre application :

$ target/customjdk/bin/java -Xmx32m -jar target/bookmark-service-1.0-SNAPSHOT-runner.jar

Si on compare le démarrage de notre application avec notre JVM customisée et la JVM standard, il n’y a pas d’avantage au RSS ni au temps de démarrage. Le seul intérêt est la taille de la JVM qui est passée de 314Mo à 51Mo !

Idéalement, il faudrait automatiser ça dans votre build Docker via un Dockerfile multi-stage …

Pour aller plus loin sur jlink, je vous invite à consulter cet article : Using jlink to build java runtimes for non modular applications.

AppCDS – Application Class Data Sharing

Attention : si vous voulez utiliser AppCDS avec jlink, il faudra utiliser la JVM customisée dans les commandes de ce paragraphe au lieu de votre JVM par défaut.

Une des raisons du temps de démarrage conséquent d’une application Java, est que, pour chaque classe, la JVM doit la charger depuis le disque, la vérifier, puis créer une structure de données spécifique (class metadata).

Application Class Data Sharing (AppCDS) est une fonctionnalité via laquelle on peut créer une archive des metadatas de nos classes, pour éviter de le faire au démarrage. Une fois ces metadatas chargés, il n’y a aucune différence de comportement avec une application n’utilisant pas AppCDS.

L’utilisation d’AppCDS se fait en trois étapes :

Etape 1 : Création de la liste de classes à archiver.

java -XX:DumpLoadedClassList=target/classes.lst -jar target/bookmark-service-1.0-SNAPSHOT-runner.jar

J’ai arrêté l’application juste après son lancement, mais on peut imaginer l’utiliser un peu plus longtemps (lancement de test fonctionnel par exemple) pour s’assurer que tout le code de l’application ait bien été appelé, et donc que le plus de classes possible seront listées.

Etape 2 : Génération de l’archive en utilisant l’option JVM -Xshare:dump qui demande à AppCDS de la créer.

java -Xshare:dump -XX:SharedClassListFile=target/classes.lst -XX:SharedArchiveFile=target/app-cds.jsa --class-path target/bookmark-service-1.0-SNAPSHOT-runner.jar

Cette commande ne lance pas l’application. Elle va seulement générer un fichier app-cds.jsa de 69Mo qu’on pourra ensuite utiliser pour lancer notre application.

Etape 3 : On lance l’application en lui passant l’archive créée.

java -XX:SharedArchiveFile=target/app-cds.jsa -jar target/bookmark-service-1.0-SNAPSHOT-runner.jar

Avec une archive, l’application se lance en 500ms au lieu de 1,2s, ce qui fait -60% de temps de démarrage !

On n’a hélas, pas de différence au niveau empreinte mémoire.

Pour aller plus loin : Application Class Data Sharing.

Conclusion

Jlink permet de réduire très fortement la taille de votre image Docker. Si vous déployez votre application comme un conteneur Docker et que vous ne maîtrisez pas forcément le nœud sur lequel elle est déployée (cloud publique ou cloud privé partagé avec d’autres applications), cela peut être intéressant pour limiter le temps de téléchargement de celle-ci. Attention par contre à bien penser à re-créer votre JVM customisée à chaque fois que vous ajoutez une nouvelle librairie car celle-ci pourrait utiliser un module absent de votre précédente JVM customisée.

AppCDS me semble très intéressant, il réduit très fortement le temps de démarrage de votre application sans aucun inconvénient autre que nécessiter la création de l’archive de classes au préalable. Par contre, la création de cette archive n’est pas triviale, mais au vu de l’optimisation (-60%) ça vaut le coup !

Un remerciement tout spécial à Logan pour sa relecture et la correction des nombreuses fautes d’orthographe 😉

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.