web-dev-qa-db-fra.com

Architecturer le DAG Airflow qui nécessite une régulation contextuelle

  • J'ai un groupe d'unités de travail (travailleurs) que je souhaite gérer en tant que DAG
  • Group1 a 10 travailleurs et chaque travailleur effectue plusieurs extraits de table à partir d'une base de données. Notez que chaque worker correspond à une seule instance de base de données et que chaque worker doit traiter avec succès 100 tables au total avant de pouvoir se marquer comme terminé.
  • Le groupe 1 a une limitation qui stipule que pas plus de 5 tables sur l'ensemble de ces 10 travailleurs doivent être consommées à la fois. Par exemple:
    • Worker1 extrait 2 tables
    • Worker2 extrait 2 tables
    • Worker3 extrait 1 table
    • Worker4 ... Worker10 doit attendre que Worker1 ... Worker3 abandonne les threads
    • Worker4 ... Worker10 peut récupérer des tables dès que les threads de l'étape1 sont libérés
    • Lorsque chaque travailleur remplit toutes les 100 tables, il passe à l'étape 2 sans attendre. Step2 n'a pas de limites de concurrence

Je devrais être en mesure de créer un seul nœud Group1 qui répond à la limitation et a également

  • 10 nœuds indépendants de travailleurs pour que je puisse les redémarrer en cas d'échec de l'un d'entre eux

J'ai essayé d'expliquer cela dans le diagramme suivant: enter image description here

  • Si l'un des travailleurs échoue, je peux le redémarrer sans affecter les autres travailleurs. Il utilise toujours le même pool de threads de Group1 afin que les limites de concurrence soient appliquées
  • Le groupe 1 se terminerait une fois que tous les éléments de l'étape 1 et de l'étape 2 sont terminés
  • Step2 n'a pas de mesures de concurrence

Comment implémenter une telle hiérarchie dans Airflow pour un Spring Boot Java application? Est-il possible de concevoir ce type de DAG en utilisant les constructions Airflow et de dire dynamiquement Java application combien de tables il peut extraire à la fois. Par exemple, si tous les nœuds de calcul sauf Worker1 ont terminé, Worker1 peut désormais utiliser les 5 threads disponibles tandis que tout le reste passera à l'étape2.

6
Rafay

Ces contraintes ne peuvent pas être modélisées comme un graphe acyclique dirigé, et donc ne peuvent pas être implémentées dans un flux d'air exactement comme décrit. Cependant, elles peuvent être modélisées sous forme de files d'attente et peuvent donc être implémentées avec une infrastructure de file d'attente de travaux. Voici vos deux options:

Mettre en œuvre de manière sous-optimale en tant que DAG de flux d'air:

from airflow.models import DAG
from airflow.operators.subdag_operator import SubDagOperator
# Executors that inherit from BaseExecutor take a parallelism parameter
from wherever import SomeExecutor, SomeOperator

# Table load jobs are done with parallelism 5
load_tables = SubDagOperator(subdag=DAG("load_tables"), executor=SomeExecutor(parallelism=5))

# Each table load must be it's own job, or must be split into sets of tables of predetermined size, such that num_tables_per_job * parallelism = 5
for table in tables:
    load_table = SomeOperator(task_id=f"load_table_{table}", dag=load_tables)

# Jobs done afterwards are done with higher parallelism
afterwards = SubDagOperator(
    subdag=DAG("afterwards"), executor=SomeExecutor(parallelism=high_parallelism)
)

for job in jobs:
    afterward_job = SomeOperator(task_id=f"job_{job}", dag=afterwards)

# After _all_ table load jobs are complete, start the jobs that should be done afterwards

load_tables > afterwards

L'aspect sous-optimal ici, c'est que, pour la première moitié du DAG, le cluster sera sous-utilisé par higher_parallelism - 5.

Implémentez de manière optimale avec la file d'attente des travaux:

# This is pseudocode, but could be easily adapted to a framework like Celery

# You need two queues
# The table load queue should be initialized with the job items
table_load_queue = Queue(initialize_with_tables)
# The queue for jobs to do afterwards starts empty
afterwards_queue = Queue()

def worker():

    # Work while there's at least one item in either queue
    while not table_load_queue.empty() or not afterwards_queue.empty():
        working_on_table_load = [worker.is_working_table_load for worker in scheduler.active()]

        # Work table loads if we haven't reached capacity, otherwise work the jobs afterwards
        if sum(working_on_table_load) < 5:
            is_working_table_load = True
            task = table_load_queue.dequeue()
        else
            is_working_table_load = False
            task = afterwards_queue.dequeue()

        if task:
            after = work(task)
            if is_working_table_load:

                # After working a table load, create the job to work afterwards
                afterwards_queue.enqueue(after)

# Use all the parallelism available
scheduler.start(worker, num_workers=high_parallelism)

En utilisant cette approche, le cluster ne sera pas sous-utilisé.

1
Dave