home : Développement : Base de données :
Gestion des connexions non-transactionnelles #1
Mardi, 15 Mars 2011 13:49 PDF Imprimer Envoyer

OpenSessionInViewFilter, TransactionManager et autres TransactionInterceptor sont les éléments de configuration qui apparaissent très régulièrement lorsqu'il s'agit d'ouvrir et de fermer automatiquement des connexions et/ou des transactions sur les bases de données de projets J2EE. Généralement indistinctement branchés sur toutes les méthodes des couches supérieures des projets, que ce soient des Web Services ou une façade métier, ils conservent ouvertes les connexions durant tout le temps de l'appel, comme le requièrent tous les process métier transactionnels.

Or on oublie souvent que toutes les méthodes n'ont pas nécessairement besoin d'une transaction complète. C'est par exemple le cas des fonctions de lecture seule n'ayant aucun impact sur les données métier. Que se passe-t-il lorsqu'une ressource distante (un Web Service fournisseur, un FTP, ...) répond lentement ? La transaction reste ouverte empêchant la connexion, inactive, de se libérer et de réalimenter le pool de connexions, nécessaire aux autres appels. Peut s'ensuivre un empilement puis un rejet des appels en attente d'une connexion disponible.

Cette série d'articles se propose de fournir une solution alternative aux configurations classiques, en permettant une meilleure gestion des connexions non-transactionnelles.

Ce premier article pose les bases du cas d'étude et revient sur les problèmes rencontrés ayant conduit à la recherche d'une solution possible.

Description du projet

Le mini-projet que nous vous proposons d'utiliser pour servir d'exemple à la solution est disponible en téléchargement à cette adresse. Nous vous engageons à le consulter pour mieux comprendre l'article et reproduire les exemples.

Ce projet reprend une architecture classique N-tiers, avec une couche modélisant la base de données que nous appellerons Business Object (BO).

L'utilisateur accède à l'application à travers une couche Facade exposant les fonctionnalités métier relatives, dans notre exemple, à la gestion de clients. Cette couche Facade se base elle-même sur les habituelles couches Service et Dao.

Notre façade exemple expose deux méthodes:

  1. getCustomer(int customerId), récupérant et retournant le client dont l'identifiant est passé en paramètre,
  2. updateCustomerName(int customerId, String newLastName), permettant la mise à jour du nom de famille du client.

Nous ne nous attarderons pas sur la méthode updateCustomerName, qui répondra aux configurations classiques. En revanche, nous allons détailler la méthode getCustomer, afin de présenter le cas d'étude complet. Voici le diagramme de séquence simplifié de cette méthode :

Description des enchaînements d'appels

Description des étapes :

  1. L'utilisateur appelle la méthode getCustomer sur la façade,
  2. La façade interroge la couche Service pour récupérer le client à partir de son identifiant,
  3. Le service interroge la couche Dao pour récupérer le client à partir de son identifiant,
  4. On souhaite compléter les informations stockées en base de données avec des informations provenant de Web Services fournisseurs (dans notre cas, nous souhaitons récupérer les produits liés au client),
  5. Une partie des informations stockées en base de données n'ont pas été remontées au premier appel. Avant de retourner l'ensemble des informations, nous refaisons un appel à la couche Service ...
  6. ... puis à la couche Dao.
  7. Enfin, nous retournons l'ensemble des informations du client à l'utilisateur.

A aucun moment, cette fonction n'est venu mettre à jour une information en base de données. Il s'agit donc d'une méthode en lecture seule.

Description des problèmes possibles

Le process de la fonction détaillée ci-dessus dépend fortement du Web Service fournisseur appelé : il est obligé d'attendre le retour du Web Service pour remonter les informations à l'utilisateur. Si le Web Service met du temps à répondre, la fonction getCustomer sera directement impactée.

Étudions un peu ce qui peut se passer dans ce cas : dans une configuration classique, le process commence par ouvrir une connexion à la base de données. Nous supposons le projet relié à un pool de connexions servi par son serveur d'application. Le pool de connexion dispose de 20 connexions simultanées maximum.

Le premier appel à la méthode getCustomer met du temps à répondre, puisque le Web Service fournisseur tourne au ralenti (pour une raison quelconque qui nous importe peu). Cet appel dure plusieurs centaines de millisecondes, voire plusieurs secondes. Pendant ce temps un deuxième, puis un troisième appel arrivent à notre application. De même, ils ne répondent pas aussitôt, puisque le Web Service fournisseur n'est toujours pas revenu. De nouveaux appels arrivent et chaque fois une nouvelle connexion est ouverte et retirée du pool de connexions, et ainsi de suite.

Si le temps de réponse du Web Service fournisseur ne s'améliore pas et que le nombre d'appels à notre application est important, nous pouvons très vite atteindre les 20 connexions maximales autorisées. Va s'ensuivre l'inévitable : le 21ème appel arrive et demande une connexion qui est indisponible. Il sera rejeté peu de temps après (suivant la configuration du temps d'attente de votre pool).

Il n'est pas impossible que l'engorgement se résolve de lui-même. Mais généralement, un problème n'arrive jamais seul et l'explosion du serveur n'est pas loin.

Le pire, dans tout cela, c'est que les connexions à la base de données ont été maintenues ouvertes alors qu'elles n'étaient pas nécessaires, puisque le process bloque sur l'appel au Web Service, hors contexte de base de données.

Premières solutions possibles

Bien entendu, vous pouvez augmenter le nombre de connexions disponibles sur votre pool. Mais votre administrateur de base de données va très vite repérer les 200 connexions ouvertures simultanément et vous tirer les oreilles plus ou moins méchamment suivant son humeur. De plus, cette solution ne fait que repousser votre problème en en diminuant la fréquence, mais ne le supprime pas.

Vous pouvez également configurer le temps d'attente de votre pool de connexion pour rejeter plus rapidement les appels en trop et soulager votre serveur. Cela ne résout pas non plus votre problème mais peut aider à soulager les services. Voici un exemple de configuration d'un pool de connexion à déployer sur un JBoss (notez que le blocking-timeout-millisec a été descendu à 3 secondes au lieu de 30 secondes par défaut):

 
<datasources>
   <localtxdatasource>
      <jndiname>MyDataSourcePool</jndiname>
      <usejavacontext>false</usejavacontext>
      <connectionurl>
         jdbc:mysql://myserver:3306/mybase?autoReconnect=true
      </connectionurl>
      <driverclass>com.mysql.jdbc.Driver</driverclass>
      <username>XXX</username>
      <password>YYY</password>
      <minpoolsize>2</minpoolsize>
      <maxpoolsize>20</maxpoolsize>
      <blockingtimeoutmillis>3000</blockingtimeoutmillis>
      <checkvalidconnectionsql>select 1</checkvalidconnectionsql>
   </localtxdatasource>
</datasources>
 

Une autre solution consiste à jouer sur le temps de connexion aux Web Services fournisseurs, en définissant un timeout plus faible que celui par défaut ou celui que vous avez déjà défini. Cependant, si vous baissez trop le timeout vous vous exposez à un taux d'échec trop important : si certains Web Services fournisseurs sont consommateurs en temps, vous risquez de systématiquement les rejeter.

Notez que toutes ces solutions consistent dans des configurations faciles à mettre en place. A défaut de résoudre le problème, elles devraient déjà vous permettre de le limiter.

 

Le prochain article présentera la configuration classique que l'on trouve dans la plupart des projets (ouverture et fermeture de la connexion sur la couche Façade) et présentera les résultats qui permettront la comparaison avec les solutions futures.

Mise à jour le Lundi, 21 Mars 2011 07:29 Thomas Huguerre
 

Ajouter un Commentaire


Code de sécurité
Rafraîchir