Si nous utilisons le mécanisme de requête/réponse JMS en utilisant la "file d'attente temporaire", ce code sera-t-il évolutif?
À l'heure actuelle, nous ne savons pas si nous accepterons 100 demandes par seconde ou 1 000 demandes par seconde.
Le code ci-dessous est ce que je pense à implémenter. Il utilise JMS de manière «synchrone». Les parties clés sont l'endroit où le "consommateur" est créé pour pointer une "file d'attente temporaire" créée pour cette session. Je n'arrive pas à comprendre si l'utilisation de telles files d'attente temporaires est une conception évolutive.
destination = session.createQueue("queue:///Q1");
producer = session.createProducer(destination);
tempDestination = session.createTemporaryQueue();
consumer = session.createConsumer(tempDestination);
long uniqueNumber = System.currentTimeMillis() % 1000;
TextMessage message = session
.createTextMessage("SimpleRequestor: Your lucky number today is " + uniqueNumber);
// Set the JMSReplyTo
message.setJMSReplyTo(tempDestination);
// Start the connection
connection.start();
// And, send the request
producer.send(message);
System.out.println("Sent message:\n" + message);
// Now, receive the reply
Message receivedMessage = consumer.receive(15000); // in ms or 15 seconds
System.out.println("\nReceived message:\n" + receivedMessage);
Mise à jour:
Je suis tombé sur un autre motif, voir ce blog L'idée est d'utiliser des files d'attente «normales» pour les envois et les réceptions. Toutefois, pour les appels "Synchrones", afin d’obtenir la réponse souhaitée (c'est-à-dire correspondant à la demande), vous créez un consommateur qui écoute la file d'attente de réception à l'aide d'un "Sélecteur".
Pas:
// 1. Create Send and Receive Queue.
// 2. Create a msg with a specific ID
final String correlationId = UUID.randomUUID().toString();
final TextMessage textMessage = session.createTextMessage( msg );
textMessage.setJMSCorrelationID( correlationId );
// 3. Start a consumer that receives using a 'Selector'.
consumer = session.createConsumer( replyQueue, "JMSCorrelationID = '" + correlationId + "'" );
La différence dans ce modèle réside donc dans le fait que nous ne créons pas de nouvelle file d'attente temporaire pour chaque nouvelle demande. Au lieu de cela, toutes les réponses arrivent dans une seule file d'attente, mais utilisez un «sélecteur» pour vous assurer que chaque demande-thread reçoit la seule réponse qui compte.
Je pense que l'inconvénient est que vous devez utiliser un «sélecteur». Je ne sais pas encore si c'est moins préféré ou plus préféré que le modèle mentionné précédemment. Pensées?
En ce qui concerne la mise à jour dans vos post-sélecteurs sont très efficaces si elles sont effectuées sur les en-têtes de message, comme vous le faites avec l’ID de corrélation. Spring Integration fait également cela en interne pour implémenter une passerelle JMS Outbound .
Il est intéressant de noter que l’évolutivité de cela peut en réalité être le contraire de ce que les autres réponses ont décrit.
WebSphere MQ enregistre et réutilise les objets de la file d'attente dynamique dans la mesure du possible. Ainsi, bien que l'utilisation d'une file d'attente dynamique ne soit pas libre, elle évolue correctement car, à mesure que les files d'attente sont libérées, tout ce que WMQ doit faire est de transmettre le handle au prochain thread qui demande une nouvelle instance de file d'attente. Dans un groupe QMgr occupé, le nombre de files d'attente dynamiques restera relativement statique tandis que les descripteurs seront transmis d'un thread à l'autre. Strictement parlant, ce n'est pas aussi rapide que de réutiliser une seule file d'attente, mais ce n'est pas si mal.
D'autre part, même si l'indexation sur CORRELID
est rapide, les performances sont inverses par rapport au nombre de messages de l'index. Cela fait également une différence si la profondeur de la file commence à se développer. Lorsque l'application exécute une GET
avec WAIT
dans une file d'attente vide, il n'y a pas de délai. Mais dans une file d'attente profonde, QMgr doit rechercher dans l'index des messages existants pour déterminer si le message de réponse n'en fait pas partie. Dans votre exemple, c'est la différence entre la recherche d'un index vide et d'un index volumineux 1 000 fois par seconde.
Le résultat est que 1 000 files d'attente dynamiques avec un message chacune peuvent en réalité être plus rapides qu'une file d'attente avec 1 000 threads obtenant CORRELID
, en fonction des caractéristiques de l'application et de la charge. Je recommanderais de le tester à grande échelle avant de s’engager dans une conception particulière.
L'utilisation du sélecteur sur l'ID de corrélation sur une file d'attente partagée sera très bien adaptée à plusieurs consommateurs.
1000 requêtes/s seront cependant beaucoup. Vous voudrez peut-être diviser un peu la charge entre différentes instances si les performances s'avèrent problématiques.
Vous voudrez peut-être élaborer sur les demandes par rapport aux numéros de clients. Si le nombre de clients est <10 et reste relativement statique et que le nombre de demandes est très élevé, la solution la plus résiliente et la plus rapide pourrait consister à avoir des files d'attente de réponse statiques pour chaque client.
La création de files d'attente temporaires n'est pas gratuite. Après tout, il alloue des ressources sur le ou les courtiers. Cela dit, si vous avez un nombre inconnu (au préalable) de clients potentiellement non liés (plusieurs JVM, plusieurs threads simultanés par JVM, etc.), vous n'avez peut-être pas le choix. Attribuer des files d'attente aux clients et les attribuer à des clients deviendrait rapidement incontrôlable.
Ce que vous avez esquissé est certainement la solution la plus simple possible. Et si vous pouvez obtenir de vrais chiffres pour le volume des transactions et que celui-ci est suffisamment évolutif, c'est bien.
Avant de chercher à éviter les files d’attente temporaires, je me concentrerais davantage sur la limitation du nombre de clients et la pérennité de ceux-ci. En d'autres termes, créez un pool de clients côté client et demandez aux clients du pool de créer la file d'attente temporaire, la session, la connexion, etc. au démarrage, de les réutiliser lors des demandes suivantes et de les supprimer à l'arrêt. Le problème de réglage devient alors l'un des suivants: taille maximale/minimale sur le pool, durée d'inactivité du nettoyage du pool et comportement (échec/blocage) lorsque le pool est saturé. À moins que vous ne créiez un nombre arbitrairement grand de JVM transitoires (auquel cas vous avez de gros problèmes de dimensionnement dus uniquement au temps de démarrage de la JVM), vous devez procéder à une dimensionnement sans précédent. Après tout, à ce stade, les ressources que vous allouez reflètent l'utilisation réelle du système. Il n'y a vraiment aucune possibilité d'utiliser moins que cela.
Ce qu'il faut éviter, c'est créer et détruire un grand nombre gratuit de files d'attente, de sessions, de connexions, etc. Concevez côté serveur pour permettre la diffusion en continu dès le début. Puis piscine si/quand vous en avez besoin. Comme pour pas, pour quelque chose de non trivial, vous devrez.
L'utilisation de la file d'attente temporaire entraînera des coûts de création à chaque fois que compte confiance. Au lieu d'utiliser des producteurs mis en cache pour une replyToQueue statique, la méthode createProducer sera plus coûteuse et aura un impact sur les performances dans un environnement d'appel hautement concurrentiel.
J'ai été confronté au même problème et j'ai décidé de mettre en commun les connexions dans un haricot sans état. Une connexion client a une tempQueue et se trouve à l'intérieur de l'objet JMSMessageExchanger (qui contient connectionFactory, Queue et tempQueue), qui est lié à une instance de bean. Je l'ai testé dans des environnements JSE/EE. Mais je ne suis pas vraiment sûr du comportement des pools JMS de Glassfish. Est-ce que cela ferme réellement les connexions JMS, obtenues "à la main" après la fin de la méthode Bean? Est-ce que je fais quelque chose de terriblement faux?
J'ai également désactivé la transaction dans le bean client (TransactionAttributeType.NOT_SUPPORTED) pour envoyer immédiatement des messages de demande à la file d'attente des demandes.
package net.sf.selibs.utils.amq;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TemporaryQueue;
import lombok.Getter;
import lombok.Setter;
import net.sf.selibs.utils.misc.UHelper;
public class JMSMessageExchanger {
@Setter
@Getter
protected long timeout = 60 * 1000;
public JMSMessageExchanger(ConnectionFactory cf) {
this.cf = cf;
}
public JMSMessageExchanger(ConnectionFactory cf, Queue queue) {
this.cf = cf;
this.queue = queue;
}
//work
protected ConnectionFactory cf;
protected Queue queue;
protected TemporaryQueue tempQueue;
protected Connection connection;
protected Session session;
protected MessageProducer producer;
protected MessageConsumer consumer;
//status
protected boolean started = false;
protected int mid = 0;
public Message makeRequest(RequestProducer producer) throws Exception {
try {
if (!this.started) {
this.init();
this.tempQueue = this.session.createTemporaryQueue();
this.consumer = this.session.createConsumer(tempQueue);
}
//send request
Message requestM = producer.produce(this.session);
mid++;
requestM.setJMSCorrelationID(String.valueOf(mid));
requestM.setJMSReplyTo(this.tempQueue);
this.producer.send(this.queue, requestM);
//get response
while (true) {
Message responseM = this.consumer.receive(this.timeout);
if (responseM == null) {
return null;
}
int midResp = Integer.parseInt(responseM.getJMSCorrelationID());
if (mid == midResp) {
return responseM;
} else {
//just get other message
}
}
} catch (Exception ex) {
this.close();
throw ex;
}
}
public void makeResponse(ResponseProducer producer) throws Exception {
try {
if (!this.started) {
this.init();
}
Message response = producer.produce(this.session);
response.setJMSCorrelationID(producer.getRequest().getJMSCorrelationID());
this.producer.send(producer.getRequest().getJMSReplyTo(), response);
} catch (Exception ex) {
this.close();
throw ex;
}
}
protected void init() throws Exception {
this.connection = cf.createConnection();
this.session = this.connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
this.producer = this.session.createProducer(null);
this.producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
this.connection.start();
this.started = true;
}
public void close() {
UHelper.close(producer);
UHelper.close(consumer);
UHelper.close(session);
UHelper.close(connection);
this.started = false;
}
}
La même classe est utilisée dans le client (bean sans état) et le serveur (@MessageDriven). RequestProducer et ResponseProducer sont des interfaces:
package net.sf.selibs.utils.amq;
import javax.jms.Message;
import javax.jms.Session;
public interface RequestProducer {
Message produce(Session session) throws Exception;
}
package net.sf.selibs.utils.amq;
import javax.jms.Message;
public interface ResponseProducer extends RequestProducer{
void setRequest(Message request);
Message getRequest();
}
J’ai aussi lu un article d’AMQ sur l’implémentation demande-réponse sur AMQ: http://activemq.Apache.org/how-should-i-implement-request-response-with-jms.html