Quarkus, un framework Java compilé nativement
Quarkus est un framework Java conçu pour les architectures de microservices. Il permet de développer des applications Java. Lorsque l’on implémente des microservices comme dans l’article précédent, il convient d’observer et comprendre l’impact sur les ressources système pour une bonne approche DevOps.
Comment mesurer la performance du framework Java Quarkus?
La notion de performance est vaste. Définir des indicateurs est nécessairement dépendant d’un contexte. Pour celui de notre agent immobilier, les indicateurs principaux sont :
- Le nombre de requêtes servies par unité de temps doit être suffisant pour répondre aux sollicitations des clients ;
- L’empreinte mémoire nécessaire pour servir ces requêtes doit idéalement être la plus faible possible pour ne pas engendrer de coûts supplémentaires.
Pour mesurer le nombre de requêtes servies, un test Apache JMeter est tout à fait adapté.
Pour mesurer l’empreinte mémoire d’un microservice, il convient de mesurer l’empreinte du programme et de la machine virtuelle qui le fait tourner. C’est-à-dire la mémoire résidente du processus Java correspondant. Cette empreinte se mesure par exemple avec la commande suivante :
ps –o pid,rss,command –p <pid du processus>
Observer le microservice Thorntail
En déployant le microservice Thorntail sur un simple serveur et en utilisant Apache JMeter pour le solliciter, voici le comportement observé sous Prometheus :
Le microservice consomme environ 300Mo de mémoire en charge pour servir approximativement 800 requêtes par seconde (en simulant 5 utilisateurs réalisant des requêtes en permanence).
Cette consommation mémoire au regard du nombre de requêtes services est habituelle dans le monde Java. Elle se compose de l’empreinte mémoire du programme mais aussi de l’empreinte mémoire de la machine virtuelle Java.
Consommation de la machine virtuelle Java
Par exemple, l’exécution du code suivant qui ne nécessite aucune libraire, sur OpenJDK 11 consomme tout de même 35Mo essentiellement attribuables à la machine virtuelle Java pour stocker le code compilé, le garbage collector, les threads stacks, etc.
A l’échelle d’un système d’information, lorsque les microservices se multiplient, et qu’une plateforme telle que Kubernetes/OpenShift en gère une multitude, le cumul de cette part n’est plus négligeable et engendre un besoin de ressources “cloud” ou physiques, qui ont un coût.
Optimiser l’empreinte mémoire avec le framework Java Quarkus
Ce constat amène l’émergence de nouveaux frameworks et de nouvelles approches. Je vous propose d’explorer Quarkus (http://quarkus.io) et de migrer notre implémentation Thorntail vers ce framework.
Migrer de Thorntail vers Quarkus
La migration est plutôt aisée. Thorntail et Quarkus implémentent tous deux les spécifications MicroProfile. Il suffit de comparer le code, disponible sur les dépôts GitHub https://github.com/its4u/immo-tenants-management-thorntail et https://github.com/its4u/immo-tenants-management en licence opensource MIT, pour se rendre compte de leur grande similitude.
Un projet Quarkus se crée facilement, par exemple avec Maven :
On peut observer facilement les différences induites par la migration comme ceci :
diff -r immo-tenants-management/src/ immo-tenants-m
anagement-thorntail/src/ > diff.txt
Les différences notables sont :
- Les fichiers pom.xml référencent les librairies spécifiques à chaque framework Thorntail ou les librairies Quarkus.
- Des adapteurs permettant à Thorntail de mieux présenter les types LocalDate ainsi que certaines exceptions ne sont plus utiles avec Quarkus
- Quarkus a besoin de l’annotation “@RegisterForReflection” sur les DTOs en entrée/sortie de l’API. En effet, son processus de compilation cherche à éliminer autant de code non utile que possible. Cette annotation lui indique de ne pas éliminer cette classe.
- Enfin, la structure des tests unitaires varie pour utiliser QuarkusTest au lieu d’Arquillian mais le code de test reste inchangé.
Construire notre microservice Quarkus
Deux modes de construction sont possibles : un mode Java ou un mode natif. Le mode natif nous intéresse plus particulièrement. Il produit un binaire natif, donc spécifique au système d’exploitation, qui inclut une machine virtuelle Java optimisée et notre programme et ces librairies compilés également optimisés.
mvn clean package –Pnative
Cette étape est un peu plus longue que pour Thorntail du fait des précédentes optimisations.
Observer le microservice Quarkus
En appliquant le même protocole de test, j’observe que le microservice Quarkus consomme 178Mo, soit deux fois moins de mémoire, sous la même charge que son homologue Thorntail.
Un autre constat est que le microservice Quarkus démarre presque instantanément. En cas de besoin, il est donc possible de réagir à une soudaine charge pour démultiplier le nombre d’instance de microservice.
Quarkus réalise ces progrès en éliminant le code non utilisé dès la compilation, et s’appuyant sur un compilateur d’un nouveau genre, nommé “GraalVM », qui produit du code natif dès la phase de construction de l’application (alors que les JVMs classiques le font à la volée “Just In Time”).
Économiser de la mémoire avec Quarkus
Quarkus représente un framework intéressant pour qui souhaite s’appuyer sur l’écosystème Java, ses connaissances Java, l’écosystème Java tout en optimisant l’empreinte mémoire et le temps de démarrage de ses microservices, ce qui est un avantage non négligeable si l’on souhaite mettre en place de l’auto-scaling.