ระบบจัดการเวิร์กโฟลว์ข้อมูล (Airflow, Dagster & Prefect)
5.2 workflow Orchestration ด้วย Apache Airflow
ระบบจัดการสายงาน (Workflow Orchestration) ทำหน้าที่สแตนด์บายประสานเวลารันและลำดับขั้นจราจรของท่อส่งข้อมูลหลากหลายแบบให้เป็นไปตามเงื่อนไขอย่างราบรื่น
1. โครงสร้างภายใน Scheduler, Database Backend, และรอบการคัดกรองงานของ Executors
ทฤษฎีและกลไกการทำงาน (How it works): Apache Airflow ประกอบด้วยแกนทำงานหลัก: - **Scheduler**: สแกนไดเรกทอรีไฟล์รหัส DAG เพื่อถอดสูตร และทำการบันทึกจัดเก็บประวัติสถานะลงใน **Metadata Database (เช่น PostgreSQL)** เพื่อค้นหา Task ที่พร้อมรัน จากนั้นยิงคำสั่งไปต่อคิวในระบบข้อความ (Redis/RabbitMQ) - **Executor (เช่น Celery, Kubernetes)**: ดึงคำสั่งจากคิวข้อความส่งต่อกระจายให้ **Workers** ที่มีหน้าที่เปิดรันโพรเซสทำงานจริง
# Conceptual architecture of Airflow execution loop
[DAG Definitions] -> [Scheduler Engine] <-> [PostgreSQL Metadata]
|
(Pushes to Queue)
|
v
[Celery/K8s Queue] -> [Worker Node 1 / Pod 1]
Use Case ในชีวิตจริง (Real-world Scenario): สายงานการสกัดข้อมูลบัตรเครดิตลูกค้าที่ต้องคอยรอบรันทุกคืน ตัว Scheduler จะคอยเฝ้าเช็คประวัติใน PostgreSQL และเมื่อถึงเวลารันตีหนึ่งตรง จะสั่งจอง Workers เพื่อเปิดเครื่องประมวลผลทันที
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): สภาวะ **Database Bottleneck** เมื่อ Scheduler สแกนโฟลเดอร์ DAG ถี่เกินไปและยิงล็อกฐานข้อมูล PostgreSQL พังเสียหาย แก้ไขโดยตั้งค่าขยายเวลารอบสแกนไฟล์ `min_file_process_interval` ในไฟล์ config เพื่อลดภาระการเขียนอ่านลงคลังสเตท
2. การออกแบบ DAG ให้มีคุณสมบัติทำงานซ้ำได้ผลลัพธ์คงเดิมและการย้อนเวลารันจ๊อบย้อนหลัง (Idempotency & Backfilling)
ทฤษฎีและกลไกการทำงาน (How it works): ท่อส่งที่ดีต้องมีคุณสมบัติ **Idempotent** หมายความว่ารันโค้ดประมวลผลข้อมูลกี่สิบรอบด้วยพารามิเตอร์วันเดิม จะต้องได้ผลลัพธ์ปลายทางคงเดิมเสมอ ห้ามเกิดปัญหายอดข้อมูลซ้อนหรือผลลัพธ์งอกเพิ่ม นอกจากนี้ Airflow รองรับระบบ **Backfill** โดยเราสามารถระบุสั่งรันย้อนเวลาตามประวัติวันเวลารัน (Execution Date) ย้อนหลังแบบอัตโนมัติได้เมื่อต้องการเก็บข้อมูลย้อนหลังแก้โครงสร้างตาราง
# Idempotent Airflow DAG with logical execution date filters
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime
with DAG(
"idempotent_etl_pipeline",
start_date=datetime(2026, 7, 1),
schedule_interval="@daily",
catchup=False # Prevents automatic run of unexecuted past DAG runs
) as dag:
# Process only partition corresponding to the execution date using Jinja templates
process_daily_data = BashOperator(
task_id="run_pipeline_partition",
bash_command="python3 /opt/etl.py --date {{ ds }}"
)
Use Case ในชีวิตจริง (Real-world Scenario): ระบบวิเคราะห์ยอดขายพบคิวรีล่มในประวัติ 5 วันที่แล้ว วิศวกรข้อมูลสามารถสั่งคำสั่งรันแก้สเปคสคริปต์ และทำ Backfill เฉพาะข้อมูลพาร์ทิชัน 5 วันนั้นใหม่ได้อย่างปลอดภัยโดยไม่ทำลายโครงสร้างตารางวันอื่น
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การกำหนดวันเวลารันโดยดึงค่าโมดูลเวลาปัจจุบัน `datetime.now()` ในเนื้อโค้ดประมวลผลโดยตรง ซึ่งจะทำให้คุณสมบัติ Idempotency และระบบ Backfill ใช้งานไม่ได้ แก้ไขโดยใช้ตัวแปรแฝงระบบของ Airflow `{{ ds }}` (Logical Date) เสมอ
3. การคัดแยกความปลอดภัยในกระบวนการทำงาน: BashOperator vs. KubernetesPodOperator
ทฤษฎีและกลไกการทำงาน (How it works): ในการเรียกใช้งาน Task ย่อยของสคริปต์ประมวลผลข้อมูล: - **BashOperator**: รันสคริปต์ในสภาพแวดล้อมระบบปฏิบัติการเดียวกับเซิร์ฟเวอร์ Airflow Worker ตรงๆ มีความยืดหยุ่นต่ำ เสี่ยงต่อปัญหาไลบรารีโค้ดขัดแย้งกันข้าม Task - **KubernetesPodOperator (KPOs)**: สั่งสร้างกล่องคอนเทนเนอร์ Pod แยกต่างหากบนคลัสเตอร์ Kubernetes มั่นคงปลอดภัย แยกสิทธิ์ใช้งานแรมและ CPU อย่างเป็นสัดส่วน และช่วยคัดแยกความเสี่ยงของรหัสหลุดล่มออกไปโดยสิ้นเชิง
# Declaring a secure task using KubernetesPodOperator
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
run_spark_container = KubernetesPodOperator(
task_id="secure_data_extraction",
name="data-extract-pod",
namespace="airflow-tasks",
image="pipecraft/etl-extractor:v2.1",
cmds=["python", "extract.py"],
arguments=["--target-partition", "{{ ds }}"],
get_logs=True
)
Use Case ในชีวิตจริง (Real-world Scenario): ท่อส่งหนึ่งใช้ Python 3.9 ประมวลผลข้อมูล ขณะที่อีกท่อใช้ Python 3.11 รันประมวลผล MLOps การใช้งาน KPOs ช่วยให้ทั้งคู่สามารถรันงานพร้อมกันบนโหนดเดียวกันได้โดยไร้ปัญหา Dependency ขัดแย้งกัน
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การเริ่มต้นเปิดคอนเทนเนอร์ Pod ใหม่ในทุก Task ย่อยสร้างความล่าช้า (Pod startup latency overhead) แก้ไขโดยหลีกเลี่ยง KPOs กับงานประเภทประมวลผลขนาดสั้นต่ำกว่าวิ (Sub-second tasks)
4. การจัดการความสัมพันธ์ข้ามสายการรัน (Cross-DAG Dependencies & Triggers)
ทฤษฎีและกลไกการทำงาน (How it works): ในแพลตฟอร์มระดับองค์กร ท่อส่งข้อมูลมักไม่ได้รันจบอยู่ในไฟล์เดียว แต่ต้องแยกสายงานออกจากกัน เช่น ท่อโหลดข้อมูลลูกค้ารันเสร็จก่อน จึงค่อยสั่งรันสายงานคำนวณเงินปันผล Airflow สนับสนุนการเชื่อมโยงข้ามไฟล์ผ่านเครื่องมือ **TriggerDagRunOperator** หรือเฝ้ารอคอยความสมบูรณ์ผ่าน **ExternalTaskSensor**
# Triggering another pipeline file using TriggerDagRunOperator
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
trigger_reporting_pipeline = TriggerDagRunOperator(
task_id="trigger_downstream_report",
trigger_dag_id="financial_reporting_dashboard_pipeline",
execution_date="{{ execution_date }}",
reset_dag_run=True,
wait_for_completion=False
)
Use Case ในชีวิตจริง (Real-world Scenario): ทีมวิจัยและทีมการตลาดแยกกันดูแลจ๊อบการทำงานของระบบตนเอง เมื่อทีมข้อมูลหลักโหลดประวัติลูกค้าเข้ามาเสร็จสิ้น จึงจะยิงสัญญาณปลุกท่อวิจัยวิเคราะห์ MLOps ให้ตื่นขึ้นมารันคำนวณแคมเปญโฆษณา
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): ปัญหาวงจรรันระบบรอสายค้างคา (Deadlocks) เมื่อ Sensor เฝ้ารอคอยสายงานอื่นนานเกินไปแย่งโควต้า Workers รันค้างเต็มระบบ แก้ไขโดยตั้งค่าโหมดประหยัดทรัพยากร `mode='reschedule'` เพื่อให้ Sensor ปล่อยแรม Workers ไปทำจ๊อบอื่นระหว่างคอยตรวจเช็ครอบใหม่
5. กลไกกู้ภัยสายงานล้มเหลว: การเซ็ตอัป Retries, Backoff, และ Custom Failure Callbacks
ทฤษฎีและกลไกการทำงาน (How it works): เพื่อเสถียรภาพสูงสุดในการให้บริการ Airflow จัดเตรียมคุณสมบัติช่วยแก้ปัญหาระบบหลุดชั่วคราว: - **Retries**: ตั้งค่าจำนวนรอบแก้ตัวใหม่เมื่อภารกิจพังเสียหาย - **Retry Delay / Exponential Backoff**: หน่วงเวลาก่อนสู้ใหม่เป็นทวีคูณเพื่อรอลดปัญหาเน็ตเวิร์กหนาแน่น - **Failure Callbacks**: ฟังก์ชันตรวจจับเออเร่อ เพื่อสั่งยิงสัญญาณด่วนดักจับรหัสข้อผิดพลาดแจ้งวิศวกรผ่าน Slack Webhook ทันที
# Retries configuration template with Custom Failure Callback
def notify_incident_slack(context):
exception = context.get("exception")
task_id = context.get("task_instance").task_id
# Logic to send Slack alert payload with error detail
print(f"Incident: Task {task_id} failed with error: {exception}")
default_args = {
"owner": "data_ops",
"retries": 3,
"retry_delay": timedelta(minutes=5),
"retry_exponential_backoff": True,
"on_failure_callback": notify_incident_slack
}
Use Case ในชีวิตจริง (Real-world Scenario): ท่อส่งโหลด API อัตราแลกเปลี่ยนเงินต่างประเทศล้มเหลวเนื่องจากเซิร์ฟเวอร์ธนาคารต้นทางขัดข้องชั่วขณะ การตั้ง Retry Exponential Backoff ช่วยให้ระบบพักรอ 5 นาทีและกลับไปดึงใหม่จนผ่านได้โดยไม่ต้องเรียกพนักงานตามแก้หน้างานกลางดึก
ข้อควรระวังและวิธีแก้ (Pitfalls & Mitigations): การแจ้งเตือนความผิดพลาดแบบพร่ำเพรื่อ (Alert Fatigue) จาก Task ย่อยที่ไม่สำคัญพังบ่อย แก้ไขโดยเปิดใช้งานการส่งเสียงเตือนเฉพาะ Task สำคัญระดับคอขวด (Critical Path End-nodes) เท่านั้น
🛠️ Weekend Sandbox Challenge: Build an Idempotent DAG with Slack Notification
โจทย์ปฏิบัติการ: จงเขียนไฟล์ Airflow DAG กำหนดความเสถียรของงาน โดยตั้งค่า `catchup=False`, การรันแก้ตัวใหม่ (Retries) และฟังก์ชันส่งข้อความเตือนเมื่อจ๊อบพังล้มเหลว (Failure Callback)
# local_airflow_alert_dag.py
from airflow import DAG
from airflow.operators.bash import BashOperator
from datetime import datetime, timedelta
def alert_failure(context):
print(f"Slack Notification: Task {context.get('task_instance').task_id} failed!")
default_args = {
'owner': 'data_ops',
'retries': 2,
'retry_delay': timedelta(seconds=30),
'on_failure_callback': alert_failure
}
with DAG(
'monitored_sales_pipeline',
default_args=default_args,
start_date=datetime(2026, 7, 1),
schedule_interval='@daily',
catchup=False
) as dag:
run_etl = BashOperator(
task_id='execute_etl_script',
bash_command='python3 /opt/pipecraft/run_etl.py --date {{ ds }}'
)
💼 Senior Technical Interview Q&A
Q1: เพราะเหตุใดท่อส่งข้อมูลที่ดีต้องยึดมั่นคุณสมบัติการรันซ้ำได้ผลเดิม (Idempotency) และควรหลีกเลี่ยงการใช้คำสั่ง `datetime.now()` ใน DAG อย่างไร?
A1: คุณสมบัติ Idempotency การันตีว่าไม่ว่าจะสั่งรันจ๊อบย้อนหลังกี่ครั้งด้วยพารามิเตอร์วันเดิม ข้อมูลในตารางปลายทางจะต้องไม่บวมซ้ำและถูกต้องเท่าเดิม การใช้ `datetime.now()` จะดึงเวลาจริงของระบบขณะกดปุ่มรัน ทำให้ประวัติพาร์ทิชันผิดเพี้ยนไปจากวันเวลาจริงของรอบธุรกรรมนั้น แก้ไขโดยใช้ตัวแปรแฝงระบบของ Airflow `{{ ds }}` เสมอ
Q2: จงวิเคราะห์ความแตกต่างของขอบเขตทรัพยากรระหว่างการรันโปรเซสด้วย CeleryExecutor และ KubernetesExecutor ในระบบ Airflow ขนาดใหญ่?
A2: CeleryExecutor ต้องการเปิดเซิร์ฟเวอร์ Workers ทิ้งไว้สแตนด์บายตลอดเวลา (Static resources pool) มีข้อจำกัดในการสเกลเครื่องและเสี่ยงต่อปัญหาไลบรารีโค้ดขัดแย้งข้ามจ๊อบ ในขณะที่ KubernetesExecutor จะสั่งเปิด Pod ใหม่ขนานบนคลัสเตอร์ขึ้นมารันงานรายจ๊อบย่อยเฉพาะกิจและทำลายโหนดทิ้งทันทีเมื่อจบงาน (Dynamic auto-scaling) คัดแยกแรม CPU ปลอดภัย
⏱️ Interactive DAG Workflow Orchestrator
Interactive DAGเป้าหมาย: กำหนดลำดับการทำงาน (Dependencies) ของเวิร์กโฟลว์ข้อมูลให้ถูกต้อง โดยงาน Clean จะทำงานหลังจาก Extract สำเร็จ และงาน Load จะทำงานหลังจาก Clean สำเร็จ
ข้อดี / จุดเด่น & ข้อเสีย / ข้อควรระวัง
ข้อดี / จุดเด่น
- • สามารถจัดการลำดับความเชื่อมโยงของงานที่สลับซับซ้อนได้อย่างน่าเชื่อถือ
- • มีระบบ User Interface คอยมอนิเตอร์และสั่งทำงานย้อนหลัง (Backfilling) ได้โดยง่าย
ข้อเสีย / ข้อควรระวัง
- • ต้องการทรัพยากรจัดเตรียมระบบสูง (ฐานข้อมูล Metadata, Web Server, Scheduler)
- • มีความยุ่งยากในการพัฒนาและรันชุดทดสอบความถูกต้องของสคริปต์ที่เครื่องตนเอง
ปฏิบัติการจริง (Lab Practice)
แล็บ: สร้างและรันสคริปต์ควบคุม DAG สำหรับท่อส่งข้อมูล
💡 คำแนะนำ & ทริกเด็ด
ห้ามใช้โหนด Worker ของ Airflow ในการคำนวณหรือดึงข้อมูลปริมาณมากตรงๆ ให้ใช้คำสั่ง Operators ไปสั่งรันงานหนักภายนอก (เช่น KubernetesPodOperator หรือ BigQueryInsertJobOperator) แทน เพื่อไม่ให้ Airflow Scheduler แฮงก์