JVM vs Native - Une réelle comparaison des performances
Pour comparer l’exécution d’une application Java entre ses versions Bytecode (JVM) et native (GraalVM), il faut, tout d’abord, décider de son architecture et des framewoks à utiliser. Dans un deuxième temps, il faut aussi se demander ce que l’on va mesurer.
Récemment, je suis tombé sur un cours très intéressant, containers and orchestration, de Jérôme Petazzoni. Il utilise différentes applications Python et Ruby qui entrent en interaction au moyen de conteneurs Docker. Ils agissent comme un maillage de microservices. L’efficacité du système est mesuré en fonction du nombre de traitements exécutés par seconde.
Cela m’a semblé un bon exemple pour servir de base à ce comparatif en :
- Transposant le code en langage Java sous les frameworks Spring Boot / WebFlux et en utilisant Spring Native pour le build en Bytecode ou en natif,
- Jouant sur le nombre de conteneurs afin de faire varier la charge du système.
Voyons cela en détails.
Code source
Toutes les sources sont conservées sur https://github.com/scalastic/hotspot-vs-native
MAJ
La configuration des conteneurs est primordiale lorsqu’il s’agit de mesurer des consommations mémoire et CPU. Une mise à jour de cet article est disponible à JVM vs Native - Configuration des conteneurs Java dans Kubernetes
- Exigences
- Architecture d’application
- Build de l’appli
- Configuration de Kubernetes
- Configuration de Grafana
- Démarrage de l’application
- Modification de la configuration de Kubernetes
- Supprimons tout
- Conclusion
- Quelle est la prochaine Ă©tape ?
Exigences
Pour mettre en Ĺ“uvre cette solution, nous aurons besoin de :
- Un cluster Kubernetes pour exécuter nos conteneurs,
- Différentes mesures des traitements provenant des microservices
- Prometheus et Grafana pour récolter et afficher ces mesures,
- Une application Java compilable en Bytecode et en natif
Et bien, ce n’est pas grand-chose et cela existe déjà :
- Dans un article précédent, j’explique comment installer une stack complète Kubernetes, Prometheus et Grafana - Installez Kubernetes, Prometheus et Grafana en local,
- En intégrant Micrometer à une application Java Spring Boot, il est possible d’exposer les mesures de ses services - HasherHandler.java,
- Pour une application Python, la bibliothèque prometheus_client permet aussi d’exposer des mesures - worker.py,
- En configurant le POM Maven avec la dépendance
org.springframework.experimental:spring-native
, il est possible de compiler l’application aussi bien en Bytecode ou qu’en natif.
Version de Spring
Ce sont les dernières versions en date de Spring Experimental qui seront utilisées pour développer nos microservices Java. En effet, elles corrigent et améliorent continuellement les bogues et les performances du build natif. Mais il faut bien garder à l’esprit qu’il s’agit de versions en Bêta :
- Spring
2.5.0-RC1
- Spring Native
0.10.0-SNAPSHOT
Architecture d’application
Voyons de quoi est faite l’application:
L’application est composée de 4 microservices :
worker
: l’orchestrateur d’algorithmes [Python] qui obtient1
un nombre aléatoire,2
le hacher et3
incrémenter un compteur dans la base de données redis,rng
: le générateur de nombres aléatoires [Java],hasher
: le processeur de hachage [Java],redis
: la base de données qui enregistre un compteur de cycles de traitements.
Build de l’appli
Le but de la compilation est de produire une image Docker par microservice. Pour les microservices Java, il y aura deux images, la première en Bytecode, la seconde en natif.
Facultatif
J’ai mis ces images dans un registre public sur Docker Hub, vous pouvez donc passer cet étape de build.
Exigences pour le build
Toutefois, si vous souhaitez créer ces images Docker, vous devrez installer :
La façon facile
Note
- Il devrait fonctionner sur des systèmes basés sur Linux et macOS - et sur Windows avec quelques petites modifications
- Cela va prendre du temps……. 10-20 min en fonction de votre connexion internet et de votre processeur ! C’est le prix à payer pour compiler du code natif.
Pour ce faire, exécutez ce script, à la racine du projet :
Résumé des commandes exécutées
- Pour une application non-java :
- Pour une image basée sur la JVM :
- Pour une image native Java :
A partir de Docker Hub
Vous pouvez rapatrier les images Ă partir de Docker Hub en saisissant :
VĂ©rification
Pour lister vos images locales, entrez :
Vous devriez voir au moins ces images dans votre registre local:
Note
La date de création des images natives semblent erronées. Ce n’est pas le cas, l’explication est ici : Time Travel with Pack
Configuration de Kubernetes
Tout d’abord, nous devons définir la configuration kubernetes de notre application et indiquer à Prometheus où trouver les métriques.
Architecture de la stack Kubernetes
Voyons comment installer ces microservices dans notre cluster kubernetes :
- L’architecture de l’application est déployée dans un espace de nom dédié,
demo
, - Les outils de suivi se trouvent dans un autre espace de nom appelé
monitoring
.
- Nous voulons gérer le nombre de
conteneurs- pods dans ce cas - pour chaque microservice, Nous souhaitons également pouvoir changer l’image du pod (Bytecode ou natif) sans avoir besoin de tout redéployer.
=> Une telle ressource Kubernetes existe déjà , Deployment
Nous avons besoin que nos microservices communiquent entre eux dans le cluster Kubernetes.
=> C’est le travail de la ressource Service.
La base de données Redis n’a pas besoin d’être accessible de l’extérieur mais seulement de l’intérieur du cluster.
=> C’est déjà le cas car, par défaut, les Services Kubernetes sont de type ClusterIP.
Nous voulons que les métriques de l’application soient collectés par Prometheus.
=> Voici comment le configurer
Jetez un coup d’œil à la configuration du microservice Hasher ci-dessous:
Configuration Kubernetes du microservices Hasher
Configuration de Grafana
Pour afficher les metriques récoltés par Prometheus, Grafana a besoin de :
- Une source de données vers Prometheus,
- Un tableau de bord décrivant les métriques à afficher et sous quelle forme.
Si vous avez suivi mon article précédent Installer localement Kubernetes, Prometheus et Grafana, la source de données est déjà configurée et vous pouvez passer l’étape suivante. L’interface de Grafana est alors accessible à http://localhost:3000/
Configuration de la source de données
Grafana utilise des fichiers au format YAML pour configurer une source de données. On peut le définir grâce à la ressources Kubernetes ConfigMap:
Reste à passer cette ressource à Grafana dans la définition de son Deployment:
Configuration du tableau de bord
- Connectez-vous à l’interface web de Grafana,
- Importer le tableau de bord pré-défini demo-dashboard.json,
- Afficher le tableau de bord.
Vous devriez alors voir un tableau de bord vide comme celui-ci :
Description du tableau de bord de démonstration
Les lignes du tableau (étiquetées de A à C) représentent les 3 microservices, respectivement, Worker, Random Number Generator -RNG- and Hasher.
Les colonnes (numérotées de 1 à 4) représentent différents métriques:
- Dans la colonne 1, on peut voir le nombre de pods en cours d’exécution ainsi que la vitesse des traitements
- Dans la colonne 2 est affiché l’historique des vitesses de traitement, pour chaque microservice,
- Dans la colonne 3 s’affiche la consommation de CPU de chaque pod,
- Dans la colonne 4, la consommation de RAM de chaque pod.
Démarrage de l’application
Une configuration Kubernetes a été créée avec des Replicas de 1 pod pour chaque microservice et des images Java compilées en Bytecode.
- Pour démarrer l’application dans Kubernetes, entrez :
- Vous devriez voir en sortie :
- Visualisez le démarrage des pods dans Grafana:
RĂ©sultat
- La vitesse de traitement observée, située dans la cellule A1, nous donne une mesure de base de l’efficacité de notre application :
3,20
cycles/s. - En fonction des ressources allouées à votre espace, vous pouvez obtenir un résultat différent.
Modification de la configuration de Kubernetes
Aperçu
- Voyons la situation actuelle du déploiement en entrant :
- Ce qui devrait envoyer :
Augmentez le nombre de pods
- Pour augmenter les pods du
worker
Ă2
:
- Ce qui renvoie :
Incidence sur l’application
- Jetons un coup d’œil au tableau de bord de Grafana :
RĂ©sultats
Vous remarquez que la vitesse de l’application est multipliée par x2
.
Augmentez encore le nombre de pods
- Passons Ă 10 workers :
RĂ©sultats
La vitesse du processus augmente, mais n’atteint pas exactement 10 fois plus : la latence des 2 microservices, rng et hasher, qui a légèrement augmenté, explique cela.
- Augmentons le nombre de pods pour
hasher
etrng
:
RĂ©sultats
- L’augmentation du nombre de pods de
hasher
etrng
a réduit leur latence, mais elle reste tout de même un peu plus élevée qu’au début, - Un autre facteur est limitant mais nous ne voyons pas lequel dans les données affichées.
Déployons la version native de l’application
- Remplacez l’image actuelle des pods par leur version native en mettant à jour leur Deployment :
- Surveillez le déploiement :
- Et ouvrez le tableau de bord Grafana :
RĂ©sultats
La latence
- Aucun changement dans la réactivité des microservices: sans doute, le code est trop simple pour bénéficier d’un build native.
L’utilisation de l’UC
- Avec le Bytecode, l’utilisation du CPU avait tendance à diminuer avec le temps. Cela était dû à l’action du compilateur HotSpot
C2
qui produit un code natif de plus en plus optimisé avec le temps. - En revanche, l’utilisation du processeur natif est faible dès le départ.
L’utilisation de la RAM
- Étonnamment, les applications natives utilisent plus de mémoire que celles en Bytecode : c’est d’autant plus étonnant que la réduction de l’empreinte mémoire est l’un des avantages cités par la communauté.
Est-ce à cause des versions Bêta employées dans cette démo ou bien une fuite de mémoire dans l’implémentation ?
MAJ
La configuration des conteneurs est primordiale lorsqu’il s’agit de mesurer des consommations mémoire et CPU. Une mise à jour de cet article est disponible à JVM vs Native - Configuration des conteneurs Java dans Kubernetes
Supprimons tout
- Pour supprimer simplement l’application et tous ses microservices, saisissez :
- qui supprimera toutes les configurations Kubernetes créées précédemment :
Conclusion
Nous avons appris à installer une stack Kubernetes complète afin de pouvoir mesurer les métriques d’une application.
Cependant, nous n’obtenons pas les résultats escomptés dans le contexte des applications natives. Une explication pourrait être un manque de la version Spring Beta : Spring Native vient de passer à la version 0.10.0-SNAPSHOT et c’est précisément la version où des améliorations de performance sont prévues.
Je vais ouvrir un ticket auprès de l’équipe de Spring Boot pour leur demander leur analyse.~
MAJ
La configuration des conteneurs est primordiale lorsqu’il s’agit de mesurer des consommations mémoire et CPU. Une mise à jour de cet article est disponible à JVM vs Native - Configuration des conteneurs Java dans Kubernetes
Quelle est la prochaine Ă©tape ?
Qu’est-ce qui manque pour une évaluation encore plus réaliste ?
- La configuration de Kubernetes doit toujours inclure une limite de ressources ce qui n’a pas été effectué dans cette démo.
- J’aurais pu utiliser des Horizontal Pod Autoscaler (HPA) et encore mieux des HPA avec des métriques personnalisées (lisez ce post pour plus de détails).
Question
- J’aurais aimé trouver quelque chose sur des Scalers qui s’auto-régulent et capables de maximiser une métrique mais rien à propos d’une telle chose…
- Avez-vous déjà entendu parler de quelque chose du même genre ?
Liens utiles
Voici quelques liens pour une lecture plus approfondie :
- La formation de Jérôme Patazzoni sur les conteneurs : https://github.com/jpetazzo/container.training
- Les concepts dans Kubernetes : https://kubernetes.io/docs/concepts/
- Surveillance de vos applications dans Kubernetes avec Prometheus et Spring Boot : https://developer.ibm.com/technologies/containers/tutorials/monitoring-kubernetes-prometheus/
- Le client Prometheus pour Python : https://github.com/prometheus/client_python
- Les métriques Prometheus personnalisées pour les applications exécutées dans Kubernetes : https://zhimin-wen.medium.com/custom-prometheus-metrics-for-apps-running-in-kubernetes-498d69ada7aa
Et bien, voilà , c’est à votre tour de jouer avec les applications natives à présent !
Cheers…