web-dev-qa-db-fra.com

Gestion des tâches de longue durée dans pika / RabbitMQ

Nous essayons de mettre en place un système de file d'attente dirigée de base où un producteur générera plusieurs tâches et un ou plusieurs consommateurs saisiront une tâche à la fois, la traiteront et accuseront réception du message.

Le problème est que le traitement peut prendre 10 à 20 minutes et que nous ne répondons pas aux messages à ce moment-là, ce qui nous déconnecte du serveur.

Voici un pseudo-code pour notre consommateur:

#!/usr/bin/env python
import pika
import time

connection = pika.BlockingConnection(pika.ConnectionParameters(
        Host='localhost'))
channel = connection.channel()

channel.queue_declare(queue='task_queue', durable=True)
print ' [*] Waiting for messages. To exit press CTRL+C'

def callback(ch, method, properties, body):
    long_running_task(connection)
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_qos(prefetch_count=1)
channel.basic_consume(callback,
                      queue='task_queue')

channel.start_consuming()

Une fois la première tâche terminée, une exception est levée quelque part au fond de BlockingConnection, se plaignant que le socket a été réinitialisé. De plus, les journaux RabbitMQ montrent que le consommateur a été déconnecté pour ne pas avoir répondu à temps (pourquoi il réinitialise la connexion plutôt que d'envoyer un FIN est étrange, mais nous ne nous en inquiéterons pas).

Nous avons cherché beaucoup parce que nous pensions que c'était le cas d'utilisation normal de RabbitMQ (ayant beaucoup de tâches de longue durée qui devraient être réparties entre de nombreux consommateurs), mais il semble que personne d'autre n'ait vraiment eu ce problème. Enfin, nous sommes tombés sur un thread où il était recommandé d'utiliser les pulsations et de générer la long_running_task() dans un thread séparé.

Le code est donc devenu:

#!/usr/bin/env python
import pika
import time
import threading

connection = pika.BlockingConnection(pika.ConnectionParameters(
        Host='localhost',
        heartbeat_interval=20))
channel = connection.channel()

channel.queue_declare(queue='task_queue', durable=True)
print ' [*] Waiting for messages. To exit press CTRL+C'

def thread_func(ch, method, body):
    long_running_task(connection)
    ch.basic_ack(delivery_tag = method.delivery_tag)

def callback(ch, method, properties, body):
    threading.Thread(target=thread_func, args=(ch, method, body)).start()

channel.basic_qos(prefetch_count=1)
channel.basic_consume(callback,
                      queue='task_queue')

channel.start_consuming()

Et cela semble fonctionner, mais c'est très compliqué. Sommes-nous sûrs que l'objet ch est thread-safe? De plus, imaginez que long_running_task() utilise ce paramètre de connexion pour ajouter une tâche à une nouvelle file d'attente (c'est-à-dire que la première partie de ce long processus est terminée, envoyons la tâche à la deuxième partie). Ainsi, le thread utilise l'objet connection. Ce fil est-il sûr?

Plus précisément, quelle est la façon préférée de procéder? J'ai l'impression que c'est très compliqué et peut-être pas sûr pour les threads, alors peut-être que nous ne le faisons pas correctement. Merci!

50
jmacdonagh

Pour l'instant, votre meilleur pari est de désactiver les battements cardiaques, cela empêchera RabbitMQ de fermer la connexion si vous bloquez trop longtemps. J'expérimente la gestion des connexions de base de pika et la boucle IO exécutée dans un thread d'arrière-plan, mais elle n'est pas assez stable pour être libérée.

Dans pika v1.1. c'est ConnectionParameters(heartbeat=0)

22
Gavin M. Roy

Je rencontre le même problème que vous aviez.
Ma solution est:

  1. ture le battement de coeur côté serveur
  2. évaluer le temps maximum que la tâche peut prendre
  3. définir le délai d'expiration du rythme cardiaque du client à l'heure obtenue à partir de l'étape 2

Pourquoi ça?

Comme je teste avec les cas suivants:

  1. activation du rythme cardiaque du serveur, années 1800
  2. client non défini

Je reçois toujours une erreur lorsque la tâche s'exécute pendant très longtemps -> 1800

  1. désactiver la pulsation du serveur
  2. désactiver le rythme cardiaque du client

Il n'y a pas d'erreur côté client, sauf un problème - lorsque le client plante (mon os redémarre sur certains défauts), la connexion tcp est toujours visible sur le plugin Rabbitmq Management. Et c'est déroutant.

  1. désactiver la pulsation du serveur
  2. allumez le rythme cardiaque du client, réglez-le sur la durée d'exécution maximale prévue

Dans ce cas, je peux changer dynamiquement chaque battement de chaleur sur un client individuel. En fait, j'ai mis le rythme cardiaque sur les machines qui se sont plantées fréquemment.En outre, je peux voir la machine hors ligne via le plugin Rabbitmq Manangement.

Environnement

Système d'exploitation: centos x86_64
pika: 0.9.13
rabbitmq: 3.3.1

10
Mr. C

Veuillez ne pas désactiver les battements de cœur!

À partir de Pika 0.12.0, veuillez utiliser la technique décrite dans cet exemple de code pour exécuter votre tâche de longue durée sur un thread séparé, puis accuser réception du message de ce thread.


REMARQUE: l'équipe RabbitMQ surveille le rabbitmq-users mailing list et ne répond que parfois aux questions sur StackOverflow.

6
Luke Bakken
  1. Vous pouvez appeler périodiquement connection.process_data_events() dans votre long_running_task(connection), cette fonction enverra des pulsations au serveur lors de son appel, et gardera le client pika à distance.
  2. Définissez la valeur de pulsation supérieure à l'appel connection.process_data_events() période dans votre pika BlockingConnection.
3
Mars

Vous pouvez également configurer un nouveau thread, traiter le message dans ce nouveau thread et appeler .sleep sur la connexion pendant que ce thread est vivant pour éviter les battements de cœur manquants. Voici un exemple de bloc de code extrait de @gmr dans github, et un lien vers le problème pour référence future.

import re
import json
import threading

from google.cloud import bigquery
import pandas as pd
import pika
from unidecode import unidecode

def process_export(url, tablename):
    df = pd.read_csv(csvURL, encoding="utf-8")
    print("read in the csv")
    columns = list(df)
    ascii_only_name = [unidecode(name) for name in columns]
    cleaned_column_names = [re.sub("[^a-zA-Z0-9_ ]", "", name) for name in ascii_only_name]
    underscored_names = [name.replace(" ", "_") for name in cleaned_column_names]
    valid_gbq_tablename = "test." + tablename
    df.columns = underscored_names

    # try:
    df.to_gbq(valid_gbq_tablename, "some_project", if_exists="append", verbose=True, chunksize=10000)
    # print("Finished Exporting")
    # except Exception as error:
    #     print("unable to export due to: ")
    #     print(error)
    #     print()

def data_handler(channel, method, properties, body):
    body = json.loads(body)

    thread = threading.Thread(target=process_export, args=(body["csvURL"], body["tablename"]))
    thread.start()
    while thread.is_alive():  # Loop while the thread is processing
        channel._connection.sleep(1.0)
    print('Back from thread')
    channel.basic_ack(delivery_tag=method.delivery_tag)


def main():
    params = pika.ConnectionParameters(Host='localhost', heartbeat=60)
    connection = pika.BlockingConnection(params)
    channel = connection.channel()
    channel.queue_declare(queue="some_queue", durable=True)
    channel.basic_qos(prefetch_count=1)
    channel.basic_consume(data_handler, queue="some_queue")
    try:
        channel.start_consuming()
    except KeyboardInterrupt:
        channel.stop_consuming()
    channel.close()

if __name__ == '__main__':
    main()
python

Le lien: https://github.com/pika/pika/issues/930#issuecomment-360333837

0
Demircan Celebi