I. Introduction

« Mon application ne vas pas assez vite! »

« J'ai besoin de supporter plus d'utilisateurs simultanés ! »

« Ma base de donnée n'est pas suffisamment rapide ! »

« Dois-je changer de serveur d'application ? »

Prenez un groupe de développeurs, réunissez-les pour discuter de votre application. La meilleure façon d'attirer l'attention de vos collègues et les faire venir a votre réunion ? Incluez l'un des deux mots magiques suivant : Sécurité et Performance.

Quelle que soit l'application, la performance est toujours une question primordiale. Plus exactement, la question est de savoir comment obtenir un niveau de performance satisfaisant en gardant les coûts minimums.

Lorsque quelqu'un énonce un problème de performance pour son application, une attitude courante est de blâmer son système d'exploitation, son serveur d'application, ou sa base de donnée. Pourquoi ? Parce qu'il est difficile de se blâmer soi-même. Pourtant, un grand nombre de problèmes de performance est dû au développeur ou à l'architecte.

Dans ce document, je présente certains patterns, indispensables au développeur J2EE. Pour nous aider dans cette tâche, supposons que nous essayions de construire un forum, comme celui de www.developpez.com.

I-A. Le modèle de données

Nous allons utiliser une base de donnée (BD) pour stocker les messages postés par les gentils utilisateurs. Dans un monde idéal :

  • Le design de votre application devrait être construit avant celui de votre BD. Cependant, d'expérience, c'est peu souvent le cas. Il faut donc pouvoir s'adapter.
  • Vous avez un administrateur de BD (DBA) à votre disposition. Ne supposez pas parce que vous connaissez SQL que vous savez administrer une base de donnée ! Si vous dirigez une équipe, j'insiste sur la nécessité d'engager un DBA au moins à titre temporaire, et ne lésinez pas sur les séminaires d'une semaine proposés par les vendeurs de BD. Ceci étant dit, je ne m'étendrai pas sur l'optimisation de la BD. Ma vue est celle du développeur qui connaît SQL.

Notre forum sera amplement plus simple et moins sophistiqué que celui de www.developpez.com, il ne contient que deux types d'objets :

  • POST. Un Post est un message.
  • FORUM. Un Forum est un regroupement de Posts. Nous aurons plusieurs Forums, par exemple l'un qui s'appelle JAVA et l'autre J2EE.
Image non disponible

Le modèle présenté est une façon simple de représenter notre système. Nous avons une relation one-to-many entre POST et FORUM, ce qui signifie qu'un FORUM peut contenir plusieurs POSTs. Un POST à un POST parent, c'est notre façon de représenter un fil (suite de POSTs).

I-B. Accéder à la base de donnée

L'un des énormes avantages d'utiliser J2EE est la possibilité d'acheter une partie de son système plutôt qu'avoir à le construire soi-même. Il existe deux moyens de réaliser la persistance en utilisant les EJB.

  • Bean Managed Persistence : Le développeur doit coder la persistance
  • Container Managed Persistence : Le container gère la persistance

La seconde solution (CMP) offre de nombreux avantages, en particulier portabilité, rapidité de développement, performance et robustesse. Préférez-les autant que possible.

I-C. Local et Remote Interfaces

Il existe deux moyens d'accéder à un EJB, en utilisant :

  • Son interface Remote, ou distante
  • Son interface Locale !

Deux points importants à saisir :

  • L'utilisation de l'interface distante implique la sérialisation et RMI, donc le passage d 'objets au travers du réseau. L'utilisation de l'interface locale est directe, donc beaucoup plus rapide. Limiter le nombre d'appels distants est un facteur important pour la performance de votre système.
  • Lors d'un appel distant, les paramètres sont passés par valeur, la référence n'ayant aucun sens dans ce cas. Lors d'un appel local, les paramètres sont passés par référence. Des comportements différents existent donc entre les deux appels. Gardez ceci en tête.

I-D. Session Façade

Voici sans doute le pattern J2EE le plus utilisé. Le problème est le suivant : Chaque appel distant est coûteux, il faut trouver un moyen de les limiter. Une Session Façade est un session bean qui a accès aux interfaces locales d'autres beans (parce qu'il vit dans la même JVM). Un appel à une méthode d'un Session Façade entraîne généralement plusieurs appels vers un ou plusieurs autres beans.

Supposons par exemple le cas suivant : Un modérateur veut déplacer un fil (série de POSTs) d'un FORUM vers un autre.

Nous avons ces étapes

  1. Trouver le premier POST du fil à déplacer
  2. Trouver tous les POST qui suivent le fil
  3. Déplacer chacun des POSTs

Le modérateur ne veut se soucier que du fait qu'il déplace un fil. Notre Session Façade va posséder la méthode deplaceFile qui prend comme argument l'identifiant d'un POST.

Image non disponible

Le réseau se trouve entre le serveur WEB (Servlet Container, ou SC) et la Façade. Les appels entre Façade et POST CMP sont locaux. Notre opération ne requiert qu'un seul appel distant, quel que soit le nombre de POSTs à déplacer.

De façon générale, c'est un bon exercice de ne pas générer d'interfaces distantes pour ses EJB persistants. En d'autres termes, une façade porte bien son nom, un mur entre application et persistance.

Notre façade pourrait faire tout un tas d'autres choses, comme par exemple vérifier l'autorisation de l'utilisateur qui a instancié la requête. Une façade peut aussi appeler d'autres Session Beans ou Message Driven Beans. La leçon à retenir étant qu'il faut limiter le nombre d'appels distants.

I-E. Value Objects

Le Session Façade Pattern présenté plus haut implique un challenge supplémentaire. Puisque nous n'avons pas de référence directe à notre objet persistent, comment allons-nous lire le contenu d'un objet ? Heureusement, cette limitation à une solution très simple, et qui ajoute même de nombreuses possibilités d'amélioration. Le Value Object n'est autre qu'un objet qui offre une vue figée de notre entité à un moment donné. Voyons un exemple de code :

 
Sélectionnez
import java.io.Serializable;
import java.util.Date;
public class PostVO implements Serializable{
    private Long postID;
    private String posterName;
    private Long parentPostID;
    private Date postDate;
    private String messageContent;
    private Long forumID;
    
    public PostVO(Long postID,
            String posterName,
            Long parentPostID,
            Date postDate,
            String messageContent,
            Long forumID){
        this.postID = postID;
        this.posterName = posterName;
        this.parentPostID = parentPostID;
        this.postDate = postDate;
        this.messageContent = messageContent; 
        this.forumID = forumID;
    }
    
    public Long getPostID(){
        return postID;
    }
    public String getPosterName(){
        return posterName;
    }
    public Long getParentPostID(){
        return parentPostID;
    }
    public Date getPostDate(){
        return postDate;
    }
    public String getMessageContent(){
        return messageContent;
    }
    public Long  getForumID(){
        return forumID;
    }
    public Key getKey(){
        return new PostKey(postID);
    }
}

Le mot clef ici est Serializable. En effet, ce Value Object (VO) est l'objet que nous ferons traverser le réseau. Il est donc nécessaire que l'objet et tous ces membres soient Serializable. Grâce au VO, nous pouvons accéder aux membres de notre entité sans faire d'appels répétés à notre EJB persistant.

Un EJB persistant utilise une clef primaire pour identifier une entité, notre VO va utiliser cette même clef pour s'identifier. Une clef primaire pour un EJB à des règles strictes à suivre, dont :

  • Les variables doivent être déclarée public
  • Hashcode et equals doivent être implémentées pour optimiser les recherches. Dans notre cas, leurs implémentations sont triviales et appellent les méthodes de la classe Long. Plus généralement, hashcode() va être utilisé par le container pour retrouver un EJB dans son cache, et nous l'utiliserons pour notre propre cache.

L'utilisation d'un VO va considérablement soulager notre EJB container et notre BD, mais ce soulagement se fait a un prix : un VO n'est qu'une image figée de votre entité, c'est a dire qu'un client peut se trouver en possession d'un VO dont les valeurs ont déjà expiré. Nous allons voir plus tard comment se débarrasser de cette limitation.

Notre exemple de code est légèrement simplifié, nous verrons plus tard qu'une bonne idée est pour un VO d'implémenter une interface (par exemple nommée VO) qui ne défini aucune méthode ni variable. Nous verrons qu'un VO est un excellent candidat pour être mis en cache, et ainsi démultiplier nos performances. Mais avant ça, il nous reste deux trois points à voir afin de rendre notre application plus solide.

I-F. Service Locator

Le client, quel qu'il soit : GUI, Servlet container (SC) ou autre a besoin d'un moyen de localiser les objets distants, en l'occurrence notre Session Façade. Le service de nommage couramment utilisé avec J2EE est JNDI, la méthode à suivre est simple et supposée connue. Malgré tout, dans le but de rendre notre système flexible et ré-utilisable, nous définissons un Service Locator, dont le seul but est de servir au client des Home interfaces, par exemple en implémentant un pool. Notre service locator est simple et n'implémente qu'une seule méthode : getPostFacade() : PostFacade

Un Service Locator est un bon candidat pour le Singleton Pattern.

De façon générale, dans une application distribuée ou multi-threadée, l'utilisation du Singleton pattern doit se faire avec prudence.
Le piège du pattern Singleton, c'est qu'à l'évidence, il n'est pas inter-jvm. Dans notre cas, ce cas est tout a fait acceptable et même approprié. Chaque client aura son propre Service Locator Singleton.

I-G. Business Delegate

En voilà un joli nom, n'est-ce pas ? Pas de panique, ce pattern n'est autre qu'une adaptation du pattern Wrapper (ou Adapter) bien connu, appliqué au modèle J2EE. Son but est multiple :

  • Se débarrasser de la dépendance du client pour J2EE. J2EE offre les bases nécessaires pour séparer le client du serveur. Complètement. Dès lors, un « import javax.ejb.* » dans une servlet est malvenu. Pourquoi ? Parce que le développeur de votre EJB peut changer certaines signatures de méthodes à tout moment. Cela veut-il dire que chacune de vos servlet doit être mise à jour ? Le Business Delegate va nous permettre de regrouper en un seul endroit tous les appels distants.
  • Transformer les Exceptions génériques en Exceptions propres à votre application
  • Proposer des méthodes spécifiques à votre client
  • Exécuter des portions de code nécessaires à chaque appel distant. Patience, nous irons plus en détail lorsque nous expliquerons notre principe de cache.

Voici un exemple d'implémentation de méthode dans notre Delegate :

 
Sélectionnez
public class Delegate{
    public PostVO findPost(long postID) throws ApplicationException{
        try{
            
            // On va commencer par regarder dans le cache ici
            // nous verrons ca plus loin
            
            PostFacade facade = ServiceLocator.getPostFacade();
            return facade.findByPostID(new Long(postID));
        }catch(javax.ejb.FinderException ex){
            Log.log(severity, ex.getMessage());
            throw new ApplicationException("Post introuvable.");
        }
    }
}

La méthode nous retourne le Value Object (VO) dont nous avons parlé plus haut. Elle s'occupe de chercher le cache, loguer les erreurs et transformer l'exception en quelque chose d'acceptable pour notre client. Une servlet par exemple peut maintenant appeler cette fonction et ne jamais savoir tout ce qui se passe derrière le rideau.

Ce type d'architecture permet d'efficacement définir les rôles dans une équipe, un développeur pour les EJB, un pour les servlets. Utilisez ce pattern sans modération.

I-H. External SC VO Cache

À ce stade du design, nous avons un système solide, portable, robuste et flexible. Que demande le peuple ? La performance. Ou comment donner des ailes à cette belle application. La solution n'est pas nouvelle : utiliser un cache. Cela repose sur un fondement simple : la mémoire ne coûte pas cher. Ajouter une barrette de mémoire à votre machine peut diviser par 10 ou 100 le nombre d'appels via JDBC, suivant le type d'application, et d'expérience, une proportion non négligeable des problèmes de performance est due aux très chers appels à la BD.

Prenez un cas typique d'application où à chaque visite sur votre site Web, un utilisateur engendre 10 appels à la base de données. Au fur et à mesure que votre site se popularise, lorsque le nombre d'utilisateur est multiplié par 10, le nombre d'appel à la BD est multiplié par 100. À ce rythme, ajouter une machine à votre île de BD n'est qu'une solution très temporaire, même s'il est vrai que la BD elle-même a son propre système de cache.

Notre solution consiste à installer un système de cache au niveau de notre Servlet Container (SC). Maintenant, lorsque le nombre d'utilisateur est multiplié par 2, ajoutez un SC. Les SC sont peu chers, beaucoup moins que les BD ou les EJB containers. Nous n'utiliserons nos EJB que si le résultat attendu n'est pas dans le cache. Il est possible que votre vendeur propose déjà ce service, auquel cas, il ne vous est pas nécessaire de l'implémenter.

Que contient le cache ? Le cache peut être aussi simple qu'une Hashtable dont la clef est la clef du VO (Key). Le cache implémente le pattern Singleton (voir remarque plus haut).

Nous avons déjà vu brièvement que chaque Value Object devrait implémenter l'interface VO, et chaque Key (comme PostKey) devrait implémenter l'interface Key. Cela va nous permettre de définir la méthode suivante dans notre cache :

 
Sélectionnez
public VO getObject(Key key) ;

Une fois cette méthode implémentée, c'est le moment de la référencer dans notre Business Delegate.

Réécrivons notre Business Delegate de la sorte :

 
Sélectionnez
pblic class Delegate{
    public PostVO findPost(long postID) throws ApplicationException{
        try{
            
            VOCache cache = VOCache.getInstance();
            
            PostVO vo = cacahe.getObject(new PostKey(postID));
            if(vo == null){ // l'objet n'est pas en cache
                PostFacade facade = ServiceLocator.getPostFacade();
                vo = facade.findByPostID(new Long(postID));
                cache.put(new postKey(postID), vo);
            }
            
            return vo;
        }catch(javax.ejb.FinderException ex){
            Log.log(severity, ex.getMessage());
            throw new ApplicationException("Post introuvable.");
        }
    }
}

Et voila, il ne nous reste plus que deux détails a régler

  1. Limiter la taille de notre cache.
  2. S'assurer que le cache est toujours a jour.

Limiter la taille de notre cache consiste à utiliser une version améliorée de Hashtable, qui se débarrasse des objets qui n'ont pas servi depuis longtemps, au profit de nouveaux objets. La technique ici est largement variable, et dépend grandement de votre application. Par exemple, faites le ménage à chaque fois que la limite supérieure de taille est atteinte. Ou bien implémenter un système de compteur pour se débarrasser des objets les moins usités.

Plus délicat est le problème de s'assurer qu'il n'existe pas d'objet invalide dans notre cache. Et voici le challenge : notre cache est sur un SC, distant de notre EJB container. L'EJB n'a aucun moyen d'appeler une méthode sur notre SC. Qu'à cela ne tienne. Nous allons utiliser JMS pour réduire ce problème a néant. Définissons un Topic dans notre EJB containeur. L'EJB persistent va jouer le rôle du Publisher, et placer des messages sur ce Topic, à chaque fois qu'il réalise un update (voir spec. EJB).

Notre cache, elle, va s'inscrire comme Listener de ce Topic, et recevoir des messages d'invalidation, qui, vous l'avez compris, contiennent la clef de l 'objet à invalider. Le cache définit la méthode suivante :Ø

 
Sélectionnez
invalidate(Key key) :void

qui ne fait rien d'autre que d'enlever l'objet de notre cache.

Ce système d'invalidation ne vous protège pas des accès directs à la BD, via un client SQL par exemple, ou toute autre application. La plupart des BD offrent l'accès exclusif par une application, c'est une solution. Ou, si plusieurs applications accèdent la même BD en même temps, il est impératif de construire une couche supplémentaire (par un Observer pattern par exemple), ou désactiver tout système de cache, celui de votre SC, et celui de votre EJB container. Ce problème est toujours vrai si vous utilisez un quelconque cache.

Ce système de cache à un coût. Celui d'envoyer des messages JMS à chaque mise à jour. Ce coût, comme souvent, peut devenir insignifiant si le nombre de requêtes excède le nombre de mises à jour.
Il est possible d'étendre plus loin notre système de cache, et aller jusqu'a cacher des collections de VO, par exemple des collections de POST. Pourquoi ne pas mettre en cache la collection de 10 ou 15 POSTs que nous voulons afficher par défaut sur la première page de notre forum. Cette page est typiquement appelée de nombreuses fois, c'est autant de fois que nous allègerons notre BD, pour lui laisser les choses qu'elle seule sait si bien faire, comme un moteur de recherche par exemple.

I-I. L'architecture

Image non disponible

II. Références