web-dev-qa-db-fra.com

RabbitMQ par exemple: Plusieurs threads, canaux et files d'attente

Je viens de lire RabbitMQ Java documentation de l'API , et le trouve très informatif et simple. L'exemple montre comment configurer un simple Channel pour publier/consommer est très facile à suivre et à comprendre, mais c’est un exemple très simple/basique et il m’a laissé une question importante: Comment puis-je configurer 1+ Channels publier/consommer vers et depuis plusieurs files d'attente?

Disons que j'ai un serveur RabbitMQ avec 3 files d'attente: logging, security_events et customer_orders. Nous aurions donc besoin d’un seul Channel pour pouvoir publier/consommer dans les 3 files d’attente, ou plus vraisemblablement, de 3 Channels distincts, chacun étant dédié à une seule file d’attente.

De plus, les meilleures pratiques de RabbitMQ nous imposent de configurer 1 Channel par thread consommateur. Pour cet exemple, disons security_events est correct avec 1 seul thread consommateur, mais logging et customer_order Les deux nécessitent 5 threads pour gérer le volume. Donc, si je comprends bien, cela signifie-t-il que nous avons besoin de:

  • 1 Channel et 1 thread consommateur pour la publication/consommation de et vers security_events; et
  • 5 Channels et 5 threads grand public pour publication/consommation depuis et vers logging; et
  • 5 Channels et 5 threads grand public pour la publication/consommation de et vers customer_orders?

Si ma compréhension est erronée ici, veuillez commencer par me corriger. Quoi qu’il en soit, un ancien combattant RabbitMQ , épuisé par les combats, pourrait-il m'aider à "relier les points" à un exemple de code correct pour configurer des éditeurs/consommateurs qui répondent à mes exigences ici? Merci d'avance!

55
user1768830

Je pense que vous avez plusieurs problèmes avec la compréhension initiale. Franchement, je suis un peu surpris de voir ce qui suit: both need 5 threads to handle the volume. Comment avez-vous identifié que vous aviez besoin de ce nombre exact? Avez-vous des garanties 5 fils suffiront?

RabbitMQ est réglé et testé dans le temps, il est donc essentiel de concevoir correctement et de traiter efficacement les messages.

Essayons d'examiner le problème et de trouver une solution appropriée. En passant, la file de messages en elle-même ne fournira aucune garantie que vous ayez une très bonne solution. Vous devez comprendre ce que vous faites et faire des tests supplémentaires.

Comme vous le savez certainement, de nombreuses dispositions sont possibles:

enter image description here

J'utiliserai la mise en page B comme moyen le plus simple d'illustrer 1 _ producteur N problème de consommation. Depuis que vous êtes tellement préoccupé par le débit. Au fait, RabbitMQ se comporte plutôt bien ( source ). Faites attention à prefetchCount, je vais y répondre plus tard:

enter image description here

Il est donc probable que la logique de traitement des messages constitue un endroit approprié pour vous assurer que le débit est suffisant. Naturellement, vous pouvez créer un nouveau thread à chaque fois que vous devez traiter un message, mais une telle approche finira par tuer votre système. Fondamentalement, plus vous aurez de latence, plus vous obtiendrez de latence (vous pouvez vérifier loi d'Amdahl si vous le souhaitez).

enter image description here

(voir loi d’Amdahl illustrée )

Astuce n ° 1: soyez prudent avec les discussions, utilisez ThreadPools ( details )

Un pool de threads peut être décrit comme une collection d'objets Runnable (file d'attente de travail) et une connexion de threads en cours d'exécution. Ces threads sont en cours d'exécution et vérifient la requête de travail pour un nouveau travail. S'il y a du nouveau travail à faire, ils exécutent ce Runnable. La classe Thread elle-même fournit une méthode, par ex. execute (Runnable r) pour ajouter un nouvel objet Runnable à la file d'attente de travail.

public class Main {
  private static final int NTHREDS = 10;

  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(NTHREDS);
    for (int i = 0; i < 500; i++) {
      Runnable worker = new MyRunnable(10000000L + i);
      executor.execute(worker);
    }
    // This will make the executor accept no new threads
    // and finish all existing threads in the queue
    executor.shutdown();
    // Wait until all threads are finish
    executor.awaitTermination();
    System.out.println("Finished all threads");
  }
} 

Conseil n ° 2: Faites attention à la surcharge de traitement des messages

Je dirais que c'est une technique d'optimisation évidente. Vous allez probablement envoyer de petits messages faciles à traiter. Toute l’approche consiste à définir et à traiter en continu des messages plus petits. Les gros messages finiront par jouer une mauvaise blague, il est donc préférable d'éviter cela.

enter image description here

Il est donc préférable d'envoyer de minuscules informations, mais qu'en est-il du traitement? Il y a des frais généraux chaque fois que vous soumettez un travail. Le traitement par lots peut être très utile en cas de débit de messages entrant élevé.

enter image description here

Par exemple, supposons que nous ayons une logique de traitement de message simple et que nous ne voulions pas avoir de temps système spécifique à chaque thread à chaque traitement du message. Afin d'optimiser cette très simple CompositeRunnable can be introduced:

class CompositeRunnable implements Runnable {

    protected Queue<Runnable> queue = new LinkedList<>();

    public void add(Runnable a) {
        queue.add(a);
    }

    @Override
    public void run() {
        for(Runnable r: queue) {
            r.run();
        }
    }
}

Ou faites la même chose d'une manière légèrement différente, en collectant les messages à traiter:

class CompositeMessageWorker<T> implements Runnable {

    protected Queue<T> queue = new LinkedList<>();

    public void add(T message) {
        queue.add(message);
    }

    @Override
    public void run() {
        for(T message: queue) {
            // process a message
        }
    }
}

De cette manière, vous pouvez traiter les messages plus efficacement.

Astuce n ° 3: Optimiser le traitement des messages

Bien que vous sachiez pouvoir traiter des messages en parallèle (Tip #1) et réduire les coûts de traitement (Tip #2) tu dois tout faire vite. Les étapes de traitement redondantes, les boucles lourdes, etc. peuvent affecter considérablement les performances. Veuillez consulter une étude de cas intéressante:

enter image description here

décuplement du débit de la file d'attente de messages en choisissant le bon analyseur XML

Astuce n ° 4: Gestion de la connexion et des canaux

  • Le démarrage d'un nouveau canal sur une connexion existante implique un aller-retour réseau - le démarrage d'une nouvelle connexion en nécessite plusieurs.
  • Chaque connexion utilise un descripteur de fichier sur le serveur. Les chaînes ne le font pas.
  • La publication d'un message volumineux sur un canal bloque une connexion lorsqu'elle est interrompue. En dehors de cela, le multiplexage est assez transparent.
  • Les connexions en cours de publication peuvent être bloquées si le serveur est surchargé. Il est conseillé de séparer les connexions de publication et de consommation.
  • Soyez prêt à gérer les rafales de messages

( source )

Veuillez noter que tous les conseils fonctionnent parfaitement ensemble. N'hésitez pas à me faire savoir si vous avez besoin de détails supplémentaires.

Exemple de consommateur complet ( source )

Veuillez noter les points suivants:

  • channel.basicQos (prefetch) - Comme vous l'avez vu plus tôt, prefetchCount pourrait être très utile:

    Cette commande permet à un consommateur de choisir une fenêtre de prélecture spécifiant la quantité de messages non accusés de réception qu'il est prêt à recevoir. En définissant le nombre de prélecture sur une valeur différente de zéro, le courtier ne transmettra au consommateur aucun message susceptible d'enfreindre cette limite. Pour déplacer la fenêtre, le consommateur doit accuser réception du message (ou d'un groupe de messages).

  • ExecutorService threadExecutor - vous pouvez spécifier un service d'exécuteur correctement configuré.

Exemple:

static class Worker extends DefaultConsumer {

    String name;
    Channel channel;
    String queue;
    int processed;
    ExecutorService executorService;

    public Worker(int prefetch, ExecutorService threadExecutor,
                  , Channel c, String q) throws Exception {
        super(c);
        channel = c;
        queue = q;
        channel.basicQos(prefetch);
        channel.basicConsume(queue, false, this);
        executorService = threadExecutor;
    }

    @Override
    public void handleDelivery(String consumerTag,
                               Envelope envelope,
                               AMQP.BasicProperties properties,
                               byte[] body) throws IOException {
        Runnable task = new VariableLengthTask(this,
                                               envelope.getDeliveryTag(),
                                               channel);
        executorService.submit(task);
    }
}

Vous pouvez également vérifier les éléments suivants:

126
Renat Gilmanov

Comment puis-je configurer 1 canal ou plus pour publier/consommer vers et depuis plusieurs files d'attente?

Vous pouvez implémenter en utilisant des threads et des canaux. Tout ce dont vous avez besoin est un moyen de classer les éléments, c'est-à-dire tous les éléments de la file d'attente de la connexion, tous les éléments de la file d'attente de security_events, etc. La catégorisation peut être réalisée à l'aide d'une clé de routage.

c'est-à-dire: chaque fois que vous ajoutez un élément à la file d'attente, spécifiez la clé de routage. Il sera ajouté en tant qu'élément de propriété. Vous pouvez ainsi obtenir les valeurs d'un événement particulier, par exemple journalisation.

L'exemple de code suivant explique comment vous procédez de la sorte côté client.

Par exemple:

La clé de routage utilisée sert à identifier le type du canal et à retriver les types.

Par exemple, si vous devez obtenir tous les canaux concernant le type Login, vous devez spécifier la clé de routage en tant que login ou un autre mot clé pour l'identifier.

            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            string routingKey="login";

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());

Vous pouvez regarder ici pour plus de détails sur la catégorisation ..


Partie de filets

Une fois la publication terminée, vous pouvez exécuter la partie thread.

Dans cette partie, vous pouvez obtenir les données publiées en fonction de la catégorie. c'est à dire; Clé de routage qui est dans votre cas logging, security_events et customer_orders etc.

regardez dans l'exemple pour savoir comment récupérer les données dans les threads.

Par exemple:

    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
//**The threads part is as follows** 
 channel.exchangeDeclare(EXCHANGE_NAME, "direct");      
 String queueName = channel.queueDeclare().getQueue();
    // This part will biend the queue with the severity (login for eg:)
    for(String severity : argv){
              channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
    }
    boolean autoAck = false;
    channel.basicConsume(queueName, autoAck, "myConsumerTag",
    new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag,
                                Envelope envelope,
                                AMQP.BasicProperties properties,
                                byte[] body)
         throws IOException
     {
             String routingKey = envelope.getRoutingKey();
             String contentType = properties.contentType;
             long deliveryTag = envelope.getDeliveryTag();

             // (process the message components here ...)
             channel.basicAck(deliveryTag, false);
     }
 });

Maintenant, un thread qui traite les données dans la file d'attente du type login (clé de routage) est créé. De cette façon, vous pouvez créer plusieurs threads. Chacun servant un but différent.

look here pour plus de détails sur la partie threads ..

20
Dileep