Airflow'da dinamik iş akışları oluşturmanın doğru yolu


101

Sorun

Airflow'da, Görev A tamamlanana kadar B görevlerinin sayısının bilinmeyeceği bir iş akışı oluşturmanın bir yolu var mı? Alt etiketlere baktım, ancak görünen o ki, yalnızca Dag oluşturma sırasında belirlenmesi gereken statik bir görevler dizisi ile çalışabilir.

Hançer tetikleyicileri çalışır mı? Ve eğer öyleyse, lütfen bir örnek verin.

Görev A tamamlanana kadar Görev C'yi hesaplamak için gereken görev B sayısını bilmenin imkansız olduğu bir sorunum var. Her Görev B. * 'nin hesaplanması birkaç saat sürer ve birleştirilemez.

              |---> Task B.1 --|
              |---> Task B.2 --|
 Task A ------|---> Task B.3 --|-----> Task C
              |       ....     |
              |---> Task B.N --|

Fikir 1

Bu çözümü beğenmedim çünkü engelleyen bir ExternalTaskSensor oluşturmam gerekiyor ve tüm Görev B * 'nin tamamlanması 2-24 saat sürecek. Bu yüzden bunu geçerli bir çözüm olarak görmüyorum. Elbette daha kolay bir yolu var mı? Yoksa Airflow bunun için tasarlanmamış mıydı?

Dag 1
Task A -> TriggerDagRunOperator(Dag 2) -> ExternalTaskSensor(Dag 2, Task Dummy B) -> Task C

Dag 2 (Dynamically created DAG though python_callable in TriggerDagrunOperator)
               |-- Task B.1 --|
               |-- Task B.2 --|
Task Dummy A --|-- Task B.3 --|-----> Task Dummy B
               |     ....     |
               |-- Task B.N --|

Düzenleme 1:

Şu an itibariyle bu sorunun hala harika bir cevabı yok . Çözüm arayan birkaç kişi benimle temasa geçti.


Tüm B * görevleri, bir döngü içinde oluşturulabilmeleri açısından benzer mi?
Daniel Lee

Evet, tüm B. * görevleri, Görev A tamamlandıktan sonra bir döngü içinde hızlı bir şekilde oluşturulabilir. Görev A'nın tamamlanması yaklaşık 2 saat sürer.
costrouc

Soruna bir çözüm buldunuz mu? belki gönderebilir misin?
Daniel Dubovski

3
1. Fikir için faydalı bir kaynak: linkedin.com/pulse/…
Juan Riaza

1
İşte bunun nasıl açıklayan yazdı bir makale linkedin.com/pulse/dynamic-workflows-airflow-kyle-bridenstine
Kyle Bridenstine

Yanıtlar:


33

Herhangi bir alt etiket olmadan benzer bir istekle bunu şu şekilde yaptım:

Önce istediğiniz değerleri döndüren bir yöntem oluşturun

def values_function():
     return values

Daha sonra işleri dinamik olarak oluşturacak bir yöntem oluşturun:

def group(number, **kwargs):
        #load the values if needed in the command you plan to execute
        dyn_value = "{{ task_instance.xcom_pull(task_ids='push_func') }}"
        return BashOperator(
                task_id='JOB_NAME_{}'.format(number),
                bash_command='script.sh {} {}'.format(dyn_value, number),
                dag=dag)

Ve sonra onları birleştirin:

push_func = PythonOperator(
        task_id='push_func',
        provide_context=True,
        python_callable=values_function,
        dag=dag)

complete = DummyOperator(
        task_id='All_jobs_completed',
        dag=dag)

for i in values_function():
        push_func >> group(i) >> complete

Değerler nerede tanımlanır?
keşiş

11
Bunun yerine for i in values_function()gibi bir şey beklerdim for i in push_func_output. Sorun şu ki, bu çıktıyı dinamik olarak elde etmenin bir yolunu bulamıyorum. PythonOperator'ın çıktısı, yürütmeden sonra Xcom'da olacak, ancak ona DAG tanımından başvurabilir miyim bilmiyorum.
Ena

@Ena Bunu başarmanın bir yolunu buldunuz mu?
eldos

1
@eldos aşağıdaki cevabımı görün
Ena

1
Döngü içinde bir dizi adıma bağlı adım gerçekleştirmemiz gerekse ne olur? groupİşlev içinde ikinci bir bağımlılık zinciri olur mu?
CodingInCircles

12

Önceki görevlerin sonucuna göre iş akışları oluşturmanın bir yolunu buldum.
Temel olarak yapmak istediğiniz şey, aşağıdakileri içeren iki alt etikete sahip olmaktır:

  1. Xcom, ilk çalıştırılan alt etiket içinde bir liste (veya daha sonra dinamik iş akışını oluşturmak için ihtiyacınız olan her şeyi) itin (bkz. Test1.py def return_list())
  2. Ana dag nesnesini parametre olarak ikinci alt etiketinize iletin
  3. Şimdi ana dag nesnesine sahipseniz, onu görev örneklerinin bir listesini almak için kullanabilirsiniz. Bu görev örnekleri listesinden, geçerli çalıştırmanın bir görevini parent_dag.get_task_instances(settings.Session, start_date=parent_dag.get_active_runs()[-1])[-1]) kullanarak filtreleyebilirsiniz, muhtemelen buraya daha fazla filtre eklenebilir.
  4. Bu görev örneğiyle, ilk alt etiketten birine dag_id belirterek ihtiyacınız olan değeri elde etmek için xcom pull kullanabilirsiniz: dag_id='%s.%s' % (parent_dag_name, 'test1')
  5. Görevlerinizi dinamik olarak oluşturmak için listeyi / değeri kullanın

Şimdi bunu yerel hava akımı tesisatımda test ettim ve iyi çalışıyor. Aynı anda çalışan birden fazla dag örneği varsa xcom çekme kısmında herhangi bir sorun olup olmayacağını bilmiyorum, ancak o zaman xcom'u benzersiz bir şekilde tanımlamak için muhtemelen benzersiz bir anahtar veya buna benzer bir şey kullanırsınız. istediğiniz değer. Muhtemelen 3. adımı, mevcut ana dag'in belirli bir görevini alacağınızdan% 100 emin olacak şekilde optimize edebilir, ancak benim kullanımım için bu yeterince iyi performans gösteriyor, bence xcom_pull'u kullanmak için yalnızca bir task_instance nesnesine ihtiyaç var.

Ayrıca, kazara yanlış bir değer almadığımdan emin olmak için, her yürütmeden önce ilk alt etiket için xcom'ları temizliyorum.

Açıklamakta oldukça kötüyüm, bu yüzden aşağıdaki kodun her şeyi netleştireceğini umuyorum:

test1.py

from airflow.models import DAG
import logging
from airflow.operators.python_operator import PythonOperator
from airflow.operators.postgres_operator import PostgresOperator

log = logging.getLogger(__name__)


def test1(parent_dag_name, start_date, schedule_interval):
    dag = DAG(
        '%s.test1' % parent_dag_name,
        schedule_interval=schedule_interval,
        start_date=start_date,
    )

    def return_list():
        return ['test1', 'test2']

    list_extract_folder = PythonOperator(
        task_id='list',
        dag=dag,
        python_callable=return_list
    )

    clean_xcoms = PostgresOperator(
        task_id='clean_xcoms',
        postgres_conn_id='airflow_db',
        sql="delete from xcom where dag_id='{{ dag.dag_id }}'",
        dag=dag)

    clean_xcoms >> list_extract_folder

    return dag

test2.py

from airflow.models import DAG, settings
import logging
from airflow.operators.dummy_operator import DummyOperator

log = logging.getLogger(__name__)


def test2(parent_dag_name, start_date, schedule_interval, parent_dag=None):
    dag = DAG(
        '%s.test2' % parent_dag_name,
        schedule_interval=schedule_interval,
        start_date=start_date
    )

    if len(parent_dag.get_active_runs()) > 0:
        test_list = parent_dag.get_task_instances(settings.Session, start_date=parent_dag.get_active_runs()[-1])[-1].xcom_pull(
            dag_id='%s.%s' % (parent_dag_name, 'test1'),
            task_ids='list')
        if test_list:
            for i in test_list:
                test = DummyOperator(
                    task_id=i,
                    dag=dag
                )

    return dag

ve ana iş akışı:

test.py

from datetime import datetime
from airflow import DAG
from airflow.operators.subdag_operator import SubDagOperator
from subdags.test1 import test1
from subdags.test2 import test2

DAG_NAME = 'test-dag'

dag = DAG(DAG_NAME,
          description='Test workflow',
          catchup=False,
          schedule_interval='0 0 * * *',
          start_date=datetime(2018, 8, 24))

test1 = SubDagOperator(
    subdag=test1(DAG_NAME,
                 dag.start_date,
                 dag.schedule_interval),
    task_id='test1',
    dag=dag
)

test2 = SubDagOperator(
    subdag=test2(DAG_NAME,
                 dag.start_date,
                 dag.schedule_interval,
                 parent_dag=dag),
    task_id='test2',
    dag=dag
)

test1 >> test2

Airflow 1.9'da bunlar DAG klasörüne eklendiğinde yüklenmedi, bir şey eksik mi?
Anthony Keane

@AnthonyKeane, test1.py ve test2.py'yi dag klasörünüzdeki alt etiketler adlı bir klasöre koydunuz mu?
Christopher Beck

Evet yaptım. Her iki dosyayı da alt etiketlere kopyaladı ve test.py'yi dag klasörüne yerleştirdi, yine de bu hatayı al. Bozuk DAG: [/home/airflow/gcs/dags/test.py] subdags.test1 adında modül yok Not Google Cloud Composer'ı kullanıyorum (Google'ın yönetilen Airflow 1.9.0)
Anthony Keane

@AnthonyKeane, günlüklerde gördüğünüz tek hata bu mu? Bozuk DAG, bir derleme hatası olan alt etiketten kaynaklanıyor olabilir.
Christopher Beck

3
Merhaba @Christopher Beck _ _init_ _.pyAlt etiketler klasörüne eklemem gereken hatamı buldum. çaylak hatası
Anthony Keane

11

Evet bu mümkün Bunu gösteren örnek bir DAG oluşturdum.

import airflow
from airflow.operators.python_operator import PythonOperator
import os
from airflow.models import Variable
import logging
from airflow import configuration as conf
from airflow.models import DagBag, TaskInstance
from airflow import DAG, settings
from airflow.operators.bash_operator import BashOperator

main_dag_id = 'DynamicWorkflow2'

args = {
    'owner': 'airflow',
    'start_date': airflow.utils.dates.days_ago(2),
    'provide_context': True
}

dag = DAG(
    main_dag_id,
    schedule_interval="@once",
    default_args=args)


def start(*args, **kwargs):

    value = Variable.get("DynamicWorkflow_Group1")
    logging.info("Current DynamicWorkflow_Group1 value is " + str(value))


def resetTasksStatus(task_id, execution_date):
    logging.info("Resetting: " + task_id + " " + execution_date)

    dag_folder = conf.get('core', 'DAGS_FOLDER')
    dagbag = DagBag(dag_folder)
    check_dag = dagbag.dags[main_dag_id]
    session = settings.Session()

    my_task = check_dag.get_task(task_id)
    ti = TaskInstance(my_task, execution_date)
    state = ti.current_state()
    logging.info("Current state of " + task_id + " is " + str(state))
    ti.set_state(None, session)
    state = ti.current_state()
    logging.info("Updated state of " + task_id + " is " + str(state))


def bridge1(*args, **kwargs):

    # You can set this value dynamically e.g., from a database or a calculation
    dynamicValue = 2

    variableValue = Variable.get("DynamicWorkflow_Group2")
    logging.info("Current DynamicWorkflow_Group2 value is " + str(variableValue))

    logging.info("Setting the Airflow Variable DynamicWorkflow_Group2 to " + str(dynamicValue))
    os.system('airflow variables --set DynamicWorkflow_Group2 ' + str(dynamicValue))

    variableValue = Variable.get("DynamicWorkflow_Group2")
    logging.info("Current DynamicWorkflow_Group2 value is " + str(variableValue))

    # Below code prevents this bug: https://issues.apache.org/jira/browse/AIRFLOW-1460
    for i in range(dynamicValue):
        resetTasksStatus('secondGroup_' + str(i), str(kwargs['execution_date']))


def bridge2(*args, **kwargs):

    # You can set this value dynamically e.g., from a database or a calculation
    dynamicValue = 3

    variableValue = Variable.get("DynamicWorkflow_Group3")
    logging.info("Current DynamicWorkflow_Group3 value is " + str(variableValue))

    logging.info("Setting the Airflow Variable DynamicWorkflow_Group3 to " + str(dynamicValue))
    os.system('airflow variables --set DynamicWorkflow_Group3 ' + str(dynamicValue))

    variableValue = Variable.get("DynamicWorkflow_Group3")
    logging.info("Current DynamicWorkflow_Group3 value is " + str(variableValue))

    # Below code prevents this bug: https://issues.apache.org/jira/browse/AIRFLOW-1460
    for i in range(dynamicValue):
        resetTasksStatus('thirdGroup_' + str(i), str(kwargs['execution_date']))


def end(*args, **kwargs):
    logging.info("Ending")


def doSomeWork(name, index, *args, **kwargs):
    # Do whatever work you need to do
    # Here I will just create a new file
    os.system('touch /home/ec2-user/airflow/' + str(name) + str(index) + '.txt')


starting_task = PythonOperator(
    task_id='start',
    dag=dag,
    provide_context=True,
    python_callable=start,
    op_args=[])

# Used to connect the stream in the event that the range is zero
bridge1_task = PythonOperator(
    task_id='bridge1',
    dag=dag,
    provide_context=True,
    python_callable=bridge1,
    op_args=[])

DynamicWorkflow_Group1 = Variable.get("DynamicWorkflow_Group1")
logging.info("The current DynamicWorkflow_Group1 value is " + str(DynamicWorkflow_Group1))

for index in range(int(DynamicWorkflow_Group1)):
    dynamicTask = PythonOperator(
        task_id='firstGroup_' + str(index),
        dag=dag,
        provide_context=True,
        python_callable=doSomeWork,
        op_args=['firstGroup', index])

    starting_task.set_downstream(dynamicTask)
    dynamicTask.set_downstream(bridge1_task)

# Used to connect the stream in the event that the range is zero
bridge2_task = PythonOperator(
    task_id='bridge2',
    dag=dag,
    provide_context=True,
    python_callable=bridge2,
    op_args=[])

DynamicWorkflow_Group2 = Variable.get("DynamicWorkflow_Group2")
logging.info("The current DynamicWorkflow value is " + str(DynamicWorkflow_Group2))

for index in range(int(DynamicWorkflow_Group2)):
    dynamicTask = PythonOperator(
        task_id='secondGroup_' + str(index),
        dag=dag,
        provide_context=True,
        python_callable=doSomeWork,
        op_args=['secondGroup', index])

    bridge1_task.set_downstream(dynamicTask)
    dynamicTask.set_downstream(bridge2_task)

ending_task = PythonOperator(
    task_id='end',
    dag=dag,
    provide_context=True,
    python_callable=end,
    op_args=[])

DynamicWorkflow_Group3 = Variable.get("DynamicWorkflow_Group3")
logging.info("The current DynamicWorkflow value is " + str(DynamicWorkflow_Group3))

for index in range(int(DynamicWorkflow_Group3)):

    # You can make this logic anything you'd like
    # I chose to use the PythonOperator for all tasks
    # except the last task will use the BashOperator
    if index < (int(DynamicWorkflow_Group3) - 1):
        dynamicTask = PythonOperator(
            task_id='thirdGroup_' + str(index),
            dag=dag,
            provide_context=True,
            python_callable=doSomeWork,
            op_args=['thirdGroup', index])
    else:
        dynamicTask = BashOperator(
            task_id='thirdGroup_' + str(index),
            bash_command='touch /home/ec2-user/airflow/thirdGroup_' + str(index) + '.txt',
            dag=dag)

    bridge2_task.set_downstream(dynamicTask)
    dynamicTask.set_downstream(ending_task)

# If you do not connect these then in the event that your range is ever zero you will have a disconnection between your stream
# and your tasks will run simultaneously instead of in your desired stream order.
starting_task.set_downstream(bridge1_task)
bridge1_task.set_downstream(bridge2_task)
bridge2_task.set_downstream(ending_task)

DAG'yi çalıştırmadan önce bu üç Hava Akışı Değişkenini oluşturun

airflow variables --set DynamicWorkflow_Group1 1

airflow variables --set DynamicWorkflow_Group2 0

airflow variables --set DynamicWorkflow_Group3 0

DAG'nin bundan çıktığını göreceksiniz

görüntü açıklamasını buraya girin

Buna koştuktan sonra

görüntü açıklamasını buraya girin

Bu DAG hakkında daha fazla bilgiyi Airflow Üzerinde Dinamik İş Akışları oluşturma hakkındaki makalemde görebilirsiniz .


1
Ancak bu DAG'den birden fazla DagRun'a sahipseniz ne olur? Hepsi aynı Değişkenleri mi paylaşıyor?
Mar-k

1
Evet, aynı değişkeni kullanırlar; Bunu makalemin en sonunda ele alıyorum. Değişkeni dinamik olarak oluşturmanız ve değişken adında dag çalıştırma kimliğini kullanmanız gerekir.
Örneğim

Dinamik görevler oluştururken köprüler gerekli mi? Yazınızı anlık olarak tamamen okuyacak ama sormak istedim. Şu anda yukarı akış göreve dayalı dinamik bir görev oluşturmakta zorlanıyorum ve nerede yanlış yaptığımı anlamaya başlıyorum. Şu andaki sorunum, bazı nedenlerden dolayı DAG'nin DAG-Bag ile senkronize olmasını sağlayamıyorum. DAG'ım modülde statik bir liste kullanırken senkronize edildi, ancak bu statik listeyi bir yukarı akış görevinden oluşturulacak şekilde değiştirdiğimde durdu.
lucid_goose

Bu çok akıllıca
jvans

1
@jvans, akıllıca olduğu için teşekkürler ama muhtemelen üretim kalitesi değil
Kyle Bridenstine

6

OA: "Airflow'da, Görev A tamamlanana kadar B görevlerinin sayısı bilinmeyecek şekilde bir iş akışı oluşturmanın herhangi bir yolu var mı?"

Kısa cevap hayır. Hava akışı, çalıştırmaya başlamadan önce DAG akışını oluşturacaktır.

Bununla birlikte, basit bir sonuca vardık, yani böyle bir ihtiyacımız yok. Bazı işleri paralel hale getirmek istediğinizde, işlenecek öğe sayısını değil, elinizdeki kaynakları değerlendirmelisiniz.

Bunu şu şekilde yaptık: dinamik olarak işi bölen sabit sayıda, mesela 10 görev oluşturuyoruz. Örneğin, 100 dosya işlememiz gerekirse, her görev bunlardan 10 tanesini işleyecektir. Kodu bugün daha sonra göndereceğim.

Güncelleme

İşte kod, gecikme için özür dilerim.

from datetime import datetime, timedelta

import airflow
from airflow.operators.dummy_operator import DummyOperator

args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'start_date': datetime(2018, 1, 8),
    'email': ['myemail@gmail.com'],
    'email_on_failure': True,
    'email_on_retry': True,
    'retries': 1,
    'retry_delay': timedelta(seconds=5)
}

dag = airflow.DAG(
    'parallel_tasks_v1',
    schedule_interval="@daily",
    catchup=False,
    default_args=args)

# You can read this from variables
parallel_tasks_total_number = 10

start_task = DummyOperator(
    task_id='start_task',
    dag=dag
)


# Creates the tasks dynamically.
# Each one will elaborate one chunk of data.
def create_dynamic_task(current_task_number):
    return DummyOperator(
        provide_context=True,
        task_id='parallel_task_' + str(current_task_number),
        python_callable=parallelTask,
        # your task will take as input the total number and the current number to elaborate a chunk of total elements
        op_args=[current_task_number, int(parallel_tasks_total_number)],
        dag=dag)


end = DummyOperator(
    task_id='end',
    dag=dag)

for page in range(int(parallel_tasks_total_number)):
    created_task = create_dynamic_task(page)
    start_task >> created_task
    created_task >> end

Kod açıklaması:

Burada tek bir başlangıç ​​görevimiz ve tek bir son görevimiz var (her ikisi de kukla).

Daha sonra for döngüsü ile başlangıç ​​görevinden aynı python çağrılabilir ile 10 görev oluşturuyoruz. Görevler create_dynamic_task işlevinde oluşturulur.

Çağrılabilir her python'a argüman olarak paralel görevlerin toplam sayısını ve mevcut görev dizinini iletiriz.

Ayrıntılandırmanız gereken 1000 öğeniz olduğunu varsayalım: İlk görev, 10 parçadan ilk parçayı ayrıntılandırması gereken girdiyi alacak. 1000 öğeyi 10 parçaya böler ve ilkini detaylandırır.


1
Öğe başına belirli bir göreve ihtiyacınız olmadığı sürece bu iyi bir çözümdür (ilerleme, sonuç, başarı / başarısızlık, yeniden
denemeler

@Ena parallelTasktanımlanmadı: Bir şeyi mi kaçırıyorum?
Anthony Keane

2
@AnthonyKeane Aslında bir şeyler yapmak için aramanız gereken python işlevidir. Kodda açıklandığı gibi, toplam elemanların bir yığınını ayrıntılandırmak için toplam sayıyı ve mevcut sayıyı girdi olarak alacaktır.
Ena

4

DAG'yi dinamik olarak oluşturmak olduğunu düşündüğüm şey, bu tür bir durumla birkaç gün önce bu blogu bulduğum bazı aramalardan sonra karşılaştım .

Dinamik Görev Oluşturma

start = DummyOperator(
    task_id='start',
    dag=dag
)

end = DummyOperator(
    task_id='end',
    dag=dag)

def createDynamicETL(task_id, callableFunction, args):
    task = PythonOperator(
        task_id = task_id,
        provide_context=True,
        #Eval is used since the callableFunction var is of type string
        #while the python_callable argument for PythonOperators only receives objects of type callable not strings.
        python_callable = eval(callableFunction),
        op_kwargs = args,
        xcom_push = True,
        dag = dag,
    )
    return task

DAG iş akışını ayarlama

with open('/usr/local/airflow/dags/config_files/dynamicDagConfigFile.yaml') as f:
    # Use safe_load instead to load the YAML file
    configFile = yaml.safe_load(f)

    # Extract table names and fields to be processed
    tables = configFile['tables']

    # In this loop tasks are created for each table defined in the YAML file
    for table in tables:
        for table, fieldName in table.items():
            # In our example, first step in the workflow for each table is to get SQL data from db.
            # Remember task id is provided in order to exchange data among tasks generated in dynamic way.
            get_sql_data_task = createDynamicETL('{}-getSQLData'.format(table),
                                                 'getSQLData',
                                                 {'host': 'host', 'user': 'user', 'port': 'port', 'password': 'pass',
                                                  'dbname': configFile['dbname']})

            # Second step is upload data to s3
            upload_to_s3_task = createDynamicETL('{}-uploadDataToS3'.format(table),
                                                 'uploadDataToS3',
                                                 {'previous_task_id': '{}-getSQLData'.format(table),
                                                  'bucket_name': configFile['bucket_name'],
                                                  'prefix': configFile['prefix']})

            # This is where the magic lies. The idea is that
            # once tasks are generated they should linked with the
            # dummy operators generated in the start and end tasks. 
            # Then you are done!
            start >> get_sql_data_task
            get_sql_data_task >> upload_to_s3_task
            upload_to_s3_task >> end

Kodu bir araya getirdikten sonra DAG'miz böyle görünüyor görüntü açıklamasını buraya girin

import yaml
import airflow
from airflow import DAG
from datetime import datetime, timedelta, time
from airflow.operators.python_operator import PythonOperator
from airflow.operators.dummy_operator import DummyOperator

start = DummyOperator(
    task_id='start',
    dag=dag
)


def createDynamicETL(task_id, callableFunction, args):
    task = PythonOperator(
        task_id=task_id,
        provide_context=True,
        # Eval is used since the callableFunction var is of type string
        # while the python_callable argument for PythonOperators only receives objects of type callable not strings.
        python_callable=eval(callableFunction),
        op_kwargs=args,
        xcom_push=True,
        dag=dag,
    )
    return task


end = DummyOperator(
    task_id='end',
    dag=dag)

with open('/usr/local/airflow/dags/config_files/dynamicDagConfigFile.yaml') as f:
    # use safe_load instead to load the YAML file
    configFile = yaml.safe_load(f)

    # Extract table names and fields to be processed
    tables = configFile['tables']

    # In this loop tasks are created for each table defined in the YAML file
    for table in tables:
        for table, fieldName in table.items():
            # In our example, first step in the workflow for each table is to get SQL data from db.
            # Remember task id is provided in order to exchange data among tasks generated in dynamic way.
            get_sql_data_task = createDynamicETL('{}-getSQLData'.format(table),
                                                 'getSQLData',
                                                 {'host': 'host', 'user': 'user', 'port': 'port', 'password': 'pass',
                                                  'dbname': configFile['dbname']})

            # Second step is upload data to s3
            upload_to_s3_task = createDynamicETL('{}-uploadDataToS3'.format(table),
                                                 'uploadDataToS3',
                                                 {'previous_task_id': '{}-getSQLData'.format(table),
                                                  'bucket_name': configFile['bucket_name'],
                                                  'prefix': configFile['prefix']})

            # This is where the magic lies. The idea is that
            # once tasks are generated they should linked with the
            # dummy operators generated in the start and end tasks. 
            # Then you are done!
            start >> get_sql_data_task
            get_sql_data_task >> upload_to_s3_task
            upload_to_s3_task >> end

Bir başkasına da yardım edecek.


Kendiniz mi başardınız? Yorgunum. Ama başarısız oldum.
Newt

Evet, benim için çalıştı. Hangi sorunla karşılaşıyorsun?
Muhammed Bin Ali

1
Anladım. Sorunum çözüldü. Teşekkürler. Docker görüntülerinde ortam değişkenlerini okumanın doğru yolunu bulamadım.
Newt

1
Ya tablo öğeleri değişebilirse, bu yüzden onları statik bir yaml dosyasına koyamazsak?
FrankZhu

Gerçekten onu nerede kullandığınıza bağlı. Yine de ne öneriyorsun ilgimi çekiyor. @FrankZhu nasıl düzgün yapılmalı?
Muhammed Bin Ali

3

Ben de buna güzel bir çözüm bulduklarını düşünüyorum https://github.com/mastak/airflow_multi_dagrun benzer birden dagruns, tetikleyerek DagRuns basit enqueuing kullanır TriggerDagRuns . Kredilerin çoğu https://github.com/mastak adresine gidiyor , ancak en son hava akışıyla çalışmasını sağlamak için bazı ayrıntıları düzeltmek zorunda kaldım .

Çözüm, birkaç DagRun'u tetikleyen özel bir operatör kullanır :

from airflow import settings
from airflow.models import DagBag
from airflow.operators.dagrun_operator import DagRunOrder, TriggerDagRunOperator
from airflow.utils.decorators import apply_defaults
from airflow.utils.state import State
from airflow.utils import timezone


class TriggerMultiDagRunOperator(TriggerDagRunOperator):
    CREATED_DAGRUN_KEY = 'created_dagrun_key'

    @apply_defaults
    def __init__(self, op_args=None, op_kwargs=None,
                 *args, **kwargs):
        super(TriggerMultiDagRunOperator, self).__init__(*args, **kwargs)
        self.op_args = op_args or []
        self.op_kwargs = op_kwargs or {}

    def execute(self, context):

        context.update(self.op_kwargs)
        session = settings.Session()
        created_dr_ids = []
        for dro in self.python_callable(*self.op_args, **context):
            if not dro:
                break
            if not isinstance(dro, DagRunOrder):
                dro = DagRunOrder(payload=dro)

            now = timezone.utcnow()
            if dro.run_id is None:
                dro.run_id = 'trig__' + now.isoformat()

            dbag = DagBag(settings.DAGS_FOLDER)
            trigger_dag = dbag.get_dag(self.trigger_dag_id)
            dr = trigger_dag.create_dagrun(
                run_id=dro.run_id,
                execution_date=now,
                state=State.RUNNING,
                conf=dro.payload,
                external_trigger=True,
            )
            created_dr_ids.append(dr.id)
            self.log.info("Created DagRun %s, %s", dr, now)

        if created_dr_ids:
            session.commit()
            context['ti'].xcom_push(self.CREATED_DAGRUN_KEY, created_dr_ids)
        else:
            self.log.info("No DagRun created")
        session.close()

Daha sonra PythonOperator'ınızdaki çağrılabilir işlevden birkaç dagrun gönderebilirsiniz, örneğin:

from airflow.operators.dagrun_operator import DagRunOrder
from airflow.models import DAG
from airflow.operators import TriggerMultiDagRunOperator
from airflow.utils.dates import days_ago


def generate_dag_run(**kwargs):
    for i in range(10):
        order = DagRunOrder(payload={'my_variable': i})
        yield order

args = {
    'start_date': days_ago(1),
    'owner': 'airflow',
}

dag = DAG(
    dag_id='simple_trigger',
    max_active_runs=1,
    schedule_interval='@hourly',
    default_args=args,
)

gen_target_dag_run = TriggerMultiDagRunOperator(
    task_id='gen_target_dag_run',
    dag=dag,
    trigger_dag_id='common_target',
    python_callable=generate_dag_run
)

Https://github.com/flinz/airflow_multi_dagrun adresindeki kodla bir çatal oluşturdum


3

İşler grafiği çalışma zamanında oluşturulmaz. Bunun yerine, grafik, Airflow tarafından dags klasörünüzden alındığında oluşturulur. Bu nedenle, her çalıştığında iş için farklı bir grafiğe sahip olmak gerçekten mümkün olmayacak. Yükleme zamanında bir sorguya dayalı bir grafik oluşturmak için bir işi yapılandırabilirsiniz . Bu grafik bundan sonraki her çalışmada aynı kalacak ve bu muhtemelen pek kullanışlı olmayacaktır.

Şube Operatörü kullanarak sorgu sonuçlarına göre her çalıştırmada farklı görevleri yürüten bir grafik tasarlayabilirsiniz.

Yaptığım şey, bir dizi görevi önceden yapılandırmak ve ardından sorgu sonuçlarını almak ve bunları görevler arasında dağıtmaktır. Bu muhtemelen daha iyidir, çünkü sorgunuz çok fazla sonuç döndürürse, muhtemelen planlayıcıya çok sayıda eşzamanlı görevle doldurmak istemezsiniz. Daha da güvenli olmak için, eşzamanlılığımın beklenmedik büyüklükteki bir sorguyla kontrolden çıkmamasını sağlamak için bir havuz da kullandım.

"""
 - This is an idea for how to invoke multiple tasks based on the query results
"""
import logging
from datetime import datetime

from airflow import DAG
from airflow.hooks.postgres_hook import PostgresHook
from airflow.operators.mysql_operator import MySqlOperator
from airflow.operators.python_operator import PythonOperator, BranchPythonOperator
from include.run_celery_task import runCeleryTask

########################################################################

default_args = {
    'owner': 'airflow',
    'catchup': False,
    'depends_on_past': False,
    'start_date': datetime(2019, 7, 2, 19, 50, 00),
    'email': ['rotten@stackoverflow'],
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 0,
    'max_active_runs': 1
}

dag = DAG('dynamic_tasks_example', default_args=default_args, schedule_interval=None)

totalBuckets = 5

get_orders_query = """
select 
    o.id,
    o.customer
from 
    orders o
where
    o.created_at >= current_timestamp at time zone 'UTC' - '2 days'::interval
    and
    o.is_test = false
    and
    o.is_processed = false
"""

###########################################################################################################

# Generate a set of tasks so we can parallelize the results
def createOrderProcessingTask(bucket_number):
    return PythonOperator( 
                           task_id=f'order_processing_task_{bucket_number}',
                           python_callable=runOrderProcessing,
                           pool='order_processing_pool',
                           op_kwargs={'task_bucket': f'order_processing_task_{bucket_number}'},
                           provide_context=True,
                           dag=dag
                          )


# Fetch the order arguments from xcom and doStuff() to them
def runOrderProcessing(task_bucket, **context):
    orderList = context['ti'].xcom_pull(task_ids='get_open_orders', key=task_bucket)

    if orderList is not None:
        for order in orderList:
            logging.info(f"Processing Order with Order ID {order[order_id]}, customer ID {order[customer_id]}")
            doStuff(**op_kwargs)


# Discover the orders we need to run and group them into buckets for processing
def getOpenOrders(**context):
    myDatabaseHook = PostgresHook(postgres_conn_id='my_database_conn_id')

    # initialize the task list buckets
    tasks = {}
    for task_number in range(0, totalBuckets):
        tasks[f'order_processing_task_{task_number}'] = []

    # populate the task list buckets
    # distribute them evenly across the set of buckets
    resultCounter = 0
    for record in myDatabaseHook.get_records(get_orders_query):

        resultCounter += 1
        bucket = (resultCounter % totalBuckets)

        tasks[f'order_processing_task_{bucket}'].append({'order_id': str(record[0]), 'customer_id': str(record[1])})

    # push the order lists into xcom
    for task in tasks:
        if len(tasks[task]) > 0:
            logging.info(f'Task {task} has {len(tasks[task])} orders.')
            context['ti'].xcom_push(key=task, value=tasks[task])
        else:
            # if we didn't have enough tasks for every bucket
            # don't bother running that task - remove it from the list
            logging.info(f"Task {task} doesn't have any orders.")
            del(tasks[task])

    return list(tasks.keys())

###################################################################################################


# this just makes sure that there aren't any dangling xcom values in the database from a crashed dag
clean_xcoms = MySqlOperator(
    task_id='clean_xcoms',
    mysql_conn_id='airflow_db',
    sql="delete from xcom where dag_id='{{ dag.dag_id }}'",
    dag=dag)


# Ideally we'd use BranchPythonOperator() here instead of PythonOperator so that if our
# query returns fewer results than we have buckets, we don't try to run them all.
# Unfortunately I couldn't get BranchPythonOperator to take a list of results like the
# documentation says it should (Airflow 1.10.2). So we call all the bucket tasks for now.
get_orders_task = PythonOperator(
                                 task_id='get_orders',
                                 python_callable=getOpenOrders,
                                 provide_context=True,
                                 dag=dag
                                )
get_orders_task.set_upstream(clean_xcoms)

# set up the parallel tasks -- these are configured at compile time, not at run time:
for bucketNumber in range(0, totalBuckets):
    taskBucket = createOrderProcessingTask(bucketNumber)
    taskBucket.set_upstream(get_orders_task)


###################################################################################################

Bir görevin sonucunda anında alt etiketler oluşturmanın mümkün olabileceğini unutmayın, ancak bulduğum alt etiketlerdeki belgelerin çoğu, çözdüğünden daha fazla soruna neden olduğu için bu özellikten uzak durmanızı şiddetle tavsiye eder. çoğu durumda. Alt etiketlerin yakında yerleşik bir özellik olarak kaldırılabileceğine dair öneriler gördüm.
çürümüş

Ayrıca, for tasks in tasksörneğimdeki döngüde üzerinde yinelediğim nesneyi sildiğime dikkat edin. Bu kötü bir fikir. Bunun yerine anahtarların bir listesini alın ve bunu yineleyin veya silme işlemlerini atlayın. Benzer şekilde, xcom_pull None döndürürse (liste veya boş liste yerine) for döngüsü de başarısız olur. Xcom_pull'u 'for'dan önce çalıştırmak ve ardından Yok olup olmadığını kontrol etmek - veya en azından boş bir liste olduğundan emin olmak isteyebilir. YMMV. İyi şanslar!
çürümüş

1
içinde ne var open_order_task?
alltej

Haklısın, bu benim örneğimde bir yazım hatası. Get_orders_task.set_upstream () olmalıdır. Ben düzelteceğim.
çürümüş

0

Sorunun ne olduğunu anlamadın mı?

İşte standart bir örnek. Şimdi eğer işlevde alt etiket ile değiştirirseniz for i in range(5):, for i in range(random.randint(0, 10)):o zaman her şey çalışacaktır. Şimdi 'start' operatörünün verileri bir dosyaya koyduğunu ve rastgele bir değer yerine fonksiyonun bu verileri okuyacağını hayal edin. Ardından operatör 'başlat' görev sayısını etkileyecektir.

Alt etiket girilirken, görev sayısı o anda dosyadan / veritabanından / XCom'dan okunan son sayıya eşit olacağından, sorun yalnızca kullanıcı arayüzündeki ekranda olacaktır. Bu, aynı anda bir dagin birkaç fırlatılmasına otomatik olarak bir kısıtlama getirir.


-1

Bu soruya çok benzeyen bu Medium gönderisini buldum . Ancak yazım hatalarıyla dolu ve uygulamayı denediğimde çalışmıyor.

Yukarıdakilere cevabım şu şekildedir:

Dinamik olarak görevler oluşturuyorsanız, bunu bir yukarı akış görevi tarafından yaratılmayan veya bu görevden bağımsız olarak tanımlanabilen bir şeyi yineleyerek yapmanız gerekir . Daha önce birçok kişinin işaret ettiği gibi, yürütme tarihlerini veya diğer hava akışı değişkenlerini şablon dışındaki bir şeye (örneğin bir görev) geçiremeyeceğinizi öğrendim. Ayrıca bu gönderiye bakın .


Yorumuma bakarsanız, yukarı akış görevlerinin sonucuna göre görevler oluşturmanın aslında mümkün olduğunu göreceksiniz.
Christopher Beck
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.