Le déclin des serveurs d’applications Java

tomcat-dockerLes serveurs d’applications Java sont-ils en train de mourir à cause de Docker? c’est la question que s’est posé James Strachan dans son article écrit en anglais: the decline of Java application servers when using docker containers
Trouvant l’article particulièrement intéressant, j’ai demandé l’autorisation à son auteur pour le traduire afin de vous le proposer en français. L’article est traduit le plus fidèlement possible, seul la conclusion a été raccourcie et adaptée mais elle reste conforme à la vision de l’auteur initial.

Depuis des années, l’éco-système Java utilise les serveurs d’application. Le principe d’un serveur d’application Java (Servlet, JEE ou OSGi) repose sur un processus Java dans lequel on déploie & retire du code grâce à une archive de déploiement (jar, war, ear, bundle, etc). Le processus Java reste donc actif dans le temps et s’adapte au code. Les serveurs d’application ont donc le plus souvent un répertoire ou l’on dépose  une archive qui va modifier le code en cours d’exécution.


Par le passé, la mémoire vive coûtait  très chère, il y avait donc du sens à regrouper plusieurs applications au sein d’un même processus java. L’empreinte sur la mémoire vive d’un seul processus était beaucoup plus faible que plusieurs processus qui tournent en même temps pour chaque application.

C’était une pratique vraiment très courante depuis des années, toutefois,dans les faits, on ne pratiquait que très rarement le redéploiement à chaud en production. A cause des des nombreuses fuites de ressources ( threads, mémoire, database, sockets, etc), on préférait souvent mettre en place une solution de redémarrage du serveur d’application par sécurité ( également appelé la technique du “juste au cas où”).

Les solutions les plus avancées utilisent le déploiement en parallèle (également appelé blue-green deployment) pour s’assurer que les serveurs d’applications ne restent pas indéfiniment en marche, et qu’ils sont redémarrés au moins une fois à chaque les mises à jour.

D’un point de vue technique, on utilisait quand même le mécanisme du redéploiement à chaud des serveurs d’application: On démarre un nouveau serveur, on déploie l’application grâce à une archive, on redirige le trafic sur le nouveau serveur, et on éteint ensuite l’ancien serveur.

Les Micro-services

Il y a depuis quelque temps une nouvelle tendance émergente, les nouvelles applications évoluent vers des micro-services et remplacent progressivement les anciennes applications monolithiques. Le principe des micro-services consiste à réaliser plusieurs petites applications qui répondent chacune à un besoin spécifique et dans un périmètre d’action donnée et proposant le plus souvent une api REST. Chaque micro-service évolue séparément des autres pour s’adapter au besoin qui lui est confié. Ce sont donc plusieurs micro-services autonomes qui répondent tous ensemble à la totalité du besoin client. Pour gérer ces nouveaux micro-services, on a une fois de plus fait appel aux serveurs d’applications. Un seul processus java pouvant faire tourner plusieurs dizaines de micro-service, la mise à jour d’un seul de ces services pouvait faire planter la JVM et affecter ainsi tous les autres.

Pour diminuer le risque et gagner en agilité, il était donc préférable de lancer chaque micro-service dans sa propre JVM et de mettre à jour n’importe lequel de ces services à tout moment sans affecter les autres services. En ayant plusieurs processus indépendants au lieu d’une seul grosse application centralisée et monolithique, cela permet ainsi d’améliorer le monitoring et de comprendre comment chaque service utilise ses ressources : mémoire, cpu, réseau, disque, etc.

La révolution docker

Les containers Docker sont une solution idéale pour packager les applications et permettre un déploiement sur les machines Linux :

  • les containers utilisent des images immutables pour l’OS et pour le code java executé
  • ils sont isolés les uns des autres et peuvent être restreint en terme de ressources (IO, mémoire, CPU)
  • Il ne sont pas uniquement liés à l’écosystème java mais s’adapte à n’importe quel technologie ou langages (Python, Ruby, NodeJs, goland, et tant d’autres)

Un des principal atout de Docker est sa capacité à pouvoir lancer plusieurs containers d’une même image sur n’importe quel machine. Docker fonctionne de manière à pouvoir répéter les mêmes opérations plusieurs fois en réutilisant des images immutables. Les volumes docker permettent de stocker et de sauvegarder ce qui a besoin de l’être à chaque démarrage d’un nouveau container d’une même image.

La méthode Docker fonctionne bien avec les serveurs d’applications Java, il suffit de créer une image contenant le serveur d’application (Tomcat, Jboss, Glassfish) mais également l’archive de l’application à déployer en production. Au lancement du container, le serveur d’application va déployer et lancer l’archive.

Cette image Docker est immuable, elle n’évolue pas à chaque mise à jour de l’application. Pour mettre à jour une application, il suffit de créer une nouvelle image Docker contenant la nouvelle version de l’application. Chaque image Docker correspond à une version de l’application, et il est dès lors possible de relancer n’importer quel version de l’application (les images docker étant versionnées dans un registry/hub)

Avec Docker, l’intérêt des serveurs d’applications devrait progressivement disparaître. Il n’y a plus de raison d’avoir un seul processus java, qui déploie à chaud chaque nouvelle version du code. La responsabilité du déploiement à chaud d’une nouvelle version est transféré à Docker. C’est lui qui va gérer le remplacement d’un container par un autre, le plus souvent avec du load balancing en amont.

Configuration

Avec les serveurs d’application, on est déjà habitué  à créer des archives de déploiement immuable (jar/war/ears/bundles). La même archive est déplacée d’un environnement à l’autre pour effectuer une installation. Pour que la même archive fonctionne sur plusieurs environnements, nous déléguons au serveur d’application la configuration des ressources ( comme par exemple l’accès à la base de données avec  JNDI).

Il est ainsi facile de déployer rapidement une même archive sur un ensemble de serveurs dans un cluster, une fois celui-ci correctement configuré. Mais, à vouloir faire tourner la même application sur plusieurs systèmes d’exploitation, différentes versions de Java et/ou du serveur d’applications, cela reste très facile de tout faire planter lors d’un changement d’environnement. Un paramètre manquant sur un des serveurs de production et c’est l’échec assuré lors de la mise en production, alors que l’archive a pourtant passé avec succès la phase de recette.

L’approche de Docker va donc plus loin, au lieu d’avoir uniquement l’archive immuable, Docker rend également immuable le système d’exploitation, le serveur d’application et la configuration système. C’est donc la même image de l’OS, de Java et de l’application qui est exécuté d’un environnement à l’autre. Il n’y a donc que très peu de chance de rencontrer une erreur de configuration lors du passage en production puisque c’est exactement la même image Docker que celle testée et validée en recette.

Pour que cela fonctionne bien sans configuration, il faut le plus souvent mettre en place un Service Discovery dans chaque environnement qui va se charger de mettre en relation les différents containers (Zookeeper ou Kubernetes pour Google). Ces Services Discovery vont permettre à la même image Docker de se lier à la base de données correspondant à son environnement par exemple. Cela permet à des solutions clusterisés de se découvrir automatiquement quelques soit le nombre d’instances lancés comme cela peux être le cas avec  ElasticSearch ou MongoDB, ou encore des systèmes de messaging.

Pour résumer

Est-ce que les serveurs d’application sont morts? Dans un monde Docker, il n’y a pas plus vraiment besoin de déployer à chaud du code java sur un serveur d’application en production. Le serveur d’application est déployé avec l’application à chaque version d’une image Docker.

Le redéploiement à chaud en développement semble toujours une fonctionnalité intéressante, mais elle n’est pas réservé aux serveurs d’applications. Il existe déjà de nombreuses solutions pour faire ça autrement (JRebel ou la plupart des débuggers utilisés par les IDE)

Les serveurs d’applications d’applications ne sont pas mort, mais ils sont en train de se transformer pour ressembler d’avantages à des frameworks ou des librairies qu’ont va pouvoir inclure ou pas à nos projets. Les serveurs d’applications reposent sur des standards (Servlet, JSP, EL, WebSocket) et proposent des implémentations de ces derniers qui ont fait leurs preuves. A titre d’exemple, le framework Spring Boot lance désormais une version embarquée de Tomcat. Les rôles se sont donc inversés, c’est désormais l’application qui lance son propre serveur d’application.

La plupart des développeurs Java ont appris à utiliser les serveurs d’applications Java et devrait continuer à les utiliser dans un monde Docker. Mais alors que le choix d’un serveur d’application était jusqu’à maintenant un critère déterminant pour la réalisation d’un projet, il devrait passer progressivement au second plan. Une partie des frameworks de plus haut niveau comme Spring Boot ont rendu l’utilisation d’un conteneur d’application transparent. Les possibilités de configuration des serveurs proposés auparavant par Tomcat et autres sont désormais remplacés par des solutions Docker. La solution la plus simple est d’utiliser les liaisons inter-container (link) ou encore Docker Swarm dans un environnement multi-host. Pour des environnement plus complexe faisant intervenir un plus grand nombre d’instances, il existe de nombreuse solutions de  Service  Discovery qui sont apparu ou qui se sont adapté à Docker :  Kubernete, Zookeeper, etcd.