web-dev-qa-db-fra.com

Tâche distribuée parallèle de céleri avec multitraitement

J'ai une tâche Céleri gourmande en CPU. Je voudrais utiliser toute la puissance de traitement (cœurs) sur de nombreuses instances EC2 pour faire ce travail plus rapidement (une tâche distribuée parallèle céleri avec multitraitement - je pense ).

Les termes threading, multiprocessing, computing distribué, traitement parallèle distribué sont tous des termes que j'essaie de comprendre mieux.

Exemple de tâche:

  @app.task
  for item in list_of_millions_of_ids:
      id = item # do some long complicated equation here very CPU heavy!!!!!!! 
      database.objects(newid=id).save()

En utilisant le code ci-dessus (avec un exemple si possible) comment on aurait distribué cette tâche à l'aide de Céleri en autorisant celle-ci tâche à répartir en utilisant toute la puissance du processeur informatique sur toutes les machines disponibles dans le cloud?

62
Prometheus

Vos objectifs sont:

  1. Répartissez votre travail sur plusieurs machines (calcul distribué/traitement parallèle distribué)
  2. Répartir le travail sur une machine donnée sur tous les CPU (multiprocessing/threading)

Le céleri peut faire les deux pour vous assez facilement. La première chose à comprendre est que chaque travailleur céleri est configuré par défaut pour exécuter autant de tâches qu'il y a de cœurs CPU disponibles sur un système:

La simultanéité est le nombre de processus de travail pré-fourche utilisés pour traiter vos tâches simultanément, lorsque tous ceux-ci sont occupés à faire du travail, de nouvelles tâches devront attendre la fin d'une des tâches avant de pouvoir être traitées.

Le numéro de concurrence par défaut est le nombre de CPU sur cette machine (y compris les cœurs) , vous pouvez spécifier un nombre personnalisé en utilisant l'option -c. Il n'y a pas de valeur recommandée, car le nombre optimal dépend d'un certain nombre de facteurs, mais si vos tâches sont principalement liées aux E/S, vous pouvez essayer de l'augmenter, l'expérimentation a montré que l'ajout de plus du double du nombre de CPU est rarement efficace et susceptible de dégrader les performances.

Cela signifie que chaque tâche individuelle n'a pas à se soucier de l'utilisation du multitraitement/threading pour utiliser plusieurs processeurs/cœurs. Au lieu de cela, le céleri exécutera suffisamment de tâches simultanément pour utiliser chaque processeur disponible.

Avec cela à l'écart, l'étape suivante consiste à créer une tâche qui gère le traitement d'un sous-ensemble de votre list_of_millions_of_ids. Vous avez ici deux options - l'une consiste à faire en sorte que chaque tâche gère un seul ID, donc vous exécutez N tâches, où N == len(list_of_millions_of_ids). Cela garantira que le travail est réparti également entre toutes vos tâches, car il n'y aura jamais un cas où un travailleur termine tôt et attend simplement; s'il a besoin de travail, il peut retirer un identifiant de la file d'attente. Vous pouvez le faire (comme mentionné par John Doe) en utilisant le céleri group.

tasks.py:

@app.task
def process_id(item):
    id = item #long complicated equation here
    database.objects(newid=id).save()

Et pour exécuter les tâches:

from celery import group
from tasks import process_id

jobs = group(process_id.s(item) for item in list_of_millions_of_ids)
result = jobs.apply_async()

Une autre option consiste à diviser la liste en petits morceaux et à les distribuer à vos employés. Cette approche risque de gâcher certains cycles, car vous risquez de vous retrouver avec des travailleurs qui attendent tandis que d'autres continuent de travailler. Cependant, le notes de documentation du céleri que cette préoccupation est souvent infondée:

Certains peuvent craindre que la segmentation de vos tâches entraîne une dégradation du parallélisme, mais cela est rarement vrai pour un cluster occupé et dans la pratique, car vous évitez la surcharge de la messagerie, cela peut considérablement augmenter les performances.

Par conséquent, vous constaterez peut-être que le fractionnement de la liste et la distribution des segments à chaque tâche sont plus efficaces, en raison de la surcharge de messagerie réduite. Vous pouvez probablement également alléger un peu la charge sur la base de données de cette façon, en calculant chaque identifiant, en le stockant dans une liste, puis en ajoutant la liste entière dans la base de données une fois que vous avez terminé, plutôt que de le faire un identifiant à la fois . L'approche par morceaux ressemblerait à quelque chose comme ça

tasks.py:

@app.task
def process_ids(items):
    for item in items:
        id = item #long complicated equation here
        database.objects(newid=id).save() # Still adding one id at a time, but you don't have to.

Et pour démarrer les tâches:

from tasks import process_ids

jobs = process_ids.chunks(list_of_millions_of_ids, 30) # break the list into 30 chunks. Experiment with what number works best here.
jobs.apply_async()

Vous pouvez expérimenter un peu avec quelle taille de segment vous donne le meilleur résultat. Vous voulez trouver un endroit idéal où vous réduisez les frais généraux de messagerie tout en conservant une taille suffisamment petite pour que vous ne finissiez pas avec des travailleurs finissant leur morceau beaucoup plus rapidement qu'un autre travailleur, puis en attendant sans rien faire.

108
dano

Dans le monde de la distribution, il n'y a qu'une chose à retenir avant tout:

L'optimisation prématurée est la racine de tout Mal. Par D. Knuth

Je sais que cela semble évident, mais avant de distribuer une double vérification, vous utilisez le meilleur algorithme (s'il existe ...). Cela dit, l'optimisation de la distribution est un équilibre entre 3 choses:

  1. Écriture/lecture de données à partir d'un support persistant,
  2. Déplacement des données du support A vers le support B,
  3. Données en cours,

Les ordinateurs sont fabriqués de sorte que plus vous vous rapprochez de votre unité de traitement (3), plus rapide et efficace (1) et (2) sera. L'ordre dans un cluster classique sera: disque dur réseau, disque dur local, RAM, à l'intérieur du territoire de l'unité de traitement ... De nos jours, les processeurs deviennent suffisamment sophistiqués pour être considérés comme un ensemble d'unités de traitement matériel indépendant communément appelées cœurs, ces cœurs traitent des données (3) à travers des threads (2). Imaginez que votre cœur soit si rapide que lorsque vous envoyez des données avec un seul thread, vous utilisez 50% de la puissance de l'ordinateur, si le cœur a 2 threads, vous utiliserez alors 100%. Deux threads par cœur sont appelés hyper threading, et votre système d'exploitation verra 2 CPU par noyau hyper threadé.

La gestion des threads dans un processeur est communément appelée multi-threading. La gestion des CPU à partir du système d'exploitation est communément appelée multi-traitement. La gestion de tâches simultanées dans un cluster est communément appelée programmation parallèle. La gestion des tâches dépendantes dans un cluster est communément appelée programmation distribuée.

Où est donc votre goulot d'étranglement?

  • Dans (1): essayez de persister et de diffuser à partir du niveau supérieur (celui le plus proche de votre unité de traitement, par exemple si le disque dur du réseau est lent, enregistrez d'abord sur le disque dur local)
  • Dans (2): c'est le plus courant, essayez d'éviter les paquets de communication non nécessaires pour la distribution ou compressez les paquets "à la volée" (par exemple si le disque dur est lent, enregistrez uniquement un message "calculé par lot" et conservez le résultats intermédiaires en RAM).
  • En (3): Vous avez terminé! Vous utilisez toute la puissance de traitement à votre disposition.

Et le céleri?

Celery est un framework de messagerie pour la programmation distribuée, qui utilisera un module courtier pour la communication (2) et un module backend pour la persistance (1), cela signifie que vous pourrez en changeant la configuration pour éviter la plupart des goulots d'étranglement (si possible) sur votre réseau et uniquement sur votre réseau. Profilez d'abord votre code pour obtenir les meilleures performances sur un seul ordinateur. Utilisez ensuite le céleri dans votre cluster avec la configuration par défaut et définissez CELERY_RESULT_PERSISTENT=True:

from celery import Celery

app = Celery('tasks', 
             broker='amqp://guest@localhost//',
             backend='redis://localhost')

@app.task
def process_id(all_the_data_parameters_needed_to_process_in_this_computer):
    #code that does stuff
    return result

Pendant l'exécution, ouvrez vos outils de surveillance préférés, j'utilise la valeur par défaut pour rabbitMQ et flower pour céleri et top pour cpus, vos résultats seront enregistrés dans votre backend. Un exemple de goulot d'étranglement du réseau est la file d'attente des tâches qui augmente tellement qu'elles retardent l'exécution, vous pouvez procéder à la modification des modules ou de la configuration du céleri, sinon votre goulot d'étranglement est ailleurs.

11
tk.

Pourquoi ne pas utiliser la tâche de céleri group pour cela?

http://celery.readthedocs.org/en/latest/userguide/canvas.html#groups

Fondamentalement, vous devez diviser ids en morceaux (ou plages) et les affecter à un tas de tâches dans group.

Pour des applications plus sophistiquées, comme l'agrégation des résultats de tâches de céleri particulières, j'ai utilisé avec succès la tâche chord dans un but similaire:

http://celery.readthedocs.org/en/latest/userguide/canvas.html#chords

Augmenter settings.CELERYD_CONCURRENCY à un nombre raisonnable et que vous pouvez vous permettre, alors ces travailleurs du céleri continueront d'exécuter vos tâches en groupe ou en accord jusqu'à ce qu'ils soient terminés.

Remarque: en raison d'un bogue dans kombu il y a eu des problèmes avec la réutilisation de travailleurs pour un grand nombre de tâches dans le passé, je ne sais pas si c'est corrigé maintenant. Peut-être que oui, mais sinon, réduisez CELERYD_MAX_TASKS_PER_CHILD.

Exemple basé sur du code simplifié et modifié que j'exécute:

@app.task
def do_matches():
    match_data = ...
    result = chord(single_batch_processor.s(m) for m in match_data)(summarize.s())

summarize obtient les résultats de tous les single_batch_processor les tâches. Chaque tâche s'exécute sur n'importe quel travailleur Celery, kombu coordonne cela.

Maintenant je comprends: single_batch_processor et summarize doivent également être des tâches de céleri, pas des fonctions régulières - sinon bien sûr, elles ne seront pas parallélisées (je ne suis même pas sûr que le constructeur d'accords l'acceptera si ce n'est pas une tâche de céleri).

9
LetMeSOThat4U

Ajouter plus de travailleurs du céleri accélérera certainement l'exécution de la tâche. Vous pourriez cependant avoir un autre goulot d'étranglement: la base de données. Assurez-vous qu'il peut gérer les insertions/mises à jour simultanées.

Concernant votre question: vous ajoutez des travailleurs céleri en affectant un autre processus sur vos instances EC2 en tant que celeryd. Selon le nombre de travailleurs dont vous avez besoin, vous souhaiterez peut-être ajouter encore plus d'instances.

3