Créer et monitorer un pool d'objets avec Spring

Les intérêts d’un pool

On rencontre souvent la notion de pool dans un serveur d’application Java.
L’exemple probablement le plus connu est le pool de connections JDBC associé aux datasources :
Tomcat par exemple crée les pools JDBC à l’aide de commons-dbcp (basé sur commons-pool)

Un pool regroupe plusieurs instances équivalentes d’une même classe que différents threads peuvent utiliser. (Une instance n’étant utilisée que par un thread à la fois)
Tout traitement ayant besoin d’une instance doit suivre le cycle suivant :

  • il emprunte une instance au pool. L’instance est alors réservée à son seul usage.Dans le cas d’un pool JDBC, il s’agit le plus souvent d’un appel à getConnection sur une datasource
  • il utilise cette instance comme bon lui sembleDans le cas d’un pool JDBC, il s’agit d’effectuer des insert/select sur la base de donnée à l’aide de la connection
  • il rend l’instance au poolDans le cas d’un pool JDBC, cela arrive typiquement à l’appel de la méthode close() sur la connection JDBC ou au commit de la transaction

Le pool gère lui-même plusieurs aspects qui sont transparents pour le traitement client :

  • la création / la configuration des instances
  • le nombre d’instances présentes dans le pool : en particulier un pool peut souvent être configuré pour ne pas permettre l’utilisation de plus d’un certain nombre d’instances

Un pool est souvent nécessaire lorsque deux conditions sont réunies :

  • les instances en question ne sont pas thread-safe : une unique instance (singleton) utilisée en même temps pour tous les threads ne conviendrait donc pas
  • on ne veut/peut pas se permettre de créer une instance à chaque fois que nécessaire : par exemple, si le coût d’intialisation est assez élevé et/ou que chaque instance est associée à des ressources systèmes externes (c’est le cas des connections JDBC qui ouvre une session sur la base de donnée)

Un pool a aussi souvent un autre but (complémentaire de conditions ci-dessus) : limiter le nombre maximal de ces instances utilisables par le système.
C’est ce dernier aspect qui m’a occupé récemment.

Le besoin était simple :
A un instant donné, notre application ne devait pas effectuer plus de n appels concurrents sur un système externe.

C’est en particulier sur cet aspect que cet article insistera.

Créer un pool custom avec Spring

Pour mon besoin, il me suffit donc de créer un pool d’objets client et de configurer une limite maximale sur le nombre d’instance qu’il contient.

Spring vient avec une solution toute faite ne nécessitant que de la configuration. Elle se base sur l’infrastructure Spring AOP et en particulier sur la notion de TargetSource qu’elle propose.

Le principe est simple :
créer un proxy qui à chaque invocation emprunte une instance au pool géré par la TargetSource, effectue l’appel sur l’instance ainsi obtenue puis libère l’instance en la rendant à la TargetSource (et donc au pool) avant de répondre à l’appelant.
Ce mécanisme est alors complètement transparent au code client qui croit effectuer un appel sur le singleton que lui a injecté Spring.

Je reprendrais ici l’exemple quelque peu modifié de la documentation de référence de Spring (Chapitre 8.10 Using TargetSources).

Le proxy s’appelle ici “businessObject”. Il est créé par une factory de proxy (ProxyFactoryBean) dont la seule particularité est de spécifier une targetSource (la plupart du temps, la target d’un proxy est spécifié par un targetName fixe qui correspond au nom d’un des beans du context Spring). Aucun intercepteur AOP n’est spécifié ici sur le proxy, mais rien ne l’empêche : on peut par exemple utiliser une instance de SimpleTraceInterceptor pour tracer chaque appel effectué sur le proxy.
La targetSource utilisée : “poolTargetSource” ; est une instance de CommonsPoolTargetSource. Cette classe de Spring utilise un pool créé à l’aide de la librairie commons-pool (qui doit donc être disponible dans le classpath)
Deux propriétés ont été spécifiées sur cette targetSource :

  • le nom du bean Spring permettant de créer les instances à mettre dans le pool : “businessObjectTarget” ici. On notera que la définition de ce type DOIT absolument spécifier un scope “prototype”. En effet, ce scope est nécessaire pour que le pool obtienne une nouvelle instance à chaque fois qu’il demandera ce bean au contexte Spring.
  • la taille maximale du pool : ici 25

J’ai rajouté la définition d’un bean “myService” pour montrer que le proxy ainsi créé prend la place des vrais instances de MyBusinessObject lors de l’injection dans les classes appelantes (ici une instance de com.mycompany.MyService)

Voila, c’est tout, aucune modification du code de com.mycompany. MyService ni de com.mycompany. MyBusinessObject n’a été nécessaire.

Monitorer le pool

Me voila donc avec un pool tout beau, tout propre qui répond pile-poil à mon besoin.
Oui mais voilà, que ce passera-t-il lorsque le nombre limite d’appels concurrents sera atteint ?

Le comportement par défaut de ce pool est de bloquer les threads qui demandent un objet le temps qu’un de ces objets soit rendu au pool. Un timeout est positionnable pour limiter dans le temps ce blocage.

On l’imagine aisément : au moindre pic d’activité, le paramétrage du pool (taille maximum et timeout) peut influencer fortement les performances.

Le tuning d’un pool, ou le diagnostique des contentions qu’il peut entrainer, nécessite d’avoir un certain nombre d’indicateurs.
Les indicateurs classiques disponibles sur les ressources des serveurs d’application (ejb, connection jdbc, …) sont souvent :

  • Indicateurs en temps réel : - nombre d’objets du pool en cours d’utilisation
  • nombre de threads bloqués en train d’attendre qu’un objet se libère (“Waiters”)
  • Indicateurs statistiques : - le nombre maximum de threads ayant été bloqués à un instant donné
  • le temps maximum qu’un thread ait attendu pour obtenir un objet du pool
  • le nombre d’appels ayant échoués suite à un timeout
  • etc…

CommonsPoolTargetSource n’expose malheureusement qu’une seule de ces métriques : le nombre d’objet en cours d’utilisation, disponible via l’appel à la méthode getActiveCount (cette limitation provient en fait de la classe GenericObjectPool utilisée pour implémenter le pool)

Afin d’obtenir une partie de ces indicateurs, ou du moins d’en avoir une idée, il est possible de sous-classer CommonsPoolTargetSource

Voici par exemple l’ajout de l’indicateur sur le nombre thread en attente :

 package util; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.aop.target.CommonsPoolTargetSource; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; @ManagedResource public class AuditableCommonsPoolTargetSource extends CommonsPoolTargetSource { AtomicInteger numWaitingCallers = new AtomicInteger(); @Override public Object getTarget() throws Exception { numWaitingCallers.incrementAndGet(); try { return super.getTarget(); } finally { numWaitingCallers.decrementAndGet(); } } @ManagedAttribute public int getNumWaitingCallers() { return numWaitingCallers.get(); } @Override @ManagedAttribute public int getActiveCount() { return super.getActiveCount(); } }

Il faut noter que l’indicateur numWaitingCallers ainsi calculé n’est pas tout à fait celui recherché. En effet, ici, on calcule le nombre de thread qui à un instant t sont en train de demander une instance au pool ; cela ne signifie pas forcément qu’ils sont bloqués à cause d’une saturation du pool.
En particulier, si le temps de création des objets gérés par le pool est long et que de nombreux threads demandent une instance au démarrage, le nombre de waiter sera relativement élevé sans que cela soit vraiment problématique.
Une fois le pool initialisé cela devrait toutefois être une approximation correcte.

Exposition JMX des indicateurs

Il nous reste une dernière chose à faire, rendre disponible ces indicateurs à la demande.
Vous aurez noté que j’ai annoté la classe avec des annotations @Managed* de Spring. Elles permettent d’exposer très rapidement ces indicateurs en JMX : il suffit ensuite d’ajouter context:mbean-export/ dans la configuration de Spring pour qu’il s’occupe de tout.
Pour trouver le nom du mbean qu’il expose, vous pouvez regarder les traces INFO de Spring lors de l’initialisation du contexte :

1225 [main] INFO org.springframework.jmx.export.annotation.AnnotationMBeanExporter - Located managed bean 'poolTargetSource': registering with JMX server as MBean [com.mycompany:name=poolTargetSource,type=MyBusinessObject]

( En fait, comme il s’agit d’une TargetSource, la règle de construction par défaut de Spring est un peu plus compliquée que celle décrite ici.
Dans ce cas particulier, la règle de nommage par défaut est plutôt :

<package_de_la_classe_target>:name=<bean_name_de_la_targetSource>,type=<classe_de_la_target> )

Il ne reste plus qu’à accéder à ce MBean avec votre client JMX favori.

Si vous êtes en jdk6 et que vous pouvez lancer JConsole sur la machine hébergeant votre programme, il n’y a rien à faire de particulier :

  • Lancer JConsole (présent dans votre installation du jdk) pendant l’exécution de votre programme
  • sélectionner le process java correspondant à votre programme
  • allez dans l’onglet MBeans
  • parcourez l’arborescence pour trouver votre MBean (ici, vous devriez trouver poolTargetSource sous com.mycompany / MyBusinessObject)
  • sélectionner le noeud attributes de ce MBean
  • vous voila avec une lecture de la valeur courante du nombre d’objet utilisé (ActiveCount) et du nombre de waiter (NumWaitingCallers)
  • un clic sur “refresh” vous permettant de rafraichir ces valeurs

Si vous n’êtes pas en jdk6, activer l’exposition jmx en local s’effectue via l’ajout de -Dcom.sun.management.jmxremote sur la ligne de commande.

Si vous voulez exposer les mbeans à distance, vous pouvez commencer par vous reporter ici.
Les serveurs Java EE exposent souvent leur propre serveur JMX, reportez vous à leur documentation dans ce cas. (il est possible qu’il faille aussi modifier la configuration Spring pour exposer le mbean dans le bon serveur JMX)

Une petite remarque pour terminer :
La documentation de Spring explique comment utiliser un advisor pour injecter dans le proxy les méthodes de l’interface PoolConfig exposant en particulier la méthode getActiveCount(). Je déconseille de suivre cette approche avec Spring AOP, car la récupération des métriques devient inutilisables dès que le pool est saturé, comme cela est décrit dans ce defect : SPR-7740

Author image
Fort de plus de 20 ans d'expérience autour de l'écosystème JavaEE, Fabien accompagne ses clients dans la conception et le cadrage de leurs projets.