Retour sur les serveurs d'application Java

Au début des années 2000, quand j’ai commencé à travailler, les serveurs d’applications Java étaient à la mode.

Souvenirs

Aujourd’hui, ils sont en voie de disparition, et quand on prononce le mot "J2EE" (ou Java EE comme on dit maintenant), c’est surtout pour évoquer des promesses déçues et des projets terminés dans la douleur.

En repensant à cette époque, je me suis rendu compte d’une chose : certes les spécifications et les produits présentaient beaucoup de problèmes, mais de nombreuses approches qu’on critiquait alors se sont révélées les bonnes sur le long terme.

Je ne veux pas dire que toutes les idées étaient bonnes : certaines étaient franchement mauvaises, et n’auraient jamais pu fonctionner même dans le meilleur de mondes. La multiplication des nouveautés était en elle-même source de difficultés, même les éditeurs le disent.

Si vous êtes un·e jeune développeur·euse, pour qui J2EE évoque une époque lointaine et mystérieuse, comme le Jurassique ou le septennat de Valéry Giscard d’Estaing, cet article vous permettra de comprendre pourquoi les personnes étaient aussi enthousiastes à l’époque, et pourquoi il·elle·s sont parfois un peu dubitatif·ve·s face aux promesses de nouvelles technologies.

Quand l'architecte J2EE vient à l'afterwork, loltoshop par Anaïs Phinith

Si vous êtes une personne qui a vécu cette époque et qui utilise peut-être encore ces technologies, il vous éclairera sur les nouvelles approches utilisées aujourd’hui. Cela pourrait vous donner des idées sur la manière dont on peut désormais répondre à des besoins que vous avez renoncé à satisfaire.

Note : J2EE et serveurs d’applications d’entreprise

Dans l’article, je mélange fonctionnalités J2EE et fonctionnalités qui ne sont pas J2EE mais qui sont fournies par la plupart des serveurs d’applications d’entreprise. Je me permets de le faire pour deux raisons :

  • Les différents serveurs Java d’entreprise ont rapidement fourni le même type de fonctionnalités, qu’elles soient ou pas dans la spécification ;
  • Personne n’a jamais vraiment compris le périmètre de J2EE : entre ce qui était dans la spécification et qui ne fonctionnait pas, ce qui n’était pas dans la spécification et qui fonctionnait mais seulement de manière non portable… personne n’y retrouvait ses petits.

Gestion de la configuration

JNDI permettait de spécifier des paramètres à passer aux applications depuis le serveur. Cela aurait dû permettre de redéployer le même artefact de la recette à la production sans avoir à lui adjoindre de fichiers de configuration spécifiques à chaque étape.

Pourquoi ça ne fonctionnait pas ?

  • La plupart des frameworks Java comme Log4j ne le supportaient pas et ne savaient que lire des fichiers de configuration ;
  • L’outillage et les pratiques n’étaient pas adaptées de sorte qu’on se retrouvait souvent à modifier les variables directement sur les serveurs. Cette modification avait lieu manuellement et sans audit, ni gestion de version, ce qui était donc peu pratique et risqué.

Aujourd’hui : c’est la même approche choisie par Docker via les variables d’environnement ou le service discovery, et les outils se sont modernisés pour s’y adapter.

Gestion de cluster et scalabilité

À l’arrivée des serveurs d’applications, le web était l’avenir, et la manière d’y aller était la scalabilité horizontale. L’approche en vogue consistant à remplacer les mainframes par des fermes de serveurs Unix, moins puissants mais beaucoup moins chers.

Pour limiter la complexité induite par la multiplication des machines, l’idée est de gérer un ensemble de serveurs vus comme une ressource unique et non plus des machines une par une.

Il y avait trois manières de faire du clustering : matériel, fournis par les systèmes d’exploitation, et par les serveurs d’applications.

Le clustering matériel et OS était plus lourd à mettre en œuvre et plus coûteux, il était donc plutôt réservé aux fonctions de stockage, alors que pour les traitements, on préférait essayer de s’appuyer sur les serveurs d’applications.

Pourquoi ça ne fonctionnait pas ?

  • Comme on le verra plus bas, beaucoup de fonctionnalités cluster-aware fonctionnaient mal, on se contentait donc d’utiliser le minimum qui fonctionnait comme le monitoring.
  • Les machines Unix de l’époque, même si elles valaient moins que les mainframes, restaient assez coûteuses en matériel et en licenses.
  • Ajouter de nouveaux serveurs physiques n’était pas simple : il fallait trouver de la place dans des baies, tirer des câbles…
  • Configurer les machines prenait du temps : les installations et configurations étaient en partie manuelles et demandaient des allers-retours entre équipes sous forme de formulaires et de tickets d’incidents.

En pratique, il s’agissait surtout d’être conservateur en utilisant au mieux un nombre limité de machines, avec comme objectif premier la redondance pour éviter les pannes.

Aujourd’hui, si les mainframes sont toujours là, les serveurs Unix eux sont en train de faire leur sortie. Les serveurs x86 qui les ont remplacés sont beaucoup plus puissants pour le même prix, et l’automatisation et la virtualisation permettent plus facilement d’ajouter des serveurs. La scalabilité n’est pas gérée au niveau des serveurs d’applications mais par l’infrastructure et les outils : proxys, réseau… La simplification des architectures ajoutée à l’industrialisation ont permis d’atteindre la cible.

Gestion de session cluster-aware

Les serveurs fournissaient une gestion de session au niveau du cluster, l’équivalent d’un outil NoSQL intégré.

Pourquoi ça ne fonctionnait pas ?

  • Les implémentation fournies nativement par les serveurs d’applications n’était pas à la hauteur en terme de performance et de fiabilité, et les implémentations externes fiables comme Hazelcast ont mis du temps à arriver.
  • Les patterns n’étaient pas encore matures et les développeur·euse·s avaient tendance à vouloir stocker beaucoup trop de choses en session car c’était "magique", ce qui écroulait les performances.

Au final, on utilisait beaucoup l’affinité de session, c’est-à-dire la capacité à lier une session à un serveur en particulier.

Aujourd’hui : on a appris à stocker le minimum dans une session voire à s’en passer, et des solutions NoSQL sont matures et permettent de répondre aux besoins.

Mutualisation des serveurs

Comme on l’a déjà dit, les machines de l’époque coûtaient cher. Pour mutualiser la consommation de mémoire, il était possible de déployer plusieurs applications dans un serveur d’applications. Cette approche était aussi beaucoup utilisée pour limiter les coûts de licences.

Pourquoi ça ne fonctionnait pas ?

L’isolation de composants n’était pas à la hauteur :

  • une application qui consommait trop de ressources mettait en danger les autres ;
  • certaines modifications nécessitaient des redémarrages du serveur, même si elles n’étaient utiles que pour une seule application ;
  • des bibliothèques partagées rendaient les montées de version plus complexes.

Aujourd’hui : on mutualise toujours. La différence est que l’augmentation de la mémoire des serveurs permet de le faire plus "bas" qu’au niveau du serveur : après la phase "une VM pour chaque application", les conteneurs ont la même approche, et les problèmes d’isolation sont de retour.

Bascule à chaud en production

Pour éviter les interruptions de services, les serveurs auraient du pouvoir faire des bascules à chaud sur une nouvelle version d’une application. Sur le papier, l’idée était prometteuse : le serveur routait les nouvelles requêtes sur la nouvelle version, en laissant les requêtes en cours d’exécution se terminer, puis décommissionnait l’ancienne version quand plus aucune requête ne l’utilisait.

Ce déploiement et cette bascule étaient même cluster-aware : les artefacts étaient déployés automatiquement sur tous les nœuds du groupe de serveurs.

Pourquoi c’était peu utilisé ?

  • Cela ne correspondait pas aux pratiques ops de l’époque.
  • Des problèmes de fuites mémoire, certaines dues à des problèmes d’implémentation, d’autres structurelles, rendaient l’utilisation de la fonctionnalité risquée. Mieux valait une interruption de service planifiée qu’un crash inattendu et non prévu.
  • Les montées de version applicatives étaient liées à des mises à jour de données qui étaient rarement prévues pour se faire sans interruption de services. Comme il fallait de toute façon couper les accès pour mettre à jour la base de données, avoir des serveurs d’applications indisponibles au même moment ne posait pas de problème.

Aujourd’hui : les pratiques ops ont beaucoup évolué mais certains problèmes de fuite mémoire sont toujours là. Au final, la bascule se fait plutôt par des proxys réseau qu’au niveau d’un serveur.

Reste parfois le soucis des modèles de données, même si les pratiques se sont améliorées et que le NoSQL apporte des réponses.

EJBs

Les EJBs sont un moyen de packager des groupes de fonctionnalités dans un artefact en exposant une façade normée sous forme de services. L’idée était de permettre de développer des applications complexes en composants des briques élémentaires bien séparées avec des appels transactionnels entre elles tout en permettant de masquer la localisation. C’était l’équivalent des annuaires de services qu’on retrouve aujourd’hui dans les microservices. Lorsque les EJBs étaient déployés ensemble, les appels se faisaient localement, ce qui permettait d’économiser la latence réseau en conservant l’isolation.

Pourquoi ça ne fonctionnait pas ?

  • Un mauvais découpage métier – par exemple la confusion entre composant technique et composant métier – faisait qu’on aboutissait souvent à un plat de spaghetti ;
  • Les découplages de service se faisaient sans découplage de persistance, ce qui limitait l’indépendance des différents composants.

Aujourd’hui : les microservices vont dans la même direction en s’appuyant sur d’autres protocoles. Les avancées dans les pratiques de découpage métier comme DDD, ou l’approche REST qui consiste à exposer uniquement des ressources, peuvent faire en sorte que les résultats soient meilleurs.

JAAS

JAAS est la partie sécurité de J2EE, elle permet de faire du contrôle d’accès au niveau des services, par annotations ou à l’aide de XML. Cela permet de gérer la sécurité de manière déclarative.

Pourquoi ça ne fonctionnait pas ?

  • La spécification JAAS n’était pas assez complète, ce qui nécessitait de faire du spécifique pour chaque éditeur.
  • L’API Security Provider à utiliser pour des implémentations spécifiques était très mal documentée et mal supportée.
  • Le contrôle d’accès n’était pas au niveau de la donnée, ce qui obligeait à implémenter une deuxième couche de sécurité au niveau du code.

Aujourd’hui : JAAS est remplacé par des frameworks plus léger comme Spring Security, qui peuvent s’appuyer sur JAAS suivant les cas mais qui en masquent les limites.

Redéploiement à chaud en développement

La JVM était lente à démarrer, les applications lentes à déployer, et J2EE rendait difficile d’écrire du code facile à tester hors du serveur. Pour accélérer le cycle le développement, l’idée était de permettre un redéploiement à chaud de l’application sans avoir à tout recharger pour que le·a développeur·se ne soit pas interrompu·e dans son travail.

Pourquoi ça ne fonctionnait pas ?

  • Pendant longtemps, la fonctionnalité n’a pas été stable, ce qui faisait perdre du temps : "est-ce-que c’est un bug dans mon code ou est-ce-que c’est le rechargement qui a cassé un truc ?".
  • Seuls certains types de modifications étaient valides (typiquement celles qui étaient limitées à l’intérieur de classes), et celles qui ne l’étaient pas n’étaient pas documentées et ne généraient pas d’erreur.

Au final, la meilleure approche était de s’en passer, quitte à ajouter des couches d’indirections pour isoler artificiellement le code.

Aujourd’hui, la JVM et les serveurs d’applications ont été optimisés et les processeurs vont beaucoup plus vite. JEE de son côté a pris en compte ces problèmes et permet aujourd’hui de tester hors serveur.

Les alternatives à JEE tels que DropWizard ou Spring sont d’ailleurs encore plus rapides.

Les limites qui ont causé la nécessité d’avoir cette fonctionnalité ayant disparues, elle est désormais inutile.

Pour conclure

Cette revue permet de dégager deux choses :

Beaucoup d’idées ont échoué pour cause de maturité autant, voire plus, que pour des raisons techniques.

Ensuite, les serveurs d’applications essayaient de résoudre beaucoup de problèmes tout seuls. Aujourd’hui, les solutions sont réparties à différents niveaux de la stack : de l’OS à la configuration réseau. Les causes sont multiples : nouvelles technologies, normalisation de l’utilisation de plusieurs langages, baisses des prix des serveurs … Cela permet de diminuer la complexité de ce qui est demandé aux stack applicatives et donc de faciliter l’adoption de nouvelles technologies. Cela veut aussi dire que les serveurs d’applications à l’ancienne sont désormais un poids mort dans un SI.

Les principales raisons de les conserver aujourd’hui sont le coût de la migration, les questions de licences et de support, et potentiellement l’intégration avec le reste de l’écosystème de l’éditeur.

Avec le temps qui passe et le mûrissement des alternatives plus légère comme Spring ou DropWizard, la force de ces arguments diminue petit à petit. En attendant que le serverless ou une autre approche les rendent à leur tour obsolètes.

Espérons que les serveurs d’applications pourront bientôt profiter de leur retraite bien méritée.