JMS Weblogic : concept et performance

OracleWebLogicLe constat est évident : comprendre les principes de l'architecture JMS offerte par Oracle ne se fait pas sans mal.  Choix d'architectures JMS multiples, orientées robustesse et scalabilité.
Un design complet, complexe également ... et qui peut s'avérer problématique si vous ne mettez pas en avant dès le départ vos contraintes de performance : haute disponibilité, QoS, volumétrie, durée de traitement, contexte transactionnel, SLA, TTL ...

J'espère pouvoir vous éclairer avec ce post, en vous proposant une synthèse balayant les principes les plus importants à retenir, avec comme toujours un vernis performance qui agrémente le tout !

Weblogic JMS design :

Présentation1

Machine :

  • représente le serveur physique

Managed server :

  • l'instance weblogic managée
  • rattachée à un cluster, ou défini comme une instance à part.
  • contient un ensemble de services Messaging (JMS) mais aussi JDBC, JTA, Work Manager, Jolt ...

jms1

 

 Persistence store :

  • filestore (tlog) contenant l'ensemble des messages
  • chaque instance managée a son (ou ses) propre(s) file store.
  • 1 fileStore = 1 seul JMS Server

jms2

Autre moyen de persistence : le Jdbc Store.

JMS Server :

  • c'est le serveur JMS dédié à 1 seule instance managée (et donc 1 seul PersistenceStore).
  • il regroupe un ensemble de modules JMS (et donc un ensemble de Queues)
  • on lui définit des tailles limites de messages (messages size), des quotas ainsi que des paramètres de régulation de flux de messages ...

jms3

JMS Modules :

  • il est propre à une instance Weblogic ou à un cluster Weblogic
  • il permet de regrouper un ensemble de ressources (distributed)Queues, (distributed)Topics et ConnectionFactory ... (définies comme des JMS Ressources)
  • il est défini avec un simple fichier descripteur (ex : jms/AMM-SERV1-JMSModule-jms.xml)

jms4

JMS Modules - Subdeployement :

  • il se définit à partir d'un Jms Server, ou d'un Jms Module
  • il permet de lier 1 ou plusieurs Jms Modules avec 1 ou plusieurs Jms Server (ex : cible=JMSServer1, ressources =Queues et Topics du JMSModule)

jms5

JMSRessources : Queues - Topics - ConnectionFactory :

  • Queue = une file JMS accessible par un JNDI Name, des quotas (taille, nombre de messages, Thresholds, TTL, delivery mode ...), tracée dans un logging mode. Queue = un chanel de (potentiellement) plusieurs producteurs, mais ne peut être consommé par seulement 1 consommateur
  • Distributed Queue = un regroupement de Queues déclarées.On définit une politique de loadbalancing pour la distribution des messages (roundrobin ou random)

jms6

JMS permet d’envoyer un message d’un producteur à un consommateur de deux façons différentes :
  • En point-to-point :  le producteur envoie un message sur une queue destination au consommateur indépendamment du type de message envoyé.
  •  En publish-subscribe : le publisher (producteur) publie un message. Plusieurs subscribers actifs (temps de durée de vie de leur abonnement) en abonnement sur une topic et sur certains types de messages (event)

Les JMS providers :

Envoi en mode DIRECT

L’application interagit directement avec la destination distante via le code applicatif (client JMS Message Driven Bean)

Solution rapide, mais très peu robuste

 

Envoi en mode INDIRECT

Système assurant la persistance locale et l’envoi du message asynchrone vers la destination distante. Il assure également les options de rejeux et de passivation garantissant l’envoi du message malgré les erreurs et interruption de la destination distante.Utilisation d' un agent intermédiaire (message forwarding agent) : c'est le principe du Bridge et du Store-And-Forward (SAF).

Solution robuste, mais moins rapide

 

Voici 2 explications claires listant les avantages et inconvénients de ces 2 services : Bridge et Store-And-Forward (SAF).

 

Les Messages Driven Bean :

Un bean MDB est un EJB qui agit comme un consommateur de messages dans le système de messagerie JMS WebLogic

Le MDB reçoit des messages à partir d'une Queue ou d'un Topic JMS , et exécute la logique métier en fonction du contenu du message.

Exemple d'intégration d'un MDB dans un EAR :

jms7

Settings de l'EJB BPLMDBMajData :

jms8

Monitoring des Currents Bean :

jms9

Exemple d'implémentation du MessageLister (MDB)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@MessageDriven(mappedName = "Service_MAJ_DATA")
public class ServiceMDBMajData implements MessageListener
{
    /**
     * Logger for this class
     */
    private static final Logger logger = Logger.getLogger(ServiceMDBMajData.class);
 
    private static final long serialVersionUID = 1L;
 
    /**
     * Session bean ServiceMajData
     */
    @EJB()
    public ServiceMajData majData;
 
    /**
     * Constructeur
     */
    public ServiceMDBMajData() {}
 
    /**
     * Methode invoqué a la reception d'un message sur la file JMS
     * 
     * @param messageIn
     *                Message recuperé dans la file JMS
     */
    public void onMessage(Message messageIn)
    {
        if (logger.isDebugEnabled())
            logger.debug("onMessage(Message) - start");
 
        try
        {
            majData.majData(messageIn);
        } catch (Exception e)
        {
            logger.error("onMessage(Message)", e);
            e.printStackTrace();
        }
 
        if (logger.isDebugEnabled())
            logger.debug("onMessage(Message) - end");
    }
}

L'implémentation de la logique métier se trouve dans l'EJB [ServiceMajData] appelé par le MDB :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Local(ServiceMajData.class)
@Stateless(mappedName = "ServiceMajData")
@TransactionManagement(TransactionManagementType.BEAN)
public class ServiceMajDataBean implements ServiceMajData
{
    /**
     * Logger for this class
     */
    private static final Logger logger = Logger.getLogger(ServiceMajDataBean.class);
 
    @Resource
    UserTransaction utx;
 
    @EJB
    Metier1EJBData metier1EJBData;
 
     public void majData(Message messageIn) throws Exception
    {
        if (logger.isDebugEnabled())
            logger.debug("majParam(TextMessage) - start");
        try
        {
            utx.begin();
            TextMessage requestMessage = (TextMessage) messageIn;
            MajDocument majDataDoc = MajDocument.Factory.parse(requestMessage.getText());
            MajType majType = majDataDoc.getMajService();
            if (logger.isDebugEnabled())
                logger.debug("MajDocument reçu = " + majDataDoc.toString());
 
            if (majType instanceof Condition1)
            {
                AckAlarme ackAlarme = (Condition1) majType;
                metier1EJBData.traiterMsg(Condition1);
            }
            if (majType instanceof ChgtPdkCom)
            {
                ChgtPdkCom chgtPdkCom = (ChgtPdkCom) majType;
                metier1EJBData.traiterMsg(chgtPdkCom);
            }
            utx.commit();
        } catch (Exception e)
        {
            try
            {
                if (utx != null
                        && ((utx.getStatus() == Status.STATUS_ACTIVE) || (utx.getStatus() == Status.STATUS_MARKED_ROLLBACK)))
                    utx.rollback();
            } catch (Exception ex)
            {
                ex.printStackTrace();
                logger.error("majData(TextMessage)", ex);
            }
            throw e;
        }
        if (logger.isDebugEnabled())
            logger.debug("majData(TextMessage) - end");
    }
}

Le producteur

exemple de code java de push de message dans une Queue par le producteur :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
    /**
     * Envoi d'un objet xml sur une file JMS.
     * 
     * @param urlProvider
     *            URL du point distant.
     * @param jmsFactory
     *            Factory de la file a atteindre.
     * @param jmsQueue
     *            File sur laquelle envoyer.
     * @param xmlDocument
     *            Document a envoyer.
     * @return true si envoi reussie, false sinon.
     */
    private boolean sendJMSQueue(String urlProvider, String jmsFactory, String jmsQueue, XmlObject xmlDocument)
    {
        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug("sendJMSQueue - start");
        }
 
        boolean estEnvoye = false;
        try
        {
            QueueConnection qcon = null;
            QueueSession qsession = null;
            QueueSender qsender = null;
            Context ctx = null;
            try
            {
                Properties props = new Properties();
                props.put(Context.INITIAL_CONTEXT_FACTORY, WEBLOGIC_WLFACT);
                props.put(Context.PROVIDER_URL, mapJndi.get(urlProvider));
                ctx = new InitialContext(props);
                QueueConnectionFactory qconFactory = (QueueConnectionFactory) ctx.lookup(mapJndi.get(jmsFactory));
                qcon = qconFactory.createQueueConnection();
                qsession = qcon.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
                String fileName = mapJndi.get(jmsQueue);
                Queue queue = (Queue) ctx.lookup(fileName);
                qsender = qsession.createSender(queue);
 
                TextMessage responseMsg = qsession.createTextMessage();
                responseMsg.setText(xmlDocument.xmlText(xmlOpt));
 
                qcon.start();
                qsender.send(responseMsg);
                estEnvoye = true;
                if (LOGGER.isInfoEnabled())
                {
                    LOGGER.info(new StringBuilder("sendJMSQueue - Message envoye sur ").append(fileName));
                }
            } finally
            {
                if (qsender != null)
                {
                    try
                    {
                        qsender.close();
                    } catch (Exception e)
                    {
                        LOGGER.warn(new StringBuilder("sendJMSQueue - Erreur de fermeture du sender ! ").append(e.getMessage()));
                    }
                }
                if (qsession != null)
                {
                    try
                    {
                        qsession.close();
                    } catch (Exception e)
                    {
                        LOGGER.warn(new StringBuilder("sendJMSQueue - Erreur de fermeture de la session ! ").append(e.getMessage()));
                    }
                }
                if (qcon != null)
                {
                    try
                    {
                        qcon.close();
                    } catch (Exception e)
                    {
                        LOGGER.warn(new StringBuilder("sendJMSQueue - Erreur de fermeture de la connexion ! ").append(e.getMessage()));
                    }
                }
            }
 
        } catch (Exception e)
        {
            LOGGER.error(new StringBuilder("sendJMSQueue - Erreur d'envoi : ").append(e.getMessage()), e);
        }
 
        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug("sendJMSQueue - end");
        }
        return estEnvoye;
    }

exemple de code java de push de message dans une Topic par le producteur :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
  /**
     * Envoi d'un objet xml sur un topic JMS.
     * 
     * @param jmsFactory
     *            Factory du topic a atteindre.
     * @param jmsTopic
     *            Topic sur lequel envoyer.
     * @param xmlDocument
     *            Document a envoyer.
     * @return true si envoi reussie, false sinon.
     */
    private boolean sendJMSTopic(String jmsFactory, String jmsTopic, XmlObject xmlDocument)
    {
        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug("sendJMSTopic - start");
        }
 
        boolean estEnvoye = false;
        try
        {
            TopicConnection topicCnx = null;
            TopicSession topicSession = null;
            TopicPublisher topicPublisher = null;
 
            try
            {
                Context ctx = new InitialContext();
                TopicConnectionFactory topicCnxFact = (TopicConnectionFactory) ctx.lookup(mapJndi.get(jmsFactory));
                topicCnx = topicCnxFact.createTopicConnection();
                topicSession = topicCnx.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);
                String topicName = mapJndi.get(jmsTopic);
                Topic topic = (Topic) ctx.lookup(topicName);
                topicPublisher = topicSession.createPublisher(topic);
 
                TextMessage responseMsg = topicSession.createTextMessage();
                responseMsg.setText(xmlDocument.xmlText(xmlOpt));
 
                topicCnx.start();
                topicPublisher.send(responseMsg);
                estEnvoye = true;
                if (LOGGER.isInfoEnabled())
                {
                    LOGGER.info(new StringBuilder("sendJMSTopic - Message envoye sur ").append(topicName));
                }
            } finally
            {
                if (topicPublisher != null)
                {
                    try
                    {
                        topicPublisher.close();
                    } catch (Exception e)
                    {
                        LOGGER.warn(new StringBuilder("sendJMSTopic - Erreur de fermeture du sender ! ").append(e.getMessage()));
                    }
                }
                if (topicSession != null)
                {
                    try
                    {
                        topicSession.close();
                    } catch (Exception e)
                    {
                        LOGGER.warn(new StringBuilder("sendJMSTopic - Erreur de fermeture de la session ! ").append(e.getMessage()));
                    }
                }
                if (topicCnx != null)
                {
                    try
                    {
                        topicCnx.close();
                    } catch (Exception e)
                    {
                        LOGGER.warn(new StringBuilder("sendJMSTopic - Erreur de fermeture de la connexion ! ").append(e.getMessage()));
                    }
                }
            }
        } catch (Exception e)
        {
            LOGGER.error(new StringBuilder("sendJMSTopic - Erreur d'envoi : ").append(e.getMessage()), e);
        }
        if (LOGGER.isDebugEnabled())
        {
            LOGGER.debug("sendJMSTopic - end");
        }
        return estEnvoye;
    }

 

Recommandations :

  • Utiliser des queues pour les envois en point-to-point et les topics en mode publish and subscribe.
  • Si vous avez besoin d'implémenter des queues/topics dans un contexte transactionnel JTA, vous avez besoin d'utiliser une XA Connection Factory. Vérifiez bien que votre timeout est suffisant long pour ne pas lever une RollBackTransaction.
  • Privilégier fortement l'utilisation des Distributed Queues et Distributed Topics pour faciliter la scalabilité de votre cluster Weblogic.
  • Bien penser à adopter une convention de nommage pour chacune des références JNDI interne et externe.
  • Tuner vos Weblogic JMS instances en définissant des quotas, thresholds et en limitant les tailles des serveurs en fonction de la charge attendue par votre applicatif.

 

Tuning JMS:

Il faut savoir que chaque message consomme de la mémoire, même si il passe en paging out (les header de message sont maintenus dans la JVM). Un grand nombre de messages associé à une mauvaise configuration du système JMS peut rapidement provoquer un OutOfMemory de la JVM.

Définir un Quota : cela permettra de refuser aux producteurs de poster des nouveaux messages lorsque le quota est atteint, jouant ainsi le rôle de garde-fou sur l'utilisation de la Heap.
Les quotas peuvent être définis au niveau des modules JMS, mais il est préférable de les définir directement au niveau du serveur JMS.

Lorsqu'un quota est atteint, la « connection factory » épuisera le "transaction Timeout"  avant de lever une "ressource allocation exception". Plusieurs options de quota sont possibles, comme la priorité aux messages plus petits ...

Définir un Paging : cela consiste à ne conserver en mémoire que l'entête des messages JMS. Le corps est sérialisé dans un répertoire appelé Paging Directory. Le messageBufferSize permet de déterminer à partir de quelle taille occupée de la Heap, le paging se déclenche.

Contrôle de débit : il permet de ralentir la production de message lorsqu'un seuil est atteint, on dit alors d'un serveur JMS ou d'une destination qu'il devient « armé ». Ce seuil peut être aussi bien exprimé en terme de nombres de messages ou d'octets occupés par les messages.

La configuration du Flow Control se fait dans la configuration de la Connection Factory. Les 2 paramètres intéressants sont :

  • Flow Maximum : le nombre maximum de messages par seconde qui peuvent être posté par un producteur dans une condition de seuil.
  • Flow Minimum :le débit minimal qui pourra être demandé par le serveur JMS à un producteur.

Contrôle des seuils :

  • Le seuil haut : c'est le niveau à partir duquel le serveur JMS est « armé »
  • Le seuil bas : c'est le niveau en dessous duquel les producteurs peuvent augmenter le débit d'envoi des messages.

Présentation3

 

Voici 2 excellents articles qui complètent la partie tuning notamment avec la notion de One-Way Sends

http://developsimpler.blogspot.fr/2011/11/jms-performance-tuning-series-part-1.html
http://developsimpler.blogspot.fr/2011/12/weblogic-jms-performance-tuning.html

Autres références :

http://docs.oracle.com/cd/E14571_01/web.1111/e13814/jmstuning.htm
http://docs.oracle.com/cd/E12840_01/wls/docs103/jms/fund.html

 

5 réponses à to “JMS Weblogic : concept et performance”

  • Pierre Primot:

    Bonjour,

    Merci pour ce très bon article synthétisant ce qui est vraiment important en une page. C’est autrement plus sympa que la doc Oracle, complète mais dispersée.
    Hélas, tout ceci est formidable… quand ça marche. Mon expérience personnelle est faite de cauchemars autour de WebLogic JMS (10.3.3). Malgré la venue de plusieurs consultants Oracle ou non (ben ouais, on a un support premium), l’ouverture de SR en veux-tu en voilà, la fourniture de paquets de logs, les revues de configuration, les revues de code, l’application de patches sans effet, personne n’a pu nous expliquer pourquoi notre système JMS figeait (cluster + distribued queues) (thread stuck dans l’envoi, thread stuck dans la consommation), sous une charge très modérée (<20 messages/seconde) sur des bêtes de courses surdimensionnées en processeur et RAM.
    Beau produit. Sur le papier. Indébuggable. Plus jamais ça.
    Cordialement !

  • Dans le cas ou vous utilisez des MDB comme producteurs, avez-vous limité la durée des transactions pour l’envoi des messages ?

    Les descripteurs de déploiement des EJBs fixent un délai maximal de transaction.
    L’impact de l’utilisation des transactions distribuées (JTA) sur les performances doit être pris en compte pour de garantir les transactions les plus courtes possible.
    Valeur de dans les weblogic-ejb-jar.xml

    A défaut de solutionner votre problème, cela pourrait permettre de limiter le nombre de thread stuck … et de lever des exceptions java plus exploitables.

    • Pierre Primot:

      Merci pour votre réponse. Archi simple : SW qui postent dans des queues distribuées, MessageListener qui consomment de l’autre côté et modifie une base. Nous avons appliqué des configurations de timeout dans tous les sens (Temporisation JTA > temporisation des ressources XA, puis l’inverse, puis en changeant les ordres de grandeurs des valeurs, et inversement encore, puis en limitant les retry, puis etc etc). Je parle de plusieurs dizaines de tests qui n’ont eu aucun effet, d’abord par nous puis par les spécialistes Oracle. Ce sont des centaines d’heures consacrées à ce sujet.
      Il faut savoir s’avouer vaincu 🙂

  • Avez-vous essayé d’isoler au maximum votre problématique ?
    passer en queue non distribuée , désactiver les appels BDD dans les transactions XA, voire même temporairement rajouter des Thread.sleep(randomSleep) dans vos onMessage() si vous avez des doutes sur la concurrence d’accès … : même si cela vous éloigne de vos besoins, l’objectif étant de reproduire le problème dans le cas le plus minimaliste.

    Je comprends votre désarroi, difficile de se faire une raison et de s’avouer vaincu ! 🙂

  • Guido Amabili:

    Merci pour cet article bien pratique.

Laisser un commentaire

Merci d'effectuer cette opération simple pour valider le commentaire *

Mots-clés
RSS Feed