Ma première expérience sur l’environnement Java / Tomcat / MySQL (Application de géolocalisation de bus urbain)

 

Bus_tracking

 

Dans mon parcours professionnel, j'ai travaillé 3 mois début 2016 dans le même bureau que l'équipe de développement qui maintenait une application de géolocalisation de bus, permettant par exemple d'afficher aux arrêts le temps d'attente. Je tiens à préciser que pour ma part je travaillais sur un autre projet en ASP.NET MVC sans aucun rapport, mais parfois pour me changer les idées, je dialoguais avec eux. Ils travaillaient sur cette application impressionnante écrite en Java couplé au capricieux MySQL, et bien sur il y eu quelques péripéties techniques…

 

 

 

Pour avoir un ordre d'idée de l'environnement nécessaire à ce genre d'application, voyons l'exemple de cette grande ville d’Écosse qui souhaitait donc afficher dans les panneaux d'arrêt les infos sur ses bus en circulation : la technologie d'affichage retenue pour y répondre fut la page web.

Le volume de requêtes sur nos serveurs pour ce client était de 300 000/jour maxi. Au niveau matériel, 16 Go de RAM et 16 CPUs étaient vus par l'OS de la machine virtuelle, car avec 2 x 4 cores physiques et un coefficient x2 par core grâce à l'hyper threading, on obtiens au final 2 x 4 x 2 = 16 cores.

L'architecture était donc composée de deux machines virtuelles : une pour le front end web écrit en PHP et une pour les web services écrit en Java. Le client a donné également les accès aux développeurs tiers, il y donc plusieurs autres applis mobiles qui se connectaient également aux web services.

On apprend beaucoup plus quand il y a des soucis que quand tout roule à merveille : ainsi pour un autre client de l'application, le gérant du réseau de bus d'une grande ville Canadienne, chaque nuit la base de donnée subissait un goulet d'étranglement, avec comme symptôme une saturation de la mémoire du serveur d'application en objets de connexion JDBC, et un accroissement anormal du nombre de ligne dans la table de statistiques…

Tout d'abord la première chose surprenante est la différence d'environnement : c'est le client qui gérait son propre serveur, avec seulement 8 Go de RAM et 4 CPUs physiques visibles pour la machine virtuelle de web services ; la ville utilisatrice de l'application fait quand même 1,65 millions d'habitants alors que la ville du client précédent, ayant 4 fois plus de CPUs, fait seulement 500 000 habitants.

On serait tenté de dire que le serveur du client Canadien n'était pas assez puissant… où alors que le serveur du client Écossais était surdimensionné… Je suis incapable de trancher mais je pencherais sur la première car si l'autre client n'avait pas rencontré ces problèmes, c'était justement peut-être grâce à sa config plus musclée. Et même notre expert réseau était circonspect : la seule chose qu'il savait c'est qu'une estimation de montée en charge avait été réalisée. Mais est-ce que cette dernière ainsi que l'ensemble des préconisations faites au départ par l'équipe de développement avaient bien été respectées ?

Bref, il faut faire avec les choix de client pas toujours logiques et faire avec ce que l'on nous donne comme environnement. Le problème était récurrent et surgissait donc à 4h du matin heure locale du Canada sur les deux SGBDR quasi-simultanément : la base de donnée chez le client, et la base de statistiques située sur nos serveurs. Au prime abord tout le monde fut un peu décontenancé car il y a logiquement peu d'utilisation du service à cette heure là.

Un collègue suggéra alors de faire une classe de test permettant de faire une pile d'appel afin d'isoler le problème (application ou base de donnée responsable ?). Mais le chef de projet pensa que cela ne venait pas de MySQL car le driver JDBC ne plantait pas en mémoire et le serveur MySQL non-plus (effectivement ce dernier peut planter lorsque le nombre de threads devient trop important). Bref pendant plusieurs jours régnait une sorte de flou autour des raisons du problème.

On appris ensuite que l'insertion quotidienne des nouveaux horaires dans le SGBDR par le client avait lieu à 4h du matin, heure où le goulet d'étranglement pointait le bout de son nez. Même si la nuit circulent des bus, assez peu de personne font des recherches d'horaire : c'est donc effectivement un moment propice pour le client de faire ce genre d'opération de maintenance. En tout cas cela démontrait qu'il y avait bien une activité à cette heure là, et donc le fait d'avoir des problèmes dans cette tranche horaire paraissait déjà moins saugrenue, d'autant plus que l'opération la plus lourde avec un SGBDR est bien l'insertion de donnée car il y a reconstruction de l'index des tables concernées.

Les premières pistes pour alléger la charge sur MySQL fut de limiter le seuil de connexion dans le pool. Ce dernier est d'ailleurs assuré par le module Tomcat DBCP. Il est obligatoire car les connexions JDBC ne sont pas Thread Safe, c'est-à-dire que vous ne pouvez pas les partager entre les threads de l'application. Pour rappel un thread est crée par la Servlet (le contrôleur) pour chaque requête cliente.

Une autre piste fut évoquée : purger la table de statistiques avec la commande troncate. Effectivement, la consolidation ne fonctionnait plus, provoquant une accumulation de 20 millions de ligne dans cette table, ce qui correspondrait environ à un 20 jours de travail car le volume quotidien est d'un million de requêtes (1 Go/s de une bande passante). J'estime donc que le problème était survenu depuis 20 jours. Est-ce qu'une nouvelle portion de code aurait pu être l'origine du phénomène ? Encore un mystère.

En tous cas il n'est jamais bon d'avoir trop d'enregistrements, car d'après certains forums, une table avec jointure peut bien aller jusqu'à des 64 millions de lignes sans problème en lecture, par contre en écriture le temps est nécessairement plus long dé-lors qu'une table contient plusieurs millions de ligne et ce à cause des index, pouvant expliquer certaines de nos difficultés avec cette table de statistiques…

Pour rappel, pour qu’une consolidation soit possible il faut au moins une relation entre deux tables. En cas de suppression d’un enregistrement dans la table mère, l’enregistrement de la table fille n’a plus de référence à la table mère. L’information serait perdue sans la consolidation. L’utilisation d’un trigger est d'ailleurs plus avantageuse par rapport à une solution logicielle de couche supérieure comme Java, qui alourdirait l’opération.

Enfin, pour remédier aux problèmes de saturation de la mémoire du serveur d'application, nous avons opté pour un redémarrage quotidien de Tomcat, en attendant de trouver une vrai solution. Cela permettait d'effacer de la mémoire de manière radicale toutes les connexions échouées. Un collègue s'est d'ailleurs étonné que ces erreurs n'aient pas été retenues par des blocs Try/Finally, permettant aux connexions échouées d'être fermées dans la foulée.

Sans soute que le code méritait effectivement d'être revu car des oublis de fermetures de ressources peuvent toujours arriver mais nos soupçons se dirigeaient aussi sur la JVM : d'après cet article, même si on ferme correctement les Statements et les ResultSets, la connexion continu de les utiliser en arrière plan, empêchant le Garbage Collector de les détruire.

 

Where-is-memory-leak-300x184

 

En fait, pour résumer ce que j'ai compris de ce plantage quotidien généré par une opération de maintenance, tout commence par des ressources non-libérées par le Garbage Collector : soit elles étaient non-libérables car toujours utilisées (memory leak), soit le GC mettait trop de temps à se déclencher (nous parlions entre nous d'un seuil de 300 Mo, mais je n'ai pas vérifié la cohérence de cette thèse). D'ailleurs pour ce dernier cas, on pourrait être tenté de faire appel au GC manuellement mais cela est déconseillé.

Bref, même si on ne sait pas vraiment qui fout le bordel, le résultat était sans équivoque : saturation de la mémoire et assèchement du pool en connexions libres, et sans connexions disponibles, la consolidation, chargée de mettre à plat les informations obsolètes avant que les nouvelles infos d'horaire soient insérées, ne pouvaient plus s'effectuer.

Je n'ai pas assisté à la suite des événements, mais la morale de cette anecdote est multiple : premièrement, Tomcat est employé pour de grosses applications Java. Mon formateur AFPA m'a pourtant dis que Tomcat n'est pas un modèle de sécurité contre le piratage, au contraire de Glassfish ou Jboss. Mais probablement que l'architecture réseau en amont du serveur est bien sécurisée, en tout cas pour une entreprise de cette taille, j'ose l'espérer.

Deuxièmement, MySQL n'est pas le serveur de base de donnée idéal sur des grosses applis, car :

  • Il est non multi-thread, c'est-à-dire qu'il ne gère pas les processus multiples

  • Il supporte mal les multiprocesseurs

  • Pas de priorité affectable aux threads

  • Non-certitude du fait qu'un thread soit alloué pour chaque connexion

  • Gestion mémoire peu souple etc.

Troisièmement, avec Java, on est pas maître du Garbage Collector, et lorsque la gestion de la mémoire est critique en raison d'un grand nombre de requête vers le SGBD, il faut s'attendre à voir débarquer des memory leak et tous les soucis qui vont avec…

 

Laisser un commentaire