วิธีที่เหมาะสมในการสร้างเวิร์กโฟลว์แบบไดนามิกใน Airflow


98

ปัญหา

มีวิธีใดบ้างใน Airflow ในการสร้างเวิร์กโฟลว์ที่ไม่ทราบจำนวนงาน B * จนกว่างาน A จะเสร็จ ฉันได้ดูแท็กย่อยแล้ว แต่ดูเหมือนว่าจะสามารถทำงานได้เฉพาะกับชุดงานคงที่ซึ่งต้องถูกกำหนดที่การสร้าง Dag

Dag จะเรียกใช้งานได้หรือไม่? และหากเป็นเช่นนั้นโปรดยกตัวอย่าง

ฉันมีปัญหาที่ไม่สามารถทราบจำนวนงาน B ที่จะต้องใช้ในการคำนวณงาน C จนกว่างาน A จะเสร็จสมบูรณ์ แต่ละงาน B. * จะใช้เวลาหลายชั่วโมงในการคำนวณและไม่สามารถรวมกันได้

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

แนวคิด # 1

ฉันไม่ชอบโซลูชันนี้เพราะฉันต้องสร้าง ExternalTaskSensor ที่ปิดกั้นและงาน B. * ทั้งหมดจะใช้เวลา 2-24 ชั่วโมงจึงจะเสร็จสมบูรณ์ ดังนั้นฉันไม่คิดว่านี่เป็นวิธีแก้ปัญหาที่เป็นไปได้ มีวิธีที่ง่ายกว่านี้แน่ ๆ ? หรือ Airflow ไม่ได้ออกแบบมาเพื่อสิ่งนี้?

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 --|

แก้ไข 1:

ตอนนี้คำถามนี้ยังคงไม่ได้มีคำตอบที่ดี ฉันได้รับการติดต่อจากคนหลายคนที่กำลังมองหาวิธีแก้ปัญหา


งานทั้งหมด B * คล้ายกันหรือไม่เพราะสามารถสร้างแบบวนซ้ำได้
Daniel Lee

ใช่งาน B. * ทั้งหมดสามารถสร้างแบบวนซ้ำได้อย่างรวดเร็วเมื่องาน A เสร็จสิ้น งาน A ใช้เวลาประมาณ 2 ชั่วโมงในการดำเนินการ
costrouc

คุณพบวิธีแก้ปัญหาหรือไม่? คุณช่วยโพสต์ได้ไหม
Daniel Dubovski

3
แหล่งข้อมูลที่มีประโยชน์สำหรับ Idea # 1: linkedin.com/pulse/…
Juan Riaza

1
นี่คือบทความที่ฉันเขียนอธิบายวิธีการทำlinkedin.com/pulse/dynamic-workflows-airflow-kyle-bridenstine
Kyle Bridenstine

คำตอบ:


33

นี่คือวิธีที่ฉันทำกับคำขอที่คล้ายกันโดยไม่มีแท็กย่อย:

ขั้นแรกให้สร้างวิธีการที่ส่งคืนค่าที่คุณต้องการ

def values_function():
     return values

สร้างวิธีการถัดไปที่จะสร้างงานแบบไดนามิก:

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)

จากนั้นรวมเข้าด้วยกัน:

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

ค่ากำหนดไว้ที่ไหน?
พระสงฆ์

11
แทนที่จะผมจะคาดหวังสิ่งที่ต้องการfor i in values_function() for i in push_func_outputปัญหาคือฉันหาวิธีรับเอาต์พุตแบบไดนามิกไม่ได้ เอาต์พุตของ PythonOperator จะอยู่ใน Xcom หลังจากการเรียกใช้งาน แต่ฉันไม่รู้ว่าสามารถอ้างอิงจากนิยาม DAG ได้หรือไม่
เอ

@Ena คุณพบวิธีที่จะบรรลุสิ่งนั้นหรือไม่?
eldos

1
@eldos ดูคำตอบของฉันด้านล่าง
Ena

1
จะเกิดอะไรขึ้นถ้าเราต้องทำชุดของขั้นตอนที่ขึ้นอยู่กับภายในลูป? จะมีห่วงโซ่การพึ่งพาที่สองภายในgroupฟังก์ชันหรือไม่?
CodingInCircles

12

ฉันได้หาวิธีสร้างเวิร์กโฟลว์ตามผลลัพธ์ของงานก่อนหน้านี้แล้ว
โดยทั่วไปสิ่งที่คุณต้องการทำคือมีสองแท็กย่อยดังต่อไปนี้:

  1. Xcom พุชรายการ (หรือสิ่งที่คุณต้องสร้างเวิร์กโฟลว์แบบไดนามิกในภายหลัง) ในแท็กย่อยที่ได้รับการดำเนินการก่อน (ดู test1.py def return_list())
  2. ส่งผ่านวัตถุ dag หลักเป็นพารามิเตอร์ไปยัง subdag ที่สองของคุณ
  3. ตอนนี้หากคุณมีวัตถุ dag หลักคุณสามารถใช้เพื่อรับรายการอินสแตนซ์งานของมันได้ จากรายการอินสแตนซ์งานดังกล่าวคุณสามารถกรองงานของการรันปัจจุบันโดยใช้parent_dag.get_task_instances(settings.Session, start_date=parent_dag.get_active_runs()[-1])[-1]) อาจเพิ่มตัวกรองเพิ่มเติมได้ที่นี่
  4. ด้วยอินสแตนซ์งานนั้นคุณสามารถใช้ xcom pull เพื่อรับค่าที่คุณต้องการโดยระบุ dag_id ให้กับหนึ่งใน subdag แรก: dag_id='%s.%s' % (parent_dag_name, 'test1')
  5. ใช้รายการ / ค่าเพื่อสร้างงานของคุณแบบไดนามิก

ตอนนี้ฉันได้ทดสอบสิ่งนี้ในการติดตั้งการไหลเวียนของอากาศในพื้นที่ของฉันแล้วและใช้งานได้ดี ฉันไม่รู้ว่าส่วนดึง xcom จะมีปัญหาหรือไม่หากมีมากกว่าหนึ่งอินสแตนซ์ของ dag ที่ทำงานในเวลาเดียวกัน แต่คุณอาจใช้คีย์เฉพาะหรืออะไรทำนองนั้นเพื่อระบุ xcom โดยไม่ซ้ำกัน คุณค่าที่คุณต้องการ หนึ่งอาจเพิ่มประสิทธิภาพขั้นตอนที่ 3 เพื่อให้แน่ใจ 100% ว่าจะได้งานเฉพาะของ dag หลักในปัจจุบัน แต่สำหรับการใช้งานของฉันสิ่งนี้ทำได้ดีพอฉันคิดว่าต้องใช้วัตถุ task_instance เพียงชิ้นเดียวเพื่อใช้ xcom_pull

นอกจากนี้ฉันทำความสะอาด xcoms สำหรับ subdag แรกก่อนการดำเนินการทุกครั้งเพื่อให้แน่ใจว่าฉันไม่ได้รับค่าผิดพลาดโดยไม่ได้ตั้งใจ

ฉันค่อนข้างแย่ที่จะอธิบายดังนั้นฉันหวังว่ารหัสต่อไปนี้จะทำให้ทุกอย่างชัดเจน:

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

และขั้นตอนการทำงานหลัก:

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 สิ่งเหล่านี้ไม่โหลดเมื่อเพิ่มลงในโฟลเดอร์ DAG ฉันขาดอะไรไปหรือไม่?
Anthony Keane

@AnthonyKeane คุณใส่ test1.py และ test2.py ลงในโฟลเดอร์ชื่อ subdags ในโฟลเดอร์ dag ของคุณหรือไม่?
Christopher Beck

ฉันใช่ คัดลอกทั้งสองไฟล์ไปยังแท็กย่อยและวาง test.py ในโฟลเดอร์ dag ยังคงได้รับข้อผิดพลาดนี้ DAG เสีย: [/home/airflow/gcs/dags/test.py] ไม่มีโมดูลชื่อ subdags.test1 หมายเหตุฉันใช้ Google Cloud Composer (Airflow ที่มีการจัดการของ Google 1.9.0)
Anthony Keane

@AnthonyKeane นี่เป็นข้อผิดพลาดเดียวที่คุณเห็นในบันทึกใช่หรือไม่ DAG เสียอาจเกิดจากการที่ subdag มีข้อผิดพลาดในการคอมไพล์
Christopher Beck

3
สวัสดี @Christopher Beck ฉันพบข้อผิดพลาดของฉันฉันต้องการเพิ่มลง_ _init_ _.pyในโฟลเดอร์ subdags ข้อผิดพลาดมือใหม่
Anthony Keane

11

ใช่เป็นไปได้ฉันได้สร้างตัวอย่าง DAG ที่แสดงให้เห็นถึงสิ่งนี้

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 ให้สร้างตัวแปร Airflow ทั้งสามนี้

airflow variables --set DynamicWorkflow_Group1 1

airflow variables --set DynamicWorkflow_Group2 0

airflow variables --set DynamicWorkflow_Group3 0

คุณจะเห็นว่า DAG มาจากสิ่งนี้

ป้อนคำอธิบายภาพที่นี่

เพื่อสิ่งนี้หลังจากที่รัน

ป้อนคำอธิบายภาพที่นี่

คุณสามารถดูข้อมูลเพิ่มเติมเกี่ยวกับ DAG นี้ในบทความของฉันเกี่ยวกับการสร้างแบบไดนามิกในเวิร์กโฟลว์ไหลเวียนของอากาศ


1
แต่จะเกิดอะไรขึ้นถ้าคุณมี DagRun หลายตัวของ DAG นี้ พวกเขาแบ่งปันตัวแปรเดียวกันหรือไม่?
มี.ค.

1
ใช่พวกเขาจะใช้ตัวแปรเดียวกัน ฉันกล่าวถึงสิ่งนี้ในบทความของฉันในตอนท้าย คุณจะต้องสร้างตัวแปรแบบไดนามิกและใช้ dag run id ในชื่อตัวแปร ตัวอย่างของฉันง่าย ๆ เพียงเพื่อแสดงให้เห็นถึงความเป็นไปได้แบบไดนามิก แต่คุณจะต้องทำให้ได้คุณภาพการผลิต :)
Kyle Bridenstine

บริดจ์จำเป็นไหมเมื่อสร้างงานไดนามิก จะอ่านบทความของคุณอย่างเต็มที่ในไม่ช้า แต่ต้องการถาม ฉันกำลังดิ้นรนกับการสร้างงานแบบไดนามิกโดยอาศัยงานต้นน้ำในตอนนี้และฉันกำลังเริ่มคิดว่าฉันผิดพลาดตรงไหน ปัญหาปัจจุบันของฉันคือด้วยเหตุผลบางประการฉันไม่สามารถรับ DAG เพื่อซิงค์กับ DAG-Bag ได้ DAG ของฉันซิงค์เมื่อฉันใช้รายการแบบคงที่ในโมดูล แต่หยุดลงเมื่อฉันเปลี่ยนรายการคงที่ออกเพื่อสร้างจากงานต้นน้ำ
lucid_goose

นี่ฉลาดมาก
jvans

1
@jvans ขอบคุณที่ฉลาด แต่น่าจะไม่ใช่คุณภาพการผลิต
Kyle Bridenstine

6

OA: "มีวิธีใดบ้างใน Airflow ที่จะสร้างเวิร์กโฟลว์ที่ไม่ทราบจำนวนงาน B * จนกว่างาน A จะเสร็จสิ้น"

คำตอบสั้น ๆ คือไม่ Airflow จะสร้างการไหลของ DAG ก่อนที่จะเริ่มทำงาน

นั่นบอกว่าเราได้ข้อสรุปง่ายๆนั่นคือเราไม่มีความจำเป็นเช่นนั้น เมื่อคุณต้องการขนานงานบางอย่างคุณควรประเมินทรัพยากรที่คุณมีไม่ใช่จำนวนรายการที่ต้องดำเนินการ

เราทำเช่นนี้: เราสร้างงานตามจำนวนที่กำหนดแบบไดนามิกเช่น 10 ซึ่งจะแบ่งงานออก ตัวอย่างเช่นหากเราต้องการประมวลผล 100 ไฟล์แต่ละงานจะประมวลผล 10 ไฟล์ ฉันจะโพสต์รหัสในภายหลังวันนี้

อัปเดต

นี่คือรหัสขออภัยในความล่าช้า

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

คำอธิบายรหัส:

ที่นี่เรามีงานเริ่มต้นเพียงงานเดียวและงานปลายเดียว (ทั้งแบบจำลอง)

จากนั้นเริ่มงานด้วย for loop เราสร้างงาน 10 งานด้วย python ที่เรียกได้เดียวกัน งานถูกสร้างขึ้นในฟังก์ชัน create_dynamic_task

สำหรับแต่ละ python ที่เรียกได้เราจะส่งผ่านอาร์กิวเมนต์จำนวนงานขนานทั้งหมดและดัชนีงานปัจจุบัน

สมมติว่าคุณมี 1,000 รายการที่ต้องทำอย่างละเอียด: งานแรกจะได้รับการป้อนข้อมูลว่าควรทำชิ้นส่วนแรกให้ละเอียดจาก 10 ชิ้น มันจะแบ่งรายการ 1,000 รายการออกเป็น 10 ชิ้นและทำรายการแรกอย่างละเอียด


1
นี่เป็นทางออกที่ดีตราบเท่าที่คุณไม่ต้องการงานที่เฉพาะเจาะจงต่อรายการ (เช่นความคืบหน้าผลลัพธ์ความสำเร็จ / ล้มเหลวการลองใหม่ ฯลฯ )
Alonzzo2

@Ena parallelTaskไม่ได้กำหนด: ฉันขาดอะไรไปหรือเปล่า?
Anthony Keane

2
@AnthonyKeane มันเป็นฟังก์ชัน python ที่คุณควรเรียกร้องให้ทำอะไรบางอย่างจริงๆ ตามที่แสดงความคิดเห็นไว้ในรหัสจะใช้เป็นป้อนจำนวนทั้งหมดและหมายเลขปัจจุบันเพื่ออธิบายองค์ประกอบทั้งหมดอย่างละเอียด
Ena

4

สิ่งที่ฉันคิดว่าคุณกำลังมองหาคือการสร้าง DAG แบบไดนามิกฉันพบสถานการณ์ประเภทนี้เมื่อไม่กี่วันที่ผ่านมาหลังจากการค้นหาฉันพบบล็อกนี้

การสร้างงานแบบไดนามิก

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

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

นี่คือลักษณะ DAG ของเราหลังจากใส่โค้ดเข้าด้วยกัน ป้อนคำอธิบายภาพที่นี่

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

มันช่วยได้มากความหวังมันจะช่วยคนอื่นด้วย


คุณประสบความสำเร็จด้วยตัวเองหรือไม่? ฉันเหนื่อย. แต่ฉันล้มเหลว
Newt

ใช่มันได้ผลสำหรับฉัน คุณกำลังเผชิญกับปัญหาอะไร
มูฮัมหมัดบินอาลี

1
ฉันเข้าใจแล้ว. ปัญหาของฉันได้รับการแก้ไขแล้ว ขอบคุณ. ฉันไม่เข้าใจวิธีที่ถูกต้องในการอ่านตัวแปรสภาพแวดล้อมในภาพนักเทียบท่า
Newt

1
จะเกิดอะไรขึ้นถ้ารายการตารางอาจมีการเปลี่ยนแปลงดังนั้นเราจึงไม่สามารถวางไว้ในไฟล์ yaml แบบคงที่ได้?
FrankZhu

มันขึ้นอยู่กับว่าคุณใช้มันอยู่ที่ไหน แม้ว่าฉันจะสนใจในสิ่งที่คุณแนะนำ @FrankZhu ควรทำอย่างไรจึงจะเหมาะสม?
มูฮัมหมัดบินอาลี

3

ฉันคิดว่าฉันได้พบทางออกที่ดีกว่านี้ที่https://github.com/mastak/airflow_multi_dagrunซึ่งใช้ enqueuing ที่เรียบง่ายของ DagRuns โดยเรียก dagruns หลายคล้ายกับTriggerDagRuns เครดิตส่วนใหญ่ไปที่https://github.com/mastakแม้ว่าฉันจะต้องแก้ไขรายละเอียดบางอย่างเพื่อให้ใช้งานได้กับกระแสลมล่าสุด

โซลูชันนี้ใช้ตัวดำเนินการแบบกำหนดเองที่ทริกเกอร์ DagRuns หลายตัว :

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()

จากนั้นคุณสามารถส่ง dagruns หลายตัวจากฟังก์ชันที่เรียกได้ใน PythonOperator ของคุณตัวอย่างเช่น:

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


3

กราฟงานไม่ถูกสร้างขึ้นในขณะทำงาน แต่กราฟจะถูกสร้างขึ้นเมื่อ Airflow หยิบขึ้นมาจากโฟลเดอร์ dags ของคุณ ดังนั้นจึงเป็นไปไม่ได้เลยที่จะมีกราฟที่แตกต่างกันสำหรับงานทุกครั้งที่ทำงาน คุณสามารถกำหนดค่างานเพื่อสร้างกราฟตามแบบสอบถามในเวลาโหลด กราฟนั้นจะยังคงเหมือนเดิมทุกครั้งที่วิ่งหลังจากนั้นซึ่งอาจไม่มีประโยชน์มากนัก

คุณสามารถออกแบบกราฟที่ดำเนินการงานที่แตกต่างกันในทุกการรันโดยอิงตามผลลัพธ์ของคิวรีโดยใช้ตัวดำเนินการสาขา

สิ่งที่ฉันทำคือกำหนดค่าชุดของงานไว้ล่วงหน้าจากนั้นนำผลการสืบค้นข้อมูลและแจกจ่ายไปตามงานต่างๆ วิธีนี้น่าจะดีกว่าเพราะหากข้อความค้นหาของคุณส่งคืนผลลัพธ์เป็นจำนวนมากคุณอาจไม่ต้องการให้ตัวกำหนดตารางเวลาทำงานพร้อมกันจำนวนมาก เพื่อให้ปลอดภัยยิ่งขึ้นฉันยังใช้พูลเพื่อให้แน่ใจว่าการทำงานพร้อมกันของฉันจะไม่หลุดมือไปพร้อมกับข้อความค้นหาขนาดใหญ่ที่ไม่คาดคิด

"""
 - 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)


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

โปรดทราบว่าดูเหมือนว่าอาจเป็นไปได้ที่จะสร้างแท็กย่อยได้ทันทีเนื่องจากงานอย่างไรก็ตามเอกสารส่วนใหญ่ในแท็กย่อยที่ฉันพบขอแนะนำให้อยู่ห่างจากคุณลักษณะนั้นเนื่องจากจะทำให้เกิดปัญหามากกว่าที่จะแก้ไขได้ ในกรณีส่วนใหญ่. ฉันเห็นคำแนะนำว่าแท็กย่อยอาจถูกลบออกเป็นฟีเจอร์ในตัวเร็ว ๆ นี้
เน่า

โปรดทราบว่าในfor tasks in tasksลูปในตัวอย่างของฉันฉันลบวัตถุที่ฉันกำลังทำซ้ำ นั่นเป็นความคิดที่ไม่ดี รับรายการคีย์แทนและทำซ้ำตามนั้นหรือข้ามการลบ ในทำนองเดียวกันถ้า xcom_pull ส่งคืน None (แทนที่จะเป็นรายการหรือรายการว่าง) ที่สำหรับลูปก็ล้มเหลวเช่นกัน อาจต้องการเรียกใช้ xcom_pull ก่อนหน้า 'for' จากนั้นตรวจสอบว่าไม่มี - หรือตรวจสอบให้แน่ใจว่ามีรายการว่างอยู่ที่นั่นเป็นอย่างน้อย YMMV. โชคดี!
เน่า

1
อะไรอยู่ในopen_order_task?
alltej

คุณพูดถูกนั่นคือการพิมพ์ผิดในตัวอย่างของฉัน ควรเป็น get_orders_task.set_upstream () ฉันจะแก้ไขมัน
เน่า

0

ไม่เข้าใจว่าปัญหาคืออะไร?

นี่คือตัวอย่างมาตรฐาน ตอนนี้ถ้าในsubdag ของฟังก์ชันแทนที่for i in range(5):ด้วยfor i in range(random.randint(0, 10)):ทุกอย่างจะทำงาน ลองนึกภาพว่าโอเปอเรเตอร์ 'start' ใส่ข้อมูลในไฟล์และแทนที่จะเป็นค่าสุ่มฟังก์ชันจะอ่านข้อมูลนี้ จากนั้นตัวดำเนินการ 'start' จะส่งผลต่อจำนวนงาน

ปัญหาจะอยู่ในการแสดงผลใน UI เท่านั้นเนื่องจากเมื่อเข้าสู่ subdag จำนวนงานจะเท่ากับการอ่านล่าสุดจากไฟล์ / ฐานข้อมูล / XCom ในขณะนี้ ซึ่งจะให้ข้อ จำกัด โดยอัตโนมัติสำหรับการเปิดใช้งานเดกเดียวหลายครั้งในคราวเดียว


-1

ฉันพบโพสต์สื่อนี้ซึ่งคล้ายกับคำถามนี้มาก อย่างไรก็ตามมันเต็มไปด้วยการพิมพ์ผิดและใช้งานไม่ได้เมื่อฉันลองใช้มัน

คำตอบของฉันสำหรับข้างต้นมีดังนี้:

หากคุณกำลังสร้างงานแบบไดนามิกคุณต้องทำเช่นนั้นโดยการทำซ้ำบนสิ่งที่ไม่ได้สร้างขึ้นโดยงานต้นน้ำหรือสามารถกำหนดได้โดยไม่ขึ้นกับงานนั้น ฉันได้เรียนรู้ว่าคุณไม่สามารถส่งต่อวันที่ดำเนินการหรือตัวแปรการไหลของอากาศอื่น ๆ ไปยังบางสิ่งที่อยู่นอกเทมเพลต (เช่นงาน) ได้อย่างที่คนอื่น ๆ กล่าวไว้ก่อนหน้านี้ ดูโพสต์นี้ด้วย


หากคุณดูความคิดเห็นของฉันคุณจะเห็นว่าเป็นไปได้จริงที่จะสร้างงานตามผลลัพธ์ของงานต้นน้ำ
Christopher Beck
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.