Regrouper les ressources à l'aide du framework de pool commun d'Apache

Le regroupement des ressources (également appelé regroupement d'objets) entre plusieurs clients est une technique utilisée pour promouvoir la réutilisation des objets et pour réduire la surcharge de création de nouvelles ressources, ce qui se traduit par de meilleures performances et un meilleur débit. Imaginez une application serveur Java robuste qui envoie des centaines de requêtes SQL en ouvrant et en fermant des connexions pour chaque requête SQL. Ou un serveur Web qui traite des centaines de requêtes HTTP, traitant chaque requête en créant un thread distinct. Ou imaginez créer une instance d'analyseur XML pour chaque demande d'analyse d'un document sans réutiliser les instances. Voici quelques-uns des scénarios qui justifient l'optimisation des ressources utilisées.

L'utilisation des ressources peut parfois s'avérer critique pour les applications lourdes. Certains sites Web célèbres ont fermé leurs portes en raison de leur incapacité à gérer de lourdes charges. La plupart des problèmes liés aux charges lourdes peuvent être traités, au niveau macro, à l'aide des fonctionnalités de clustering et d'équilibrage de charge. Des préoccupations subsistent au niveau de l'application en ce qui concerne la création excessive d'objets et la disponibilité de ressources serveur limitées telles que la mémoire, le processeur, les threads et les connexions de base de données, qui pourraient représenter des goulots d'étranglement potentiels et, lorsqu'elles ne sont pas utilisées de manière optimale, entraîner la panne de l'ensemble du serveur.

Dans certaines situations, la politique d'utilisation de la base de données peut imposer une limite au nombre de connexions simultanées. En outre, une application externe peut dicter ou limiter le nombre de connexions ouvertes simultanées. Un exemple typique est un registre de domaine (comme Verisign) qui limite le nombre de connexions socket actives disponibles pour les bureaux d'enregistrement (comme BulkRegister). La mise en commun des ressources s'est avérée être l'une des meilleures options pour gérer ces types de problèmes et, dans une certaine mesure, contribue également à maintenir les niveaux de service requis pour les applications d'entreprise.

La plupart des fournisseurs de serveurs d'applications J2EE proposent la mise en commun des ressources dans le cadre de leurs conteneurs Web et EJB (Enterprise JavaBean). Pour les connexions à la base de données, le fournisseur du serveur fournit généralement une implémentation de l' DataSourceinterface, qui fonctionne en conjonction avec l' ConnectionPoolDataSourceimplémentation du pilote JDBC (Java Database Connectivity) . L' ConnectionPoolDataSourceimplémentation sert de fabrique de connexions de gestionnaire de ressources pour les java.sql.Connectionobjets regroupés . De même, les instances EJB de beans session sans état, de beans gérés par message et de beans entité sont regroupées dans des conteneurs EJB pour un débit et des performances plus élevés. Les instances d'analyseur XML sont également des candidats au pool, car la création d'instances d'analyseur consomme une grande partie des ressources d'un système.

Une implémentation réussie de mise en commun de ressources open source est le DBCP du framework Commons Pool, un composant de regroupement de connexions de base de données d'Apace Software Foundation qui est largement utilisé dans les applications d'entreprise de production. Dans cet article, je discute brièvement des éléments internes du framework Commons Pool, puis je l'utilise pour implémenter un pool de threads.

Voyons d'abord ce que fournit le cadre.

Cadre de la piscine Commons

Le framework Commons Pool offre une implémentation basique et robuste pour la mise en commun d'objets arbitraires. Plusieurs implémentations sont fournies, mais pour les besoins de cet article, nous utilisons l'implémentation la plus générique, le GenericObjectPool. Il utilise a CursorableLinkedList, qui est une implémentation à double liste liée (qui fait partie des Jakarta Commons Collections), comme structure de données sous-jacente pour contenir les objets mis en commun.

De plus, le cadre fournit un ensemble d'interfaces qui fournissent des méthodes de cycle de vie et des méthodes d'assistance pour la gestion, la surveillance et l'extension du pool.

L'interface org.apache.commons.PoolableObjectFactorydéfinit les méthodes de cycle de vie suivantes, qui s'avèrent essentielles pour l'implémentation d'un composant de pooling:

 // Creates an instance that can be returned by the pool public Object makeObject() {} // Destroys an instance no longer needed by the pool public void destroyObject(Object obj) {} // Validate the object before using it public boolean validateObject(Object obj) {} // Initialize an instance to be returned by the pool public void activateObject(Object obj) {} // Uninitialize an instance to be returned to the pool public void passivateObject(Object obj) {}

Comme vous pouvez le constater par les signatures de méthode, cette interface traite principalement des éléments suivants:

  • makeObject(): Implémenter la création d'objet
  • destroyObject(): Implémenter la destruction d'objets
  • validateObject(): Valider l'objet avant son utilisation
  • activateObject(): Implémenter le code d'initialisation de l'objet
  • passivateObject(): Implémenter le code de désinitialisation de l'objet

Une autre interface principale - org.apache.commons.ObjectPooldéfinit les méthodes suivantes pour gérer et surveiller le pool:

 // Obtain an instance from my pool Object borrowObject() throws Exception; // Return an instance to my pool void returnObject(Object obj) throws Exception; // Invalidates an object from the pool void invalidateObject(Object obj) throws Exception; // Used for pre-loading a pool with idle objects void addObject() throws Exception; // Return the number of idle instances int getNumIdle() throws UnsupportedOperationException; // Return the number of active instances int getNumActive() throws UnsupportedOperationException; // Clears the idle objects void clear() throws Exception, UnsupportedOperationException; // Close the pool void close() throws Exception; //Set the ObjectFactory to be used for creating instances void setFactory(PoolableObjectFactory factory) throws IllegalStateException, UnsupportedOperationException;

L' ObjectPoolimplémentation de l'interface prend a PoolableObjectFactorycomme argument dans ses constructeurs, déléguant ainsi la création d'objet à ses sous-classes. Je ne parle pas beaucoup des modèles de conception ici car ce n'est pas notre objectif. Pour les lecteurs intéressés par les diagrammes de classes UML, veuillez consulter Ressources.

Comme mentionné ci-dessus, la classe org.apache.commons.GenericObjectPooln'est qu'une implémentation de l' org.apache.commons.ObjectPoolinterface. Le framework fournit également des implémentations pour les pools d'objets à clé, en utilisant les interfaces org.apache.commons.KeyedObjectPoolFactoryet org.apache.commons.KeyedObjectPool, où l'on peut associer un pool à une clé (comme dans HashMap) et ainsi gérer plusieurs pools.

La clé d'une stratégie de mise en commun réussie dépend de la façon dont nous configurons le pool. Les pools mal configurés peuvent être des porcs de ressources, si les paramètres de configuration ne sont pas correctement réglés. Regardons quelques paramètres importants et leur objectif.

Détails de configuration

Le pool peut être configuré à l'aide de la GenericObjectPool.Configclasse, qui est une classe interne statique. Sinon, nous pourrions simplement utiliser les GenericObjectPoolméthodes setter de s pour définir les valeurs.

La liste suivante détaille certains des paramètres de configuration disponibles pour l' GenericObjectPoolimplémentation:

  • maxIdle: Nombre maximal d'instances en veille dans le pool, sans que des objets supplémentaires ne soient libérés.
  • minIdle: Le nombre minimum d'instances en veille dans le pool, sans création d'objets supplémentaires.
  • maxActive: Le nombre maximum d'instances actives dans le pool.
  • timeBetweenEvictionRunsMillis: Le nombre de millisecondes à mettre en veille entre les exécutions du thread expulseur d'objet inactif. Lorsqu'elle est négative, aucun thread expulseur d'objet inactif ne s'exécutera. Utilisez ce paramètre uniquement lorsque vous souhaitez que le thread expulseur s'exécute.
  • minEvictableIdleTimeMillis: Durée minimale pendant laquelle un objet, s'il est actif, peut rester inactif dans le pool avant d'être éligible à l'expulsion par l'expulseur d'objet inactif. Si une valeur négative est fournie, aucun objet n'est expulsé en raison du seul temps d'inactivité.
  • testOnBorrow: Lorsque "true", les objets sont validés. Si l'objet échoue à la validation, il sera supprimé du pool et le pool tentera d'en emprunter un autre.

Des valeurs optimales doivent être fournies pour les paramètres ci-dessus afin d'obtenir des performances et un débit maximum. Étant donné que le modèle d'utilisation varie d'une application à l'autre, réglez le pool avec différentes combinaisons de paramètres pour arriver à la solution optimale.

Pour en savoir plus sur le pool et ses composants internes, implémentons un pool de threads.

Exigences proposées pour le pool de threads

Supposons qu'on nous demande de concevoir et d'implémenter un composant de pool de threads pour qu'un planificateur de travaux déclenche des travaux à des horaires spécifiés et signale l'achèvement et, éventuellement, le résultat de l'exécution. Dans un tel scénario, l'objectif de notre pool de threads est de regrouper un nombre de threads prérequis et d'exécuter les travaux planifiés dans des threads indépendants. Les exigences sont résumées comme suit:

  • Le thread doit pouvoir invoquer n'importe quelle méthode de classe arbitraire (le travail planifié)
  • Le thread doit pouvoir renvoyer le résultat d'une exécution
  • Le fil doit être en mesure de signaler l'achèvement d'une tâche

La première exigence fournit la possibilité d'une implémentation faiblement couplée car elle ne nous oblige pas à implémenter une interface comme Runnable. Cela facilite également l'intégration. Nous pouvons implémenter notre première exigence en fournissant au fil les informations suivantes:

  • Le nom de la classe
  • Le nom de la méthode à invoquer
  • Les paramètres à passer à la méthode
  • Les types de paramètres des paramètres passés

La deuxième exigence permet à un client utilisant le thread de recevoir le résultat de l'exécution. Une implémentation simple consisterait à stocker le résultat de l'exécution et à fournir une méthode d'accès comme getResult().

La troisième exigence est quelque peu liée à la deuxième exigence. Signaler l'achèvement d'une tâche peut également signifier que le client attend d'obtenir le résultat de l'exécution. Pour gérer cette capacité, nous pouvons fournir une forme de mécanisme de rappel. Le mécanisme de rappel le plus simple peut être implémenté en utilisant les java.lang.Objects wait()et la notify()sémantique. Alternativement, nous pourrions utiliser le modèle Observer , mais pour l'instant, gardons les choses simples. Vous pourriez être tenté d'utiliser la méthode de la java.lang.Threadclasse join(), mais cela ne fonctionnera pas car le thread poolé ne termine jamais sa run()méthode et continue de fonctionner aussi longtemps que le pool en a besoin.

Maintenant que nos exigences sont prêtes et que nous avons une idée approximative de la façon d'implémenter le pool de threads, il est temps de faire du vrai codage.

À ce stade, notre diagramme de classes UML de la conception proposée ressemble à la figure ci-dessous.

Implémentation du pool de threads

L'objet thread que nous allons regrouper est en fait un wrapper autour de l'objet thread. Appelons le wrapper la WorkerThreadclasse, qui étend la java.lang.Threadclasse. Avant de pouvoir commencer à coder WorkerThread, nous devons implémenter les exigences du cadre. Comme nous l'avons vu précédemment, nous devons implémenter le PoolableObjectFactory, qui agit comme une usine, pour créer nos poolables WorkerThread. Une fois l'usine prête, nous implémentons le ThreadPoolen étendant le GenericObjectPool. Ensuite, nous terminons notre WorkerThread.

Implémentation de l'interface PoolableObjectFactory

Nous commençons par l' PoolableObjectFactoryinterface et essayons d'implémenter les méthodes de cycle de vie nécessaires pour notre pool de threads. Nous écrivons la classe d'usine ThreadObjectFactorycomme suit:

public class ThreadObjectFactory implements PoolableObjectFactory{

public Object makeObject() { return new WorkerThread(); } public void destroyObject(Object obj) { if (obj instanceof WorkerThread) { WorkerThread rt = (WorkerThread) obj; rt.setStopped(true);//Make the running thread stop } } public boolean validateObject(Object obj) { if (obj instanceof WorkerThread) { WorkerThread rt = (WorkerThread) obj; if (rt.isRunning()) { if (rt.getThreadGroup() == null) { return false; } return true; } } return true; } public void activateObject(Object obj) { log.debug(" activateObject..."); }

public void passivateObject(Object obj) { log.debug(" passivateObject..." + obj); if (obj instanceof WorkerThread) { WorkerThread wt = (WorkerThread) obj; wt.setResult(null); //Clean up the result of the execution } } }

Passons en revue chaque méthode en détail:

La méthode makeObject()crée l' WorkerThreadobjet. Pour chaque demande, le pool est vérifié pour voir si un nouvel objet doit être créé ou un objet existant doit être réutilisé. Par exemple, si une demande particulière est la première demande et que le pool est vide, l' ObjectPoolimplémentation appelle makeObject()et ajoute le WorkerThreadau pool.

La méthode destroyObject()supprime l' WorkerThreadobjet du pool en définissant un indicateur booléen et en arrêtant ainsi le thread en cours d'exécution. Nous reviendrons sur cette pièce plus tard, mais notons que nous prenons maintenant le contrôle de la façon dont nos objets sont détruits.