RabbitMQ Learning: la tercera cola de trabajo Cola de trabajo

En el artículo anterior, el programa se implementó para enviar y recibir mensajes de una cola con nombre. Este artículo creará una cola de trabajo para asignar algunas tareas que requieren mucho tiempo a varios trabajadores.

La idea principal de la cola de trabajo es evitar el procesamiento inmediato de una tarea que consume recursos y esperar a que se complete. En cambio, podemos agregarlo a la lista de planes y realizar estas tareas más adelante. Dividimos la tarea en un mensaje y la enviamos a la cola. El programa de trabajo en segundo plano ejecutará la tarea inmediatamente después de recibir el mensaje. Al ejecutar múltiples actuadores, las tareas se compartirán entre ellos.

Este concepto es más práctico en aplicaciones web, para tareas complejas que no se pueden completar en una breve solicitud http.

1. Prepárate

Lo anterior es enviar un mensaje que contenga "Hola Mundo". Ahora enviemos una cadena que represente una tarea compleja. Aquí no tenemos una tarea real, como la tarea de modificar el tamaño de la imagen y renderizar el archivo pdf. Aquí simulamos una escena donde la tarea está ocupada (usando la función Thread.sleep ()). Aquí usamos el número de puntos en la clase de cadena para representar la complejidad de la tarea, y cada punto ocupa un segundo de tiempo de procesamiento. Por ejemplo, una tarea falsificada descrita con "Hola ..." tomará tres segundos.

Modifiquemos el código Send.java anterior para enviar mensajes arbitrarios desde el cliente. Este programa asignará tareas a nuestra lista de trabajo, llamada NewTask.java:

La parte que envía el mensaje es la siguiente:

String message = getMessage(argv);

channel.basicPublish("", "hello", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");

Obtenga el contenido del mensaje de los parámetros operativos:

private static String getMessage(String[] strings){
    if (strings.length < 1)
        return "Hello World!";
        return joinStrings(strings, " ");
    }

private static String joinStrings(String[] strings, String delimiter) {
    int length = strings.length;
    if (length == 0) return "";
    StringBuilder words = new StringBuilder(strings[0]);
    for (int i = 1; i < length; i++) {
        words.append(delimiter).append(strings[i]);
    }
    return words.toString();
    }

El antiguo receptor también debe modificarse ligeramente: una coma en el cuerpo del mensaje representa una tarea de un segundo, y el receptor recibirá el mensaje y luego ejecutará la tarea. Aquí renombrado a Work.java:

while (true) {
    QueueingConsumer.Delivery delivery = consumer.nextDelivery();
    String message = new String(delivery.getBody());

    System.out.println(" [x] Received '" + message + "'");        
    doWork(message);
    System.out.println(" [x] Done");
}

Luego simule el tiempo dedicado a ejecutar tareas:

private static void doWork(String task) throws InterruptedException {
    for (char ch: task.toCharArray()) {
        if (ch == '.') Thread.sleep(1000);
    }
}

2. Programación de encuestas

Una gran ventaja de la cola de tareas es que puede organizar fácilmente el trabajo. Si parte del trabajo se está acumulando en la cola en segundo plano, se puede resolver agregando más trabajadores.

Primero, ejecutemos dos instancias de trabajo (C1 y C2) al mismo tiempo. Recibirán mensajes de la cola al mismo tiempo. Para más detalles, consulte a continuación:

shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
 [*] Waiting for messages. To exit press CTRL+C
shell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
 [*] Waiting for messages. To exit press CTRL+C

Luego publique la tarea (ejecute el remitente):

shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask First message.
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Second message..
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Third message...
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fourth message....
shell3$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
NewTask Fifth message.....

Luego verifique qué tareas realizaron nuestros trabajadores:

shell1$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar
Worker
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'First message.'
 [x] Received 'Third message...'
 [x] Received 'Fifth message.....'
shell2$ java -cp .:commons-io-1.2.jar:commons-cli-1.1.jar:rabbitmq-client.jar 
Worker
 [*] Waiting for messages. To exit press CTRL+C
 [x] Received 'Second message..'
 [x] Received 'Fourth message....'

Por defecto, RabbitMQ enviará cada mensaje a cada consumidor como una encuesta, y enviará el mensaje de manera uniforme a cada consumidor. Este método de gestión de la asignación se denomina votación y también puede evaluar la situación de varios trabajadores.

3. Mecanismo de respuesta al mensaje

Tarda unos segundos en completar una tarea. Debe tener curiosidad, ¿qué pasa si un consumidor tarda mucho tiempo en comenzar una tarea y se bloquea cuando llega a cierta parte? En nuestro código actual, después de enviar un mensaje a los consumidores, RabbitMQ eliminará inmediatamente el mensaje. En este caso, si matamos a un trabajador, perderemos el mensaje de que el trabajador está procesando la tarea (la tarea no se ha completado), y también perderemos todos los mensajes que se enviaron a este consumidor y que no se han procesado. .

Sin embargo, no queremos perder esta parte del mensaje, esperamos que dichos mensajes puedan enviarse nuevamente a otros trabajadores.

Para garantizar que los mensajes nunca se pierdan, RabbitMQ admite un mecanismo de respuesta de mensajes. Cuando el consumidor recibe el mensaje y completa la tarea, envía un comando de confirmación al servidor RabbitMQ y luego RabbitMQ elimina el mensaje.

Si un consumidor cuelga mientras aún envía mensajes de confirmación, RabbitMQ tratará el servicio como no completado y luego enviará el servicio que ejecutó el mensaje a otro consumidor. De esta manera, incluso si un trabajador se cuelga, el mensaje no se perderá.

No se juzga por el tiempo de espera aquí. Solo cuando un consumidor está desconectado, RabbitMQ reenviará el mensaje de que el consumidor no devolvió la confirmación a otros consumidores. Se tarda mucho tiempo en procesar una determinada tarea al instante, y no hay ningún problema aquí.

El mecanismo de respuesta del mensaje está activado de forma predeterminada. En el ejemplo anterior, lo desactivamos explícitamente (autoAck = true), entonces el programa ahora debería modificarse de la siguiente manera:

QueueingConsumer consumer = new QueueingConsumer(channel);
boolean autoAck = false;
channel.basicConsume("hello", autoAck, consumer);

while (true) {
  QueueingConsumer.Delivery delivery = consumer.nextDelivery();
  //...      
  channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}

Esto asegurará que incluso si mata al trabajador, no habrá pérdida de información. Después de que el trabajador es asesinado, todos los mensajes no confirmados serán reenviados.

Puntos propensos a errores:

Mucha gente olvidará llamar al método basicAck. Aunque este es un error muy simple, a menudo es fatal. Después de que el consumidor salga, el mensaje se reenviará, pero debido a que algunos mensajes no confirmados no se pueden liberar, RabbitMQ consumirá más y más memoria.

Para poder depurar este tipo de error, puede usar rabbitmqctl para imprimir el campo messages_unacknowledged.

$ sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello    0       0
...done.

4. Resistencia informativa

Hemos aprendido el mecanismo de tolerancia a fallas cuando el consumidor se cuelga o se termina la tarea. Veamos cómo garantizar que el mensaje no se pierda cuando se detiene el servicio RabbitMQ.

Cuando RabbitMQ sale o está inactivo, se perderán las colas y los mensajes. Por supuesto, hay dos lugares a los que debe prestar atención para resolver este tipo de problema: el almacenamiento persistente de colas y mensajes.

Primero, queremos asegurarnos de que RabbitMQ nunca perderá la cola de mensajes, luego debemos declararlo como almacenamiento persistente:

boolean durable = true;
channel.queueDeclare("hello", durable, false, false, null);

Aunque la operación aquí es correcta, no tendrá efecto aquí porque la cola llamada "hola" se ha creado antes (no es persistente) y ahora ya existe. RabbitMQ no le permite redefinir una cola de mensajes existente. Si intenta modificar algunos de sus atributos, su programa informará un error. Entonces, aquí debe cambiar el nombre de la cola de mensajes:

boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);

Tanto los productores como los consumidores deben usar el método queueDeclare para especificar propiedades persistentes.

Ahora podemos asegurarnos de que incluso si se reinicia RabbitMQ, la cola de tareas no se perderá. A continuación, implementaré la persistencia de mensajes (estableciendo la propiedad MessageProperties. PERSISTENT_TEXT_PLAIN, donde MessageProperties implementa la interfaz BasicProperties).

import com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue", 
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

Marcar la persistencia del mensaje no garantiza al 100% que el mensaje no se perderá. Aunque RabbitMQ escribirá el mensaje en el disco, pero al recibir el mensaje de RabbitMQ para escribir en el disco, RabbitMQ se reinicia durante este corto período de tiempo Los mensajes escritos en el disco aún se perderán. De hecho, este es el caso: después de que RabbitMQ recibe el mensaje, primero lo escribirá en el búfer de memoria, no escribirá directamente un solo mensaje en el disco en tiempo real. La persistencia de los mensajes no es sólida, pero es suficiente para las colas de tareas simples. Si necesita una solución de persistencia muy sólida, puede usar las confirmaciones del editor (más información sobre cómo usarla más adelante).

5. Estrategia justa de distribución de tareas

Puede notar que a veces RabbitMQ no puede distribuir mensajes como esperaba. Por ejemplo, si hay dos trabajadores, las tareas correspondientes a los mensajes impares requieren mucho tiempo y las tareas correspondientes a los mensajes pares pueden ejecutarse rápidamente. En este caso, uno de los trabajadores siempre estará ocupado, y el otro trabajador difícilmente hará la tarea. RabbitMQ no hará nada para lidiar con este fenómeno y seguirá enviando mensajes de manera uniforme.

Esto se debe a que RabbitMQ se envía al consumidor después de que el productor envía el mensaje, y no verifica la cantidad de mensajes que no han recibido confirmación del consumidor. Solo distribuirá todos los N mensajes a N consumidores.

Para resolver este problema, podemos usar basicQos para establecer cuántos mensajes recibirá el consumidor al mismo tiempo. Establezca aquí 1, lo que indica que RabbitMQ no envía más de un mensaje a los consumidores al mismo tiempo. De esta manera, podemos asegurarnos de que después de procesar una tarea y enviar un mensaje de confirmación, RabbitMQ enviará nuevos mensajes a ella. Si hay nuevos mensajes intermedios, se enviará a otros consumidores. Todos los consumidores están procesando tareas, luego esperarán.

int prefetchCount = 1;
channel.basicQos(prefetchCount);

Tenga en cuenta el tamaño de la cola de mensajes:

Si todos los trabajadores están ocupados, la cola de mensajes puede ser demasiado larga (cuellos de botella en la memoria o en el disco). Debe prestar tanta atención a esta información como sea posible, y puede agregar trabajadores según corresponda.

6. La implementación final del código.

Fin de envío:

import java.io.IOException;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;

public class NewTask {

  private static final String TASK_QUEUE_NAME = "task_queue";

  public static void main(String[] argv) 
                      throws java.io.IOException {

    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    
    //指定队列持久化
    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

    String message = getMessage(argv);

    //指定消息持久化
    channel.basicPublish( "", TASK_QUEUE_NAME, 
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());
    System.out.println(" [x] Sent '" + message + "'");

    channel.close();
    connection.close();
  }      
  //...
}

Receptor

import java.io.IOException;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;

public class Worker {

  private static final String TASK_QUEUE_NAME = "task_queue";

  public static void main(String[] argv)
                      throws java.io.IOException,
                      java.lang.InterruptedException {

    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    //指定队列持久化
    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
    
    //指定该消费者同时只接收一条消息
    channel.basicQos(1);

QueueingConsumer consumer = new QueueingConsumer(channel);

//打开消息应答机制
    channel.basicConsume(TASK_QUEUE_NAME, false, consumer);

    while (true) {
      QueueingConsumer.Delivery delivery = consumer.nextDelivery();
      String message = new String(delivery.getBody());

      System.out.println(" [x] Received '" + message + "'");   
      doWork(message); 
      System.out.println(" [x] Done" );
    
      //返回接收到消息的确认信息
      channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    }
  }
  //...
}

El uso del mecanismo de respuesta de mensajes y prefetchCount puede implementar una cola de trabajo. La opción de persistencia permite que las tareas se pongan en cola y los mensajes no se perderán incluso después de reiniciar RabbitMQ.

Para más aplicaciones en Channel y MessageProperties, puede consultar la documentación oficial de la API de Java:

http://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/

Resumen final:

1. El consumidor abre el mecanismo de respuesta del mensaje en el canal y se asegura de que la información de confirmación del mensaje recibido pueda ser devuelta, para garantizar que el consumidor no perderá el mensaje si falla.

2. El servidor y el cliente deben especificar la persistencia de la cola y la persistencia del mensaje, de modo que RabbitMQ pueda reiniciarse, y la cola y el mensaje no lo serán.

3. Especifique el número de mensajes recibidos por los consumidores para evitar el problema de la utilización irrazonable de los recursos que se produce cuando los mensajes se envían de manera uniforme.

Publicó 40 artículos originales · 25 alabanzas · 100,000+ vistas

Supongo que te gusta

Origin blog.csdn.net/yym373872996/article/details/105651886
Recomendado
Clasificación