Apache Airflow 2.10 Data Pipeline: 5 DAG Orchestration Patterns for Production ETL

AI与大数据

ETL Scripts Out of Control, Task Dependencies in Chaos, Silent Pipeline Failures

At 2 AM, the data warehouse daily increment drops from 500K to 30K, yet Airflow DAG shows all green — no one knows the upstream API changed its pagination logic. ETL scripts are scattered across 20 Cron Jobs, upstream failures go unnoticed downstream, and dirty data flows straight into reports. A new colleague writes a DAG with circular dependencies that freezes the Airflow Scheduler. In 2026, Apache Airflow 2.10 brings enhanced Dynamic Task Mapping, improved TaskFlow API, and more powerful data-aware scheduling — from DAG design to production monitoring, one system handles it all.

This article covers 5 DAG orchestration patterns, guiding you through TaskFlow API design → Dynamic Task Mapping for parallel ETL → Custom Operators and Sensors → XCom data passing and branching → Error handling, retry, and SLA monitoring with complete runnable Python code at every step.


Airflow Core Concepts

Concept Description
DAG Directed Acyclic Graph, defines task dependencies and execution order
Task Single execution unit in a DAG, corresponds to an Operator instance
Operator Task execution logic encapsulation (PythonOperator, BashOperator, custom)
TaskFlow API Python functional DAG definition (Airflow 2.0+), uses @task decorator
XCom Cross-task data passing, default 48KB limit, 2.10 supports custom Backend
Dynamic Task Mapping Dynamic task expansion (Airflow 2.3+), generates parallel tasks from runtime data
Sensor Special Operator that checks conditions before downstream tasks start
Connection Airflow-managed connection info, avoids hardcoding passwords in DAGs
Pool Resource pool, limits concurrent task count
Trigger Async trigger (Airflow 2.2+), frees Worker Slots from Sensors
SLA Service Level Agreement, timeout triggers alerts
Dataset Data-aware scheduling (Airflow 2.4+), auto-triggers consumers

Problem Analysis: 5 Challenges in Data Pipeline Orchestration

  1. DAG design complexity out of control: ETL tasks grow from 3 to 30, DAG files become spaghetti code
  2. Manual parallel ETL orchestration: Processing 100 data sources daily, need Dynamic Task Mapping
  3. Custom logic difficult to reuse: Internal API calls and validation logic repeated in every DAG
  4. Cross-task data passing limited: XCom 48KB limit, branching logic is chaotic
  5. Insufficient production reliability: No retry, no SLA alerts, data quality issues masked

Step-by-Step: 5 Airflow DAG Orchestration Patterns

Pattern 1: DAG Design with TaskFlow API

Traditional Operator syntax is verbose and unintuitive. TaskFlow API uses Python functional style to dramatically simplify DAG definitions.

# dags/taskflow_etl_dag.py
from datetime import datetime, timedelta
from airflow.decorators import dag, task

default_args = {
    "owner": "data-team",
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
}

@dag(
    dag_id="taskflow_etl_pipeline",
    default_args=default_args,
    description="TaskFlow API ETL pipeline",
    schedule="0 2 * * *",
    start_date=datetime(2026, 1, 1),
    catchup=False,
    tags=["etl", "taskflow"],
)
def taskflow_etl_pipeline():

    @task
    def extract_user_events(execution_date: str) -> dict:
        """从PostgreSQL抽取用户事件数据"""
        import psycopg2
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection("postgres_source")
        pg_conn = psycopg2.connect(
            host=conn.host, port=conn.port, database=conn.schema,
            user=conn.login, password=conn.password,
        )
        cur = pg_conn.cursor()
        cur.execute(
            "SELECT user_id, event_type, amount, created_at "
            "FROM user_events "
            "WHERE created_at >= %s AND created_at < %s",
            (execution_date, execution_date + timedelta(days=1)),
        )
        rows = cur.fetchall()
        pg_conn.close()
        return {"row_count": len(rows), "data": [dict(zip(["user_id", "event_type", "amount", "created_at"], r)) for r in rows[:1000]]}

    @task
    def extract_api_data(execution_date: str) -> dict:
        """从REST API抽取数据"""
        import requests
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection("api_source")
        resp = requests.get(
            f"{conn.host}/v2/data",
            headers={"Authorization": f"Bearer {conn.password}"},
            params={"date": execution_date},
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        return {"row_count": len(data.get("records", [])), "data": data.get("records", [])[:1000]}

    @task
    def transform(user_events: dict, api_data: dict) -> dict:
        """合并、去重、清洗数据"""
        import pandas as pd
        df_events = pd.DataFrame(user_events["data"])
        df_api = pd.DataFrame(api_data["data"])
        if df_events.empty and df_api.empty:
            return {"row_count": 0, "data": []}
        df_all = pd.concat([df_events, df_api], ignore_index=True)
        df_all = df_all.drop_duplicates(subset=["user_id", "event_type"])
        df_all = df_all.dropna(subset=["user_id"])
        df_all["amount"] = df_all["amount"].fillna(0).astype(float)
        return {"row_count": len(df_all), "data": df_all.to_dict("records")[:500]}

    @task
    def load(transformed: dict) -> int:
        """加载数据到数据仓库"""
        import psycopg2
        from airflow.hooks.base import BaseHook
        if transformed["row_count"] == 0:
            return 0
        conn = BaseHook.get_connection("postgres_warehouse")
        pg_conn = psycopg2.connect(
            host=conn.host, port=conn.port, database=conn.schema,
            user=conn.login, password=conn.password,
        )
        cur = pg_conn.cursor()
        insert_sql = """
            INSERT INTO analytics.user_events_daily
            (user_id, event_type, amount, event_date)
            VALUES (%s, %s, %s, %s)
            ON CONFLICT (user_id, event_type, event_date) DO UPDATE
            SET amount = EXCLUDED.amount
        """
        batch = [(r.get("user_id"), r.get("event_type"), r.get("amount", 0), r.get("created_at")) for r in transformed["data"]]
        cur.executemany(insert_sql, batch)
        pg_conn.commit()
        pg_conn.close()
        return len(batch)

    @task
    def quality_check(loaded_count: int, transformed_count: int) -> str:
        if transformed_count > 0 and loaded_count / transformed_count < 0.95:
            raise ValueError(f"Quality gate failed: loaded {loaded_count}/{transformed_count}")
        return "PASSED"

    user_events = extract_user_events("{{ ds }}")
    api_data = extract_api_data("{{ ds }}")
    transformed = transform(user_events, api_data)
    loaded = load(transformed)
    quality_check(loaded, transformed["row_count"])

taskflow_etl_pipeline()
pip install "apache-airflow[postgres,amazon]==2.10.3"
airflow db init
airflow dags list | grep taskflow
airflow dags test taskflow_etl_pipeline 2026-06-19

Pattern 2: Dynamic Task Mapping for Parallel ETL

When the number of data sources changes dynamically, Dynamic Task Mapping expands a single Task definition into N parallel instances.

# dags/dynamic_mapping_dag.py
from datetime import datetime
from airflow.decorators import dag, task

@dag(dag_id="dynamic_mapping_etl", schedule="0 3 * * *", start_date=datetime(2026, 1, 1), catchup=False, tags=["etl", "dynamic-mapping"])
def dynamic_mapping_etl():

    @task
    def discover_data_sources() -> list[dict]:
        """动态发现需要处理的数据源列表"""
        import psycopg2
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection("postgres_source")
        pg_conn = psycopg2.connect(host=conn.host, port=conn.port, database=conn.schema, user=conn.login, password=conn.password)
        cur = pg_conn.cursor()
        cur.execute("SELECT source_id, source_type, connection_id FROM data_sources WHERE active = true")
        sources = [{"source_id": row[0], "source_type": row[1], "connection_id": row[2]} for row in cur.fetchall()]
        pg_conn.close()
        return sources

    @task
    def extract_from_source(source: dict) -> dict:
        """从单个数据源抽取数据"""
        import psycopg2, requests
        from airflow.hooks.base import BaseHook
        source_type, source_id = source["source_type"], source["source_id"]
        if source_type == "postgres":
            conn = BaseHook.get_connection(source["connection_id"])
            pg_conn = psycopg2.connect(host=conn.host, port=conn.port, database=conn.schema, user=conn.login, password=conn.password)
            cur = pg_conn.cursor()
            cur.execute(f"SELECT * FROM source_{source_id}_data WHERE processed = false LIMIT 5000")
            rows = cur.fetchall()
            pg_conn.close()
            return {"source_id": source_id, "row_count": len(rows), "status": "success"}
        elif source_type == "api":
            conn = BaseHook.get_connection(source["connection_id"])
            resp = requests.get(f"{conn.host}/v1/data/{source_id}", headers={"Authorization": f"Bearer {conn.password}"}, timeout=30)
            resp.raise_for_status()
            return {"source_id": source_id, "row_count": len(resp.json()), "status": "success"}
        return {"source_id": source_id, "row_count": 0, "status": "skipped"}

    @task
    def validate_extracted(extract_result: dict) -> dict:
        if extract_result["status"] != "success" or extract_result["row_count"] < 1:
            return {**extract_result, "valid": False}
        return {**extract_result, "valid": True}

    @task
    def aggregate_results(validation_results: list[dict]) -> dict:
        total = len(validation_results)
        valid = sum(1 for r in validation_results if r.get("valid"))
        total_rows = sum(r.get("row_count", 0) for r in validation_results if r.get("valid"))
        return {"total_sources": total, "valid_sources": valid, "total_rows": total_rows}

    # Dynamic Task Mapping: expand()将列表展开为并行任务
    sources = discover_data_sources()
    extract_results = extract_from_source.expand(source=sources)
    validation_results = validate_extracted.expand(extract_result=extract_results)
    aggregate_results(validation_results)

dynamic_mapping_etl()
# plugins/operators/internal_api_operator.py
from airflow.models import BaseOperator
from airflow.exceptions import AirflowSkipException
from typing import Any
import requests

class InternalApiOperator(BaseOperator):
    """调用公司内部API的自定义Operator"""
    template_fields = ("endpoint", "payload")

    def __init__(self, endpoint: str, payload: dict | None = None, method: str = "POST",
                 connection_id: str = "internal_api", timeout: int = 60, skip_on_empty: bool = False, **kwargs):
        super().__init__(**kwargs)
        self.endpoint, self.payload, self.method = endpoint, payload or {}, method
        self.connection_id, self.timeout, self.skip_on_empty = connection_id, timeout, skip_on_empty

    def execute(self, context: Any) -> dict:
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection(self.connection_id)
        url = f"{conn.host}{self.endpoint}"
        headers = {"Authorization": f"Bearer {conn.password}", "Content-Type": "application/json"}
        self.log.info(f"Calling {self.method} {url}")
        try:
            if self.method.upper() == "GET":
                resp = requests.get(url, headers=headers, params=self.payload, timeout=self.timeout)
            else:
                resp = requests.post(url, headers=headers, json=self.payload, timeout=self.timeout)
            resp.raise_for_status()
            result = resp.json()
            if self.skip_on_empty and not result:
                raise AirflowSkipException("API returned empty result")
            return result
        except requests.exceptions.Timeout:
            raise TimeoutError(f"API call timed out after {self.timeout}s")
        except requests.exceptions.HTTPError as e:/n            if e.response.status_code == 404 and self.skip_on_empty:
                raise AirflowSkipException(f"Resource not found: {url}")
            raise

# plugins/sensors/data_ready_sensor.py
from airflow.sensors.base import BaseSensorOperator
from typing import Any

class DataReadySensor(BaseSensorOperator):
    """检测数据源是否已就绪的Sensor"""
    template_fields = ("source_table", "expected_date")

    def __init__(self, source_table: str, expected_date: str, connection_id: str = "postgres_source",
                 min_row_count: int = 1, poke_interval: int = 300, timeout: int = 7200, **kwargs):
        super().__init__(poke_interval=poke_interval, timeout=timeout, **kwargs)
        self.source_table, self.expected_date = source_table, expected_date
        self.connection_id, self.min_row_count = connection_id, min_row_count

    def poke(self, context: Any) -> bool:
        import psycopg2
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection(self.connection_id)
        pg_conn = psycopg2.connect(host=conn.host, port=conn.port, database=conn.schema, user=conn.login, password=conn.password)
        cur = pg_conn.cursor()
        cur.execute(f"SELECT COUNT(*) FROM {self.source_table} WHERE partition_date = %s AND status = 'ready'", (self.expected_date,))
        count = cur.fetchone()[0]
        pg_conn.close()
        self.log.info(f"Data ready check: {count} rows (min: {self.min_row_count})")
        return count >= self.min_row_count

Pattern 3: Custom Operators and Sensors

Encapsulate frequently used internal system logic as custom Operators and Sensors for reuse.

# dags/xcom_branching_dag.py
from datetime import datetime
from airflow.decorators import dag, task
from airflow.operators.python import BranchPythonOperator

@dag(dag_id="xcom_branching_pipeline", schedule="0 5 * * *", start_date=datetime(2026, 1, 1), catchup=False, tags=["etl", "xcom", "branching"])
def xcom_branching_pipeline():

    @task
    def detect_data_type() -> dict:
        import psycopg2
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection("postgres_source")
        pg_conn = psycopg2.connect(host=conn.host, port=conn.port, database=conn.schema, user=conn.login, password=conn.password)
        cur = pg_conn.cursor()
        cur.execute("SELECT COUNT(*), SUM(amount) FROM user_events WHERE created_at >= CURRENT_DATE - INTERVAL '1 day'")
        row_count, total_amount = cur.fetchone()
        pg_conn.close()
        return {"row_count": row_count, "total_amount": float(total_amount or 0),
                "data_size": "large" if row_count > 100000 else "small"}

    @task
    def process_small_batch(metadata: dict) -> dict:
        return {"path": "small_batch", "processed": metadata["row_count"], "status": "completed"}

    @task
    def process_large_batch(metadata: dict) -> dict:
        chunk_size = 10000
        chunks = (metadata["row_count"] + chunk_size - 1) // chunk_size
        return {"path": "large_batch", "processed": metadata["row_count"], "chunks": chunks, "status": "completed"}

    def choose_processing_branch(**context):
        ti = context["ti"]
        metadata = ti.xcom_pull(task_ids="detect_data_type")
        return "process_large_batch" if metadata["data_size"] == "large" else "process_small_batch"

    metadata = detect_data_type()
    branch = BranchPythonOperator(task_id="choose_branch", python_callable=choose_processing_branch)
    small_result = process_small_batch(metadata)
    large_result = process_large_batch(metadata)
    metadata >> branch >> [small_result, large_result]

xcom_branching_pipeline()
# custom_xcom_backend.py - 自定义XCom Backend突破48KB限制
import json, hashlib
from airflow.models.xcom import BaseXCom
from typing import Any

class RedisXComBackend(BaseXCom):
    @staticmethod
    def serialize_value(value: Any) -> bytes:
        import redis
        r = redis.Redis(host="redis-host", port=6379, db=2)
        serialized = json.dumps(value, default=str).encode("utf-8")
        if len(serialized) <= 48000:
            return BaseXCom.serialize_value(value)
        key = f"xcom:{hashlib.md5(serialized).hexdigest()}"
        r.setex(key, 86400 * 7, serialized)
        return BaseXCom.serialize_value({"__xcom_redis_key": key, "size": len(serialized)})

    @staticmethod
    def deserialize_value(result: Any) -> Any:
        import redis
        value = BaseXCom.deserialize_value(result)
        if isinstance(value, dict) and "__xcom_redis_key" in value:
            r = redis.Redis(host="redis-host", port=6379, db=2)
            data = r.get(value["__xcom_redis_key"])
            if data: return json.loads(data)
            raise ValueError(f"XCom data expired in Redis")
        return value

# airflow.cfg: xcom_backend = custom_xcom_backend.RedisXComBackend

Pattern 4: XCom Data Passing and Pipeline Branching

XCom is the core mechanism for cross-task communication in Airflow. Version 2.10 supports custom XCom Backend to break the 48KB limit.

# dags/error_handling_sla_dag.py
from datetime import datetime, timedelta
from airflow.decorators import dag, task
from airflow.operators.python import PythonOperator
from airflow.exceptions import AirflowSkipException, AirflowFailOperator

default_args = {
    "owner": "data-team", "retries": 3, "retry_delay": timedelta(minutes=5),
    "retry_exponential_backoff": True, "max_retry_delay": timedelta(minutes=30),
    "execution_timeout": timedelta(minutes=60),
    "email_on_failure": True, "email": ["data-team@company.com"],
}

@dag(dag_id="error_handling_sla_pipeline", default_args=default_args,
     schedule="0 1 * * *", start_date=datetime(2026, 1, 1), catchup=False, tags=["etl", "production", "sla"])
def error_handling_sla_pipeline():

    @task(retries=5, retry_delay=timedelta(minutes=2), retry_exponential_backoff=True, max_retry_delay=timedelta(minutes=20))
    def extract_with_retry(**context) -> dict:
        import requests
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection("api_source")
        try:
            resp = requests.get(f"{conn.host}/v2/data", headers={"Authorization": f"Bearer {conn.password}"}, params={"date": context["ds"]}, timeout=30)
            resp.raise_for_status()
            return {"row_count": len(resp.json().get("records", [])), "status": "success"}
        except requests.exceptions.ConnectionError as e:/n            raise ConnectionError(f"Connection failed: {e}")
        except requests.exceptions.HTTPError as e:/n            if e.response.status_code == 429: raise ConnectionError(f"Rate limited: {e}")
            elif e.response.status_code >= 500: raise ConnectionError(f"Server error: {e}")
            elif e.response.status_code == 404: raise AirflowSkipException(f"Data not found")
            else: raise AirflowFailOperator(f"Client error: {e}")

    @task
    def transform_with_validation(raw_data: dict) -> dict:
        import pandas as pd
        if raw_data.get("status") != "success": raise ValueError("Upstream failed")
        df = pd.DataFrame(raw_data.get("records", []))
        if df.empty: raise AirflowSkipException("No data")
        null_rate = df.isnull().sum().sum() / (df.shape[0] * df.shape[1])
        if null_rate > 0.3: raise ValueError(f"Data quality too low: {null_rate:.1%}")
        return {"row_count": len(df), "null_rate": null_rate, "status": "transformed"}

    @task
    def load_with_idempotency(transformed: dict) -> dict:
        import psycopg2
        from airflow.hooks.base import BaseHook
        conn = BaseHook.get_connection("postgres_warehouse")
        pg_conn = psycopg2.connect(host=conn.host, port=conn.port, database=conn.schema, user=conn.login, password=conn.password)
        cur = pg_conn.cursor()
        cur.execute("DELETE FROM analytics.daily_metrics WHERE metric_date = CURRENT_DATE - INTERVAL '1 day'")
        cur.execute("INSERT INTO analytics.daily_metrics (metric_date, row_count, null_rate, status) VALUES (CURRENT_DATE - INTERVAL '1 day', %s, %s, %s)", (transformed["row_count"], transformed["null_rate"], transformed["status"]))
        pg_conn.commit(); pg_conn.close()
        return {"loaded": True, "inserted": 1}

    def sla_check(**context):
        import pendulum
        execution_date = context["execution_date"]
        if pendulum.now("UTC") > execution_date.add(hours=2):
            raise ValueError("SLA violated")
        return "SLA_OK"

    sla_check_task = PythonOperator(task_id="sla_check", python_callable=sla_check)
    sla_alert_task = PythonOperator(task_id="sla_alert", python_callable=lambda **c: print(f"SLA Alert: {c['dag'].dag_id}"), trigger_rule="one_failed")

    raw = extract_with_retry()
    transformed = transform_with_validation(raw)
    loaded = load_with_idempotency(transformed)
    loaded >> sla_check_task >> sla_alert_task

error_handling_sla_pipeline()
# Airflow 2.10数据感知调度
from airflow.datasets import Dataset

USER_EVENTS = Dataset("postgres://warehouse/analytics/user_events_daily")
ORDER_METRICS = Dataset("postgres://warehouse/analytics/order_metrics_daily")

@dag(schedule="0 2 * * *")
def producer():
    @task(outlets=[USER_EVENTS])
    def produce_user_events(): return "produced"
    @task(outlets=[ORDER_METRICS])
    def produce_order_metrics(): return "produced"
    produce_user_events(); produce_order_metrics()

@dag(schedule=[USER_EVENTS, ORDER_METRICS])  # 两个Dataset都更新后触发
def consumer():
    @task
    def generate_daily_report(): return "report_generated"
    generate_daily_report()

Pattern 5: Error Handling, Retry, and SLA Monitoring

Production environments must have robust error handling, automatic retries, and SLA monitoring.

# ❌ 每次Scheduler解析都会执行查询
sources = requests.get("https://api.example.com/sources").json()
@dag(...)
def my_dag():
    @task
    def process():
        for source in sources: ...
# ✅ 动态逻辑放在Task内部
@dag(...)
def my_dag():
    @task
    def get_sources() -> list:
        return requests.get("https://api.example.com/sources").json()
    @task
    def process(sources: list):
        for source in sources: ...

5 Pitfall Guide

1. Top-level Code Execution in DAG Files

Wrong: Execute database queries or API calls at DAG file top-level, runs every time Scheduler parses the DAG

Right: Put dynamic logic inside Tasks, DAG file top-level only defines structure

# ❌ DataFrame通过XCom传递
@task
def extract():
    df = pd.read_csv("huge_file.csv")
    return df.to_dict()  # XCom序列化失败

2. Passing Large Data Objects via XCom

Wrong: Pass entire DataFrame through XCom, exceeds 48KB and fails

Right: Pass file path references or use custom XCom Backend

# ✅ 传递文件路径引用
@task
def extract():
    df = pd.read_csv("huge_file.csv")
    path = f"/tmp/extract_{datetime.now().strftime('%Y%m%d')}.parquet"
    df.to_parquet(path)
    return {"file_path": path, "row_count": len(df)}

@task
def transform(metadata: dict):
    df = pd.read_parquet(metadata["file_path"])
    # 处理逻辑...

3. catchup=True Causing Historical Backfill Avalanche

Wrong: DAG with catchup=True and start_date a year ago triggers 365 DAG Runs on startup

Right: Production DAGs always use catchup=False, use airflow dags backfill for manual control

4. Sensor Occupying Worker Slots Causing Deadlock

Wrong: Multiple Sensors running simultaneously, each occupying a Worker Slot

Right: Use mode="reschedule" or deferrable mode

# ✅ reschedule模式
FileSensor(task_id="wait", filepath="/data/{{ ds }}.csv", mode="reschedule")
# ✅ deferrable模式(Airflow 2.2+推荐)
FileSensor(task_id="wait", filepath="/data/{{ ds }}.csv", deferrable=True)

5. Ignoring Data Quality Gates

Wrong: DAG only checks if tasks ran successfully, empty data also shows green

Right: Add quality gates at DAG end, block downstream if data volume or quality fails

@task
def quality_gate(loaded_count: int, expected_min: int = 1000) -> str:
    if loaded_count < expected_min:
        raise ValueError(f"Quality gate failed: {loaded_count} < {expected_min}")
    return "PASSED"

10 Common Error Troubleshooting

# Error Message Cause Solution
1 DAG import error: No module named 'xxx' Missing Python dependency in Worker environment Add to requirements.txt, restart Worker; or use Docker images
2 XCom value size exceeded limit of 48KB XCom data exceeds default 48KB limit Pass file path references; or configure custom XCom Backend (Redis)
3 Task is not running - Pool has no free slots Resource pool full, tasks queuing Increase Pool slots: airflow pool set default_pool 128
4 DAG seems missing from DagBag DAG file has syntax errors or circular imports Run airflow dags list-import-errors to check
5 Sensor timeout after 7200 seconds Sensor condition never met Check data source; increase timeout; use deferrable mode
6 AirflowTaskTimeout: Execution timed out Task execution exceeds execution_timeout Increase timeout; split into sub-tasks; optimize code
7 Detected deadlock while scheduling Circular dependencies or resource contention Check DAG dependency graph; check Pool config
8 Connection refused: could not connect to server Database or API connection failed Check Connection: airflow connections get-uri conn_id
9 Dynamic task mapping produced too many tasks Expanded tasks exceed max_map_length limit Adjust max_map_length in airflow.cfg (default 1024)
10 SLA missed for task xxx Task not completed within SLA window Check upstream delays; optimize performance; adjust SLA

Advanced Optimization

1. DAG Parsing Performance

@dag(
    dag_id="optimized_pipeline",
    dagrun_timeout=timedelta(hours=3),  # DAG Run最多3小时
    max_active_runs=1,  # 同时只允许1个DAG Run
    max_active_tasks=16,  # 最多16个并发Task
)
def optimized_pipeline(): ...

2. Connection Pool and Batch Operations

from airflow.hooks.base import BaseHook
import psycopg2
from contextlib import contextmanager

@contextmanager
def get_db_connection(connection_id: str):
    conn_info = BaseHook.get_connection(connection_id)
    conn = psycopg2.connect(host=conn_info.host, port=conn_info.port, database=conn_info.schema, user=conn_info.login, password=conn_info.password)
    try: yield conn
    finally: conn.close()

def batch_insert(conn, table: str, columns: list[str], rows: list[tuple], batch_size: int = 5000):
    from psycopg2.extras import execute_values
    with conn.cursor() as cur:
        for i in range(0, len(rows), batch_size):
            execute_values(cur, f"INSERT INTO {table} ({', '.join(columns)}) VALUES %s", rows[i:i + batch_size])
    conn.commit()

3. Dataset-Aware Scheduling Replacing Cron

from airflow.datasets import Dataset
RAW_DATA = Dataset("s3://data-lake/raw/events/")
CLEANED_DATA = Dataset("s3://data-lake/cleaned/events/")

@dag(schedule="0 2 * * *")
def producer():
    @task(outlets=[RAW_DATA])
    def extract(): ...
    @task(outlets=[CLEANED_DATA])
    def clean(): ...

@dag(schedule=[CLEANED_DATA])  # 数据就绪后自动触发
def consumer():
    @task
    def train_model(): ...

4. Priority and Resource Isolation

# 关键DAG使用高优先级和专用Pool
@dag(dag_id="critical_pipeline", priority_weight=100, pool="revenue_pool")
def critical_pipeline(): ...

# 非关键DAG使用低优先级
@dag(dag_id="experimental_pipeline", priority_weight=1, pool="default_pool")
def experimental_pipeline(): ...

5. Monitoring Metrics and Alert Integration

# airflow.cfg 开启StatsD指标导出
# [metrics]
# statsd_on = True
# statsd_host = localhost
# statsd_port = 8125

def sla_callback(dag, task_list, blocking_task_list, dagrun, *args, **kwargs):
    import requests
    from airflow.models import Variable
    webhook_url = Variable.get("slack_webhook_url")
    message = {"text": f":warning: SLA Violation\nDAG: {dag.dag_id}\nRun: {dagrun.execution_date}\nBlocking: {[t.task_id for t in blocking_task_list]}"}
    requests.post(webhook_url, json=message)

default_args = {"sla": timedelta(hours=2), "sla_miss_callback": sla_callback}

Comparison: Orchestration Tools

Dimension Apache Airflow Prefect Dagster Luigi
Orchestration DAG definition Functional+decorators Asset definition Task dependency
Data-aware Scheduling Dataset (2.4+) Native Native Asset Not supported
Dynamic Tasks Dynamic Task Mapping Native dynamic Native dynamic Not supported
Custom Operator Powerful plugin ecosystem Decorator-based Software-defined Asset Simple Task class
XCom/Data Passing 48KB limit, custom Backend Native unlimited Native Type system Simple parameter passing
Error Handling Retry+SLA+callbacks Native retry+state Native retry+Asset state Simple retry
Monitoring Web UI + StatsD Cloud UI Dagit UI Basic Web
Learning Curve Medium Low Medium-high Low
Community Most mature, most plugins Fast growing Growing Declining
Kubernetes KubernetesPodOperator Native K8s Agent Native K8s Not supported
2026 Recommendation ★★★★★ ★★★★ ★★★★ ★★
Use Case Enterprise ETL orchestration Small-medium projects Data asset intensive Simple batch processing

  • JSON Formatter — Format JSON when debugging Airflow DAG configs and API responses
  • Hash Encoder — Generate data fingerprints for XCom Redis Backend and verify data integrity
  • cURL to Code — Convert API debugging cURL commands to Python code for ETL extraction tasks

Further Reading


Apache Airflow 2.10 remains the de facto standard for data pipeline orchestration in 2026. Remember five core principles: TaskFlow API is the first choice for DAG design — functional style is more concise and readable than Operator; Dynamic Task Mapping is the weapon for parallel ETL — one Task definition expands into N instances; Custom Operators are the cornerstone of reuse — encapsulate internal system logic once, the whole team benefits; XCom + branching is the key to flexible scheduling — data-driven decisions, different paths for different processing; Error handling and SLA are the baseline of production — DAGs without retries and monitoring are ticking time bombs when deployed. From DAG design to production operations, Airflow ecosystem is mature enough — the rest is your practice in business scenarios.

Try these browser-local tools — no sign-up required →

#Airflow数据管道#ETL编排#DAG工作流#Airflow 2.10#2026#AI与大数据