未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用范围与待维护事项
本文仅供具备 Airflow、容器部署及 Python 扩展经验者在隔离环境练习,不是可直接投产的方案。示例以 Airflow 2.8.1 为起点,provider、Cosmos、dbt 版本尚未固定验证;Compose 缺初始化与跨节点存储设计,REST 需启用认证并使用触发响应的运行 ID。HTTP 分页和 MySQL 读写仅适用于受控数据,仍有内存、SQL 拼接与 REPLACE 语义风险。dbt 项目、连接及 Plugin 实现未提供,部署与集成练习须补齐材料后验证。
Airflow 进阶实践
abstract 本文核心内容
- CeleryExecutor 分布式部署(多 Worker 配置示例,投产前须补齐初始化、存储与安全设计)
- KubernetesExecutor 云原生方案
- 自定义 Operator 和 Hook(封装复用逻辑)
- Plugin 开发(扩展 Web UI)
- Airflow REST API 编程控制
- 与 dbt 集成(现代数据转换工具)
CeleryExecutor 分布式部署
CeleryExecutor 通过 Redis 或 RabbitMQ 消息队列,将任务分发到多个 Worker 节点执行,是中大型环境中可选的分布式执行方案;选型还需评估运维能力、隔离需求与资源规模。
架构
Scheduler → Redis Queue → Worker 1
→ Worker 2
→ Worker 3
docker-compose 多 Worker 配置
# docker-compose.prod.yaml
version: '3.8'
x-airflow-common:
&airflow-common
image: apache/airflow:2.8.1
environment: &airflow-common-env
AIRFLOW__CORE__EXECUTOR: CeleryExecutor
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:${POSTGRES_PASSWORD}@postgres/airflow
AIRFLOW__CELERY__BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
AIRFLOW__CELERY__RESULT_BACKEND: db+postgresql://airflow:${POSTGRES_PASSWORD}@postgres/airflow
# 性能调优
AIRFLOW__CORE__PARALLELISM: 64 # 全局最大并行任务数
AIRFLOW__CORE__MAX_ACTIVE_TASKS_PER_DAG: 16
AIRFLOW__CELERY__WORKER_CONCURRENCY: 8 # 每个 Worker 的并发数
# 安全
AIRFLOW__CORE__FERNET_KEY: ${FERNET_KEY}
AIRFLOW__WEBSERVER__SECRET_KEY: ${WEBSERVER_SECRET_KEY}
volumes:
- ./dags:/opt/airflow/dags
- ./logs:/opt/airflow/logs
- ./plugins:/opt/airflow/plugins
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: airflow
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: airflow
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7.0
command: redis-server --requirepass ${REDIS_PASSWORD}
airflow-webserver:
<<: *airflow-common
command: webserver
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 5
airflow-scheduler:
<<: *airflow-common
command: scheduler
# Worker 1:处理高优先级 ETL 任务
airflow-worker-etl:
<<: *airflow-common
command: celery worker -q etl,default
environment:
<<: *airflow-common-env
# 处理 etl 和 default 队列的任务
AIRFLOW__CELERY__WORKER_CONCURRENCY: 8
# Worker 2:处理机器学习任务(大内存机器)
airflow-worker-ml:
<<: *airflow-common
command: celery worker -q ml_tasks
environment:
<<: *airflow-common-env
AIRFLOW__CELERY__WORKER_CONCURRENCY: 2 # ML 任务并发少,但内存需求大
volumes:
postgres_data:
任务指定 Queue
# 在 Task 中指定执行队列
heavy_spark_task = SparkSubmitOperator(
task_id="run_ml_training",
application="/jobs/train_model.py",
queue="ml_tasks", # 只由 ml_worker 执行
# ...
)
normal_etl_task = BashOperator(
task_id="sync_data",
bash_command="python sync.py",
queue="etl", # 由 etl_worker 执行
)
KubernetesExecutor
KubernetesExecutor 为每个 Task 动态创建一个 Pod,天然隔离,适合云原生环境。
核心优势
- 每个 Task 有独立的资源(CPU/Memory 限制)
- 任务环境完全隔离(不同 Task 可以用不同镜像)
- 按任务创建执行 Pod;空闲时可没有任务 Pod,但调度器、数据库等基础组件仍占用资源
- Pod 隔离可降低任务之间的直接影响,但共享节点、集群资源或基础服务故障仍可能影响其他 Task
配置
# airflow.cfg
[kubernetes]
namespace = airflow # Task Pod 所在的 Kubernetes 命名空间
worker_container_repository = my-registry.example.com/airflow # 镜像仓库
worker_container_tag = 2.8.1
delete_worker_pods = True # Task 完成后删除 Pod
delete_worker_pods_on_failure = False # 失败时保留 Pod(方便排查)
在 Task 中覆盖资源配置
from airflow.providers.cncf.kubernetes.operators.kubernetes_pod import KubernetesPodOperator
from kubernetes.client import models as k8s
# 使用 KubernetesPodOperator 直接运行 Pod
k8s_task = KubernetesPodOperator(
task_id="k8s_etl_task",
name="etl-pod",
namespace="airflow",
image="my-registry.example.com/data-tools:latest",
cmds=["python"],
arguments=["/app/etl.py", "--date", "{{ ds }}"],
# 资源限制
container_resources=k8s.V1ResourceRequirements(
requests={"cpu": "500m", "memory": "1Gi"},
limits={"cpu": "2000m", "memory": "4Gi"},
),
# 节点亲和性(调度到有 GPU 的节点)
node_selector={"node-type": "gpu"},
# 环境变量
env_vars=[
k8s.V1EnvVar(name="TARGET_DATE", value="{{ ds }}"),
],
# 将 Secret 中的键值注入容器环境变量
env_from=[
k8s.V1EnvFromSource(
secret_ref=k8s.V1SecretEnvSource(name="db-credentials")
)
],
is_delete_operator_pod=True,
)
自定义 Operator
当内置 Operator 满足不了需求时,可以继承 BaseOperator 创建自定义 Operator。
场景:HTTP API 数据抽取 Operator
# plugins/operators/http_extract_operator.py
from typing import Any, Optional
import requests
import json
from airflow.models import BaseOperator
from airflow.exceptions import AirflowException
from airflow.hooks.base import BaseHook
class HttpExtractOperator(BaseOperator):
"""
从 HTTP API 抽取数据并保存到文件
可以在多个 DAG 中复用,封装了 Bearer 认证和状态码错误处理;
未实现请求级重试,任务级重试需通过 retries 等参数配置
"""
# 声明哪些参数支持 Jinja 模板渲染
template_fields = ["endpoint", "output_path", "params"]
def __init__(
self,
http_conn_id: str, # Airflow Connection ID
endpoint: str, # API 路径(支持模板变量)
output_path: str, # 输出文件路径(支持模板变量)
method: str = "GET", # HTTP 方法
params: Optional[dict] = None, # 查询参数(支持模板变量)
data_key: str = "data", # 响应 JSON 中数据的 key
page_size: int = 1000, # 分页大小
**kwargs,
):
super().__init__(**kwargs)
self.http_conn_id = http_conn_id
self.endpoint = endpoint
self.output_path = output_path
self.method = method
self.params = params or {}
self.data_key = data_key
self.page_size = page_size
def execute(self, context: Any) -> dict:
"""Task 执行时调用的核心方法"""
import os
import pandas as pd
self.log.info(f"开始从 {self.endpoint} 抽取数据")
# 获取连接信息(Host、Token 等)
conn = BaseHook.get_connection(self.http_conn_id)
base_url = f"{conn.schema or 'https'}://{conn.host}"
headers = {
"Authorization": f"Bearer {conn.password}",
"Content-Type": "application/json",
}
# 分页抽取
all_data = []
page = 1
while True:
params = {**self.params, "page": page, "page_size": self.page_size}
response = requests.request(
method=self.method,
url=f"{base_url}{self.endpoint}",
headers=headers,
params=params,
timeout=60,
)
if response.status_code != 200:
raise AirflowException(
f"API 请求失败:{response.status_code} - {response.text}"
)
result = response.json()
page_data = result.get(self.data_key, [])
if not page_data:
break # 没有更多数据
all_data.extend(page_data)
self.log.info(f"第 {page} 页:{len(page_data)} 条,累计 {len(all_data)} 条")
if len(page_data) < self.page_size:
break # 最后一页
page += 1
# 保存数据
os.makedirs(os.path.dirname(self.output_path), exist_ok=True)
df = pd.DataFrame(all_data)
df.to_csv(self.output_path, index=False)
self.log.info(f"数据已保存到:{self.output_path},共 {len(df)} 行")
return {"row_count": len(df), "output_path": self.output_path}
使用自定义 Operator
# 在 DAG 中使用
from plugins.operators.http_extract_operator import HttpExtractOperator
extract_api_data = HttpExtractOperator(
task_id="extract_from_api",
http_conn_id="crm_api",
endpoint="/api/v2/customers",
output_path="/data/raw/customers/dt={{ ds }}/customers.csv",
params={"updated_since": "{{ ds }}", "status": "active"},
page_size=500,
)
自定义 Hook
Hook 封装了与外部系统的交互逻辑,被 Operator 调用。
# plugins/hooks/custom_mysql_hook.py
from airflow.providers.mysql.hooks.mysql import MySqlHook
import pandas as pd
from typing import Optional
class DataWarehouseMySqlHook(MySqlHook):
"""
封装了数仓 MySQL 的常用操作,包括:
- 分页读取并汇总增量数据
- 使用继承的 insert_rows 执行 REPLACE INTO 写入
- 其他连接及底层操作沿用 MySqlHook;本例未实现专用写入重试或表存在检查
"""
conn_name_attr = "mysql_conn_id"
default_conn_name = "mysql_dw"
def read_incremental(
self,
table: str,
date_col: str,
start_date: str,
end_date: str,
chunk_size: int = 10000,
) -> pd.DataFrame:
"""分批读取后汇总增量数据;最终仍将全部结果保存在内存中,不能保证避免 OOM"""
sql = f"""
SELECT * FROM {table}
WHERE {date_col} >= '{start_date}'
AND {date_col} < '{end_date}'
"""
chunks = []
offset = 0
while True:
chunk_sql = f"{sql} LIMIT {chunk_size} OFFSET {offset}"
chunk = self.get_pandas_df(chunk_sql)
if chunk.empty:
break
chunks.append(chunk)
offset += chunk_size
if len(chunk) < chunk_size:
break
return pd.concat(chunks, ignore_index=True) if chunks else pd.DataFrame()
def upsert(self, df: pd.DataFrame, table: str, key_cols: list) -> int:
"""通过 MySqlHook 的 replace=True 执行 REPLACE INTO;不是 INSERT ON DUPLICATE KEY UPDATE,key_cols 当前未使用,冲突由表的主键或唯一索引决定,并可能产生删除后插入的副作用"""
rows = df.values.tolist()
self.insert_rows(
table=table,
rows=rows,
target_fields=df.columns.tolist(),
replace=True,
)
return len(rows)
REST API:编程控制 Airflow
Airflow 2.x 提供了完整的 REST API,可以从外部系统控制 DAG 运行。
常用 API
# 设置基础认证
AIRFLOW_URL="http://localhost:8080"
AUTH="admin:admin"
# 1. 列出所有 DAG
curl -u $AUTH "$AIRFLOW_URL/api/v1/dags"
# 2. 手动触发 DAG Run
curl -u $AUTH -X POST \
-H "Content-Type: application/json" \
-d '{
"execution_date": "2024-01-15T02:00:00+00:00",
"conf": {"source_date": "2024-01-14", "force_rerun": true}
}' \
"$AIRFLOW_URL/api/v1/dags/ecommerce_daily_etl/dagRuns"
# 3. 查看 DAG Run 状态
curl -u $AUTH \
"$AIRFLOW_URL/api/v1/dags/ecommerce_daily_etl/dagRuns/scheduled__2024-01-15T02:00:00+00:00"
# 4. 获取 Task Instance 列表
curl -u $AUTH \
"$AIRFLOW_URL/api/v1/dags/ecommerce_daily_etl/dagRuns/scheduled__2024-01-15T02:00:00+00:00/taskInstances"
Python SDK 调用
import requests
from datetime import datetime
class AirflowClient:
def __init__(self, base_url: str, username: str, password: str):
self.base_url = base_url
self.auth = (username, password)
self.session = requests.Session()
self.session.auth = self.auth
def trigger_dag(self, dag_id: str, conf: dict = None) -> dict:
"""触发 DAG Run"""
payload = {
"execution_date": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00"),
"conf": conf or {},
}
resp = self.session.post(
f"{self.base_url}/api/v1/dags/{dag_id}/dagRuns",
json=payload,
)
resp.raise_for_status()
return resp.json()
def get_dag_run_status(self, dag_id: str, run_id: str) -> str:
"""查询 DAG Run 状态"""
resp = self.session.get(
f"{self.base_url}/api/v1/dags/{dag_id}/dagRuns/{run_id}"
)
resp.raise_for_status()
return resp.json()["state"]
# 使用示例
client = AirflowClient("http://airflow:8080", "admin", "admin")
# 从上游系统触发 DAG(比如数据上传完成后触发)
run = client.trigger_dag("ecommerce_daily_etl", conf={"source": "s3://bucket/data"})
print(f"触发成功,Run ID:{run['run_id']}")
与 dbt 集成
dbt(data build tool)是现代数仓的 SQL 转换工具,可与 Airflow 配合,由 Airflow 编排 dbt 的转换和测试任务。
分工:
- Airflow:调度控制,数据抽取(Extract + Load),编排 dbt 运行
- dbt:数据转换(Transform),SQL 模型管理,血缘追踪
方式一:BashOperator 运行 dbt(简单)
from airflow.operators.bash import BashOperator
# 运行 dbt 的所有 models
dbt_run = BashOperator(
task_id="dbt_run",
bash_command="""
cd /opt/dbt/my_project && \
dbt run \
--profiles-dir /opt/dbt/profiles \
--vars '{"execution_date": "{{ ds }}"}'
""",
env={
"DBT_TARGET": "prod",
},
)
# 只运行指定 tag 的 models
dbt_run_daily = BashOperator(
task_id="dbt_run_daily_models",
bash_command="""
cd /opt/dbt/my_project && \
dbt run \
--select tag:daily \
--vars '{"run_date": "{{ ds }}"}'
""",
)
# 运行 dbt test(数据质量测试)
dbt_test = BashOperator(
task_id="dbt_test",
bash_command="cd /opt/dbt/my_project && dbt test --select tag:daily",
)
# EL 完成 → dbt Transform → dbt Test
extract_load >> dbt_run >> dbt_test
方式二:使用 Cosmos(dbt 与 Airflow 集成库)
Astronomer Cosmos 是 Astronomer 提供的 dbt 与 Airflow 集成库,可将 dbt 模型及其依赖映射为 Airflow 的 TaskGroup 和任务;具体支持范围取决于所用版本与配置。
# pip install astronomer-cosmos
from cosmos import DbtTaskGroup, ProjectConfig, ProfileConfig, ExecutionConfig, RenderConfig
from cosmos.profiles import PostgresUserPasswordProfileMapping
# 将整个 dbt 项目映射为一个 TaskGroup
dbt_tg = DbtTaskGroup(
group_id="dbt_transform",
project_config=ProjectConfig("/opt/dbt/my_project"),
profile_config=ProfileConfig(
profile_name="default",
target_name="prod",
profile_mapping=PostgresUserPasswordProfileMapping(
conn_id="postgres_dw",
profile_args={"schema": "public"},
),
),
execution_config=ExecutionConfig(dbt_executable_path="/usr/local/bin/dbt"),
render_config=RenderConfig(
# 只选择 daily tag 的 model
select=["tag:daily"],
# 每个 dbt model 变成一个独立的 Airflow Task
# 依赖关系自动映射
),
)
# 集成到完整管道
extract_load >> dbt_tg >> send_notification
小结
| 场景 | 推荐方案 |
|---|---|
| 生产分布式执行 | CeleryExecutor(成熟稳定) |
| 云原生弹性扩展 | KubernetesExecutor |
| 封装复用的操作 | 自定义 Operator 继承 BaseOperator |
| 封装外部系统连接 | 自定义 Hook |
| 外部系统触发 Airflow | REST API |
| 现代数仓 SQL 转换 | dbt + Airflow(Cosmos 集成) |
下一篇:12-Airflow最佳实践与踩坑 — 生产环境 checklist 和常见坑
所属系列:00-Airflow学习路线图 | 上一篇:10-数据管道实战 | 下一篇:12-Airflow最佳实践与踩坑
#Airflow #CeleryExecutor #KubernetesExecutor #自定义Operator #dbt #REST_API #进阶
info 前置知识
- 掌握 Airflow 基础操作,能独立编写并运行完整 DAG
- 了解 Docker Compose 和网络配置基础(CeleryExecutor 需要 Redis)
- 熟悉 Python 类继承的语法(自定义 Operator 需要继承 BaseOperator)
- 了解 dbt 的基本概念(可先参考 dbt 概述与理念)
掌握检查
- [ ] 能说出 LocalExecutor、CeleryExecutor、KubernetesExecutor 三者的适用规模和核心区别
- [ ] 能描述自定义 Operator 的最简实现:继承
BaseOperator,实现execute(self, context)方法 - [ ] 理解 Hook 和 Operator 的职责划分:Hook 负责封装连接和底层操作,Operator 负责编排逻辑
- [ ] 能说出 Airflow REST API 触发 DAG 的场景(外部系统事件驱动)及认证方式
- [ ] 知道 Airflow + dbt 集成的两种方式(BashOperator 运行 dbt 命令 vs Cosmos 库的 DbtTaskGroup)的优劣
实践练习
基础题:把你在 常用 Operator 详解 中经常用到的 BashOperator 调用某个内部脚本的模式,封装成一个自定义 Operator(如 DataSyncOperator),让 DAG 代码更简洁。写出完整的 Operator 类和使用示例。
进阶题:用 Airflow REST API 实现以下场景:当一个文件上传到指定目录后(用 Python 的文件系统监控),自动调用 Airflow API 触发对应的 ETL DAG,并传入文件路径作为 conf 参数。写出触发 API 的完整代码。