Une migration Java 11 réussie
Fabian Piau | jeudi 27 décembre 2018 - 11:45Cet article résume le travail que nous avons accompli au sein de mon équipe pour migrer nos micro-services de Java 8 à Java 11 pour le site Hotels.com.
En résumé, pour chacun de nos services, nous avons suivi les étapes suivantes:
- Compiler le code avec Java 11
- Démarrer le service (compatible Java 11) sur Java 8
- Démarrer le service sur Java 11
En réalité, nous avons eu quelques étapes supplémentaires. Lorsque nous avons commencé la migration, Java 11 n’était pas encore disponible, nous ne pouvions utiliser que Java 10.
L’hypothèse était la suivante: si le code compile avec Java 10, la migration vers Java 11 ne demandera pas beaucoup de travail, car le changement le plus important à propos de la modularité a été introduit avec Java 9 et le projet Jigsaw. Heureusement, ce fut le cas!
1. Compiler le code avec Java 11
C’est la partie qui a pris le plus de temps. En effet, nous avons dû monter en version la plupart des frameworks et des outils que nous utilisions. En particulier, nous avons dû gérer la migration de Spring Boot 1 à 2 et de Spring 4 à 5. S’agissant de versions majeures, nous avons dû prendre en compte quelques modifications importantes.
Spring Boot
Pour Spring Boot 2, les guides de migration officiels de Spring Boot 2.0 et Spring Boot 2.1 sont bien écrits et détaillés.
- Le chargement des profils a évolué
- Le relaxed binding des propriétés est un peu moins relaxed
- Certaines propriétés ont été renommées et d’autres sont indisponibles (par exemple, la propriété
security.basic.enabled
doit être remplacée par unWebSecurityConfigurerAdapter
) - Certains endpoints ont été renommés (par exemple, actuator healthchecks)
- L’overriding de bean est maintenant désactivé par défaut. Comme nous l’utilisions dans nos tests d’intégration, nous avons dû le réactiver avec la nouvelle propriété
spring.main.allow-bean-definition-overriding
.
Spring
La migration vers Spring 5 était plutôt simple du point de vue du code avec quelques changements mineurs. La partie la plus compliquée était liée au fait que le projet était legacy et que nous devions gérer une configuration Spring en XML complexe et la migration vers Dropwizard Metrics 4.
Divers
Il peut arriver que des frameworks ne soient toujours pas compatibles avec Java 9+ car ils ne sont pas activement maintenus par la communauté. Dans notre cas, nous avons dû trouver une solution de contournement pour Cassandra Unit. Nous ne voulions pas investir de temps dans un changement de framework DB de test car nous prévoyons de passer à DynamoDB.
Nous avons également dû faire face au « Maven dependency hell » car certaines dépendances obligatoires tiraient d’anciennes dépendances non compatibles avec Java 9+. Dans la plupart des cas, l’ajout d’exclusions dans le POM a résolu le problème.
Environnement local
En local, nous avons ajouté quelques alias à notre fichier de profil bash pour passer d’une version de Java à une autre.
alias setjava8="export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_172.jdk/Contents/Home/" alias setjava10="export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-10.0.2.jdk/Contents/Home/" alias setjava11="export JAVA_HOME=/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home/"
Sous IntelliJ, le changement de version de Java peut être effectué à partir des paramètres du projet (la liste déroulante « Project SDK »).
Environnement IC
Du côté de l’Intégration Continue (nous utilisons Bamboo), nous avons mis à jour l’agent pour qu’il utilise Java 11.
Nous avons constaté qu’il n’était pas possible d’avoir des versions différentes de l’agent pour les plans de branche et le plan principal (master), car la configuration du plan est globale. Cela signifie que la mise à jour de l’agent vers Java 11 cassera le plan principal si quelqu’un d’autre commite sur master (par exemple, une nouvelle fonctionnalité ou un correctif de bogue totalement indépendant de la migration vers Java 11).
Pour atténuer ce problème et éviter un build rouge, il était important pour nous de nous assurer que le projet était compilable avec Java 11 et que tous les tests passaient localement avant d’effectuer la mise à jour de l’agent, ceci afin de merger rapidement la branche concernant la migration Java. Une autre option consisterait à remettre temporairement l’agent à Java 8 une fois que le plan de branche est vert sur Java 11 sans oublier de le rebasculer sur Java 11 juste avant le merge.
2. Démarrer le service (compatible Java 11) sur Java 8
Une fois que tout a été corrigé, mergé et que le build principal est au vert, nous nous sommes assuré que la version compatible Java 11 fonctionnait correctement dans nos environnements de test. En gros, nous nous assurions que rien ne soit cassé… Nous avions des tests unitaires, d’intégration et des tests end-to-end, autant dire que notre niveau de confiance était assez élevé. Comme on est jamais trop prudent, nous avons effectué des tests exploratoires manuels sur l’API, avec quelques requêtes exotiques et autres cas en marge afin de nous assurer que le service se comportait correctement. Nous avons également consulté les log et les tableaux de bord Grafana pour s’assurer que tout fonctionnait bien.
La prochaine étape consistait à pousser cette nouvelle version en production. Le service fonctionnait toujours avec Java 8, même si le code était compatible (et compilé) avec Java 11, nous ne voulions pas introduire trop de changements en même temps, nous n’aimons pas les versions risquées après tout. Nous avons traité cette version avec un soin supplémentaire en raison de la refactorisation importante et des multiples montées en version. Après avoir examiné les tableaux de bord Grafana pendant quelques jours, en comparant les métriques avant et après la migration, nous avons constaté que tout s’était bien passé.
3. Démarrer le service sur Java 11
Le but était d’exécuter le service sur Java 11. En théorie, ce serait aussi simple que de mettre à jour le fichier Docker pour utiliser l’image Java 11 et pousser l’artifact en production. Cependant, dans la pratique, ce n’était pas si simple…
Tout d’abord, nous avons dû mettre à jour certains arguments de la JVM (le paramètre -d64
est obsolète et empêchera le service de démarrer, nous avons également dû mettre à jour l’argument concernant les logs du Garbage Collector).
Ensuite, nous avons rapidement réalisé que les logs s’étaient volatilisées dans Splunk pour notre environnement de production interne. En fait, elles apparaissaient dans le future alors qu’il n’y avait aucun soucis avec la production sur AWS. Nous avons dû mettre à jour la configuration logback pour corriger cette « distorsion temporelle » ;) en mettant à jour le date pattern de %d{ISO8601}
à %d{yyyy-MM-dd'T'HH:mm:ss.SSSZ,UTC}
.
Une autre erreur étrange VerifyError: Bad type on operand stack
a fait son apparition pendant le déploiement en production. AppDynamics empêchait le démarrage de certaines instances en raison d’une manipulation exotique du bytecode. Pour des raisons encore inconnues, le déploiement échouait aléatoirement sur une des instances après avoir réussi sur plusieurs autres! Nous avons dû désactiver AppDynamics, ce qui nous convenait bien, car nous n’utilisons pas cet outil dans notre équipe.
Après la migration vers Java 11, nous avons également dû mettre à jour certains de nos tableaux de bord Grafana afin de refléter l’utilisation d’un nouveau Garbage Collector – G1.
Conclusion
Aujourd’hui, 3 services fournissant les notifications utilisateur utilisant Spring Boot 2 et 1 service fournissant l’en-tête et le pied de page du site utilisant Spring 5 fonctionnent sur Java 11, cela fait maintenant plusieurs semaines. Ils utilisent le Garbage Collector par défaut G1 et nous n’avons rencontré aucun comportement étrange lié à la mémoire ou à tout autre problème de performance. Aussi, nous n’avons constaté aucune amélioration de nos temps de réponse. Mais maintenant nous utilisons une version Java LTS (support à long terme), la migration a été un succès.
Et après? Java 12 sera publié en mars 2019. A ce jour, nous ne savons toujours pas si nous utiliserons cette version ou attendrons le prochain Java LTS. Cela dépendra probablement des fonctionnalités incluses.
Commentaires récents