LLM WikiAccess-protected knowledge portal
← 스터디 홈
2편 · 약 24분

Airflow 운영 심화: scheduler, executor, metadata DB, pool, queue

Airflow가 task를 실행하는 실제 경로

Airflow에서 "DAG를 등록하면 자동으로 실행된다"는 경험은 익숙하지만, 그 사이에 어떤 일이 벌어지는지 모르면 왜 task가 scheduled 상태에서 수 분간 머무는지, 왜 pool이 가득 찬 것처럼 보이는데 실제로는 아무 task도 실행 중이 아닌지 이해하기 어렵다. 이 편은 scheduler가 task를 어떻게 발견하고 executor에게 넘기는지, metadata DB와 pool이 그 과정에서 어떤 역할을 하는지, 그리고 운영자가 손댈 수 있는 파라미터가 무엇인지를 처음부터 끝까지 짚는다.

DAG 파싱 계층
DagFileProcessorManager
DAG 디렉터리 감시·분배
DagFileProcessor (×N)
개별 .py 파일 파싱
↓ DagModel·SerializedDag
Metadata DB
(PostgreSQL)
스케줄링 계층
Scheduler
루프: DagRun 생성·TI 전환
Critical Section
pool·concurrency 체크 후 queued
↓ queued TI 목록
실행 계층
Executor
LocalExecutor / CeleryExecutor / K8s
Worker / Pod
실제 task 프로세스 실행
↓ 상태 콜백
Webserver는 Metadata DB를 읽기 전용으로 조회 — 스케줄링 루프에는 관여하지 않는다
Airflow 컴포넌트와 데이터 흐름

1. Scheduler 내부 루프

1.1 두 가지 프로세스: 파싱과 스케줄링

Airflow scheduler는 실제로 두 가지 별개의 일을 한다.

DAG 파싱: DagFileProcessorManager가 DAG 디렉터리를 감시하며 파일 목록을 DagFileProcessor 프로세스 풀에 분배한다. 각 프로세스는 Python 파일을 임포트해서 DAG 객체를 만들고 직렬화(SerializedDag)해서 metadata DB에 저장한다. 파싱 주기는 dag_dir_list_interval(기본 300초)와 min_file_process_interval이 제어한다.

Airflow 2.3+부터는 AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR=True로 파싱 프로세스를 별도 호스트에서 실행할 수 있다. DAG 수가 많을 때 파싱 CPU 부하를 스케줄러 노드에서 분리하는 방법이다.

스케줄링 루프: 파싱과 독립적으로 실행되는 루프로, scheduler_heartbeat_sec(기본 5초) 주기로 다음 작업을 수행한다.

1. 새 DagRun을 생성해야 하는 DAG 확인 (schedule 도래)
2. 실행 가능한 TaskInstance 상태 전환 (none → scheduled)
3. Critical Section: pool·concurrency 한도 확인 후 scheduled → queued
4. Executor에 queued TI 목록 전달
5. Executor 콜백 처리: running/failed/success 상태 반영

1.2 Critical Section: pool과 concurrency의 교차점

스케줄링 루프에서 가장 중요한 단계가 3번이다. Airflow 공식 문서는 이 단계를 "Critical Section"이라고 부른다. 이 시점에 scheduler는 slot_pool 테이블 전체에 row-level write lock을 걸고, 다음 기준을 모두 충족하는 TI만 queued 상태로 전환한다.

  • pool의 남은 슬롯 수 ≥ task의 pool_slots
  • DAG 전체 max_active_tasks 한도 미초과
  • 전역 parallelism 한도 미초과

이 lock 덕분에 여러 scheduler가 동시에 실행되더라도(HA 모드) 슬롯을 중복으로 소비하지 않는다. 반대로 말하면, pool 슬롯이 가득 찬 상태에서는 이 단계에서 새 task가 queued로 넘어가지 않는다. UI에서 task가 scheduled 상태에 오래 머무는 원인의 상당수는 이 단계에서의 블로킹이다.

1.3 HA 스케줄러: 다중 인스턴스 운영

Airflow 2.0+에서 여러 scheduler를 동시에 실행할 수 있다. PostgreSQL 12+ 또는 MySQL 8.0+ 환경에서는 추가 설정 없이 scheduler 프로세스를 여러 개 띄우기만 하면 된다.

HA 스케줄러는 Critical Section의 row-level locking으로 중복 실행을 막는다. DAG 파싱은 각 scheduler가 독립적으로 수행하므로, scheduler 수가 늘어나면 파싱 처리량도 함께 늘어난다. 스케줄링 루프 자체는 분산 처리되지만 Critical Section은 직렬화된다.

실무에서 HA 스케줄러를 2~3개 운영하면 파싱 속도가 개선되고 scheduler 장애 시 자동 페일오버가 이루어진다. 다만 scheduler 수를 과도하게 늘리면 Critical Section 경합이 늘어 오히려 성능이 저하될 수 있다.


2. Executor: task를 실제로 실행하는 방법

Executor는 scheduler가 "이 task를 실행하라"고 넘긴 TI를 실제로 프로세스/컨테이너/워커에서 실행하는 컴포넌트다. Executor 종류 선택은 Airflow 배포 아키텍처의 핵심 결정이다.

LocalExecutor
scheduler 노드에서
subprocess로 task 실행
소규모·개발·테스트에 적합
parallelism으로 병렬 제어
CeleryExecutor
Celery broker(Redis/RabbitMQ)
→ 분산 worker 풀
수평 확장 필요 시 표준 선택
queue 라우팅 지원
KubernetesExecutor
task마다 K8s Pod 생성
완전한 리소스 격리
Pod 생성 오버헤드(~수초)
K8s 환경에서 권장
Airflow 3.0에서 LocalExecutor가 기본값, CeleryExecutor는 별도 provider 패키지로 분리됨
Executor 종류 비교

2.1 LocalExecutor

scheduler 프로세스와 같은 호스트에서 subprocess를 생성해 task를 실행한다. parallelism 설정으로 동시 실행 subprocess 수를 제한한다. 단일 노드이므로 노드 장애 시 모든 task가 멈추는 단점이 있지만, 운영 복잡도가 가장 낮다. 소규모 파이프라인(동시 task < 50)이나 개발 환경에 적합하다.

2.2 CeleryExecutor

분산 task queue 라이브러리인 Celery를 통해 task를 여러 worker 노드에 분배한다. 구성 요소는 세 가지다.

  • Broker (Redis 또는 RabbitMQ): scheduler가 queued TI를 메시지로 전송하는 큐
  • Result backend (Redis 또는 DB): worker가 task 결과 상태를 저장하는 저장소
  • Celery worker: broker에서 메시지를 꺼내 task를 실행하는 프로세스

Queue 라우팅이 CeleryExecutor의 핵심 기능이다. task에 queue="heavy_queue"를 지정하면, 해당 worker에서만 그 task가 실행된다. 예를 들어 Spark job은 spark_worker 큐를, 경량 Python 작업은 default 큐를 쓰도록 분리할 수 있다. Worker는 airflow celery worker -q spark_worker,default처럼 여러 큐를 구독할 수 있다.

# worker 시작 예: spark_queue만 처리하는 전용 worker
airflow celery worker -q spark_queue --concurrency 4

# default 큐와 etl 큐 모두 처리하는 worker
airflow celery worker -q default,etl --concurrency 16

worker_concurrency 설정은 worker 하나가 동시에 실행할 수 있는 task 수다. CPU와 메모리를 고려해서 지정한다. 메모리 집약적 task라면 낮게, IO 대기 중심이라면 높게 설정한다.

2.3 KubernetesExecutor

task 하나당 K8s Pod를 생성해서 실행한다. 완전한 환경 격리가 장점이지만 Pod 생성·종료 오버헤드(보통 수~수십 초)가 있으므로 수초 내에 완료되는 경량 task에는 비효율적이다.

task별로 리소스 요청(CPU, 메모리), 환경변수, 이미지를 다르게 지정할 수 있다. executor_config 파라미터로 task 단위 Pod 설정을 오버라이드한다.

Airflow 2.7+에서는 LocalKubernetesExecutor가 추가되어, 경량 task는 LocalExecutor로, 무거운 task는 KubernetesExecutor로 라우팅할 수 있다.


3. Metadata DB: Airflow의 상태 저장소

Metadata DB는 Airflow의 단일 상태 저장소다. Scheduler, Executor, Webserver 모두 이 DB를 통해 상태를 읽고 쓴다.

3.1 핵심 테이블

테이블용도
dagDAG 메타정보, 활성화 상태, max_active_runs, is_paused
serialized_dag파싱된 DAG의 직렬화 형태 — webserver가 코드 파싱 없이 구조 표시
dag_run개별 DagRun 레코드: run_id, logical_date, data_interval_start/end, state
task_instance개별 TI: 상태, 시작/종료 시각, 재시도 횟수, pool, queue, priority_weight
jobscheduler·worker job 상태 — HA scheduler의 생존 확인에 사용
slot_poolpool 이름, 전체 슬롯 수 (Critical Section에서 row lock)
logtask 실행 로그 메타정보 (실제 로그는 파일 또는 원격 저장소)

3.2 DB 선택과 연결 풀

Airflow 공식 문서는 PostgreSQL을 운영 환경의 표준으로 권장한다. SQLite는 단일 프로세스 제한으로 운영에 부적합하다. MySQL은 지원되지만 PostgreSQL이 더 넓게 검증됐다.

PGBouncer 사용은 PostgreSQL 환경에서 강력히 권장된다. Airflow의 scheduler·worker는 DB 연결을 많이 열고 짧게 사용하는 패턴이다. PostgreSQL은 연결마다 프로세스를 생성하므로, 연결이 많으면 PostgreSQL 서버 메모리와 CPU가 급증한다. PGBouncer의 transaction 모드 pooling이 이 문제를 해결한다.

권장 연결 설정 예:
  [scheduler]
  sql_alchemy_pool_size = 5           # SQLAlchemy 연결 풀 크기
  sql_alchemy_max_overflow = 10       # 초과 허용 연결 수
  sql_alchemy_pool_recycle = 1800     # 유휴 연결 재활용 간격(초)

  PGBouncer:
    pool_mode = transaction           # transaction 완료 후 연결 반환
    max_client_conn = 500
    default_pool_size = 20

3.3 Metadata DB 유지 관리

Metadata DB는 시간이 지나면서 오래된 DagRun, TaskInstance, Log 레코드가 쌓여 성능이 저하된다. Airflow는 airflow db clean 명령을 제공한다.

# 30일 이전 run 기록 정리
airflow db clean --clean-before-timestamp "$(date -d '30 days ago' +%Y-%m-%dT%H:%M:%S)"

# PostgreSQL: 주기적으로 VACUUM ANALYZE 실행 필요
VACUUM ANALYZE task_instance;
VACUUM ANALYZE dag_run;

운영 규모에 따라 다르지만 일반적으로 30~90일치 이력만 DB에 유지하고, 더 오래된 감사 로그는 별도 cold storage에 보관하는 것이 좋다.


4. Pool: 리소스 격리의 기본 도구

Pool은 "동시에 실행할 수 있는 task 수를 자원 단위로 제한"하는 메커니즘이다. DB 연결, API 호출 TPS, 메모리 집약적 작업 등을 보호하는 용도로 쓴다.

4.1 default_pool

모든 task는 명시적으로 pool을 지정하지 않으면 default_pool에 배정된다. 초기 슬롯 수는 128이고, UI(Admin > Pools)나 CLI에서 수정할 수 있다.

# default_pool 슬롯 확인
airflow pools list

# 새 pool 생성 (외부 API 동시 호출 10개 제한)
airflow pools set api_pool 10 "external API rate limit"

4.2 Pool 설계 패턴

db_pool (슬롯: 20)
MySQL 쿼리 task
PostgreSQL 적재 task
DB max_connections 보호
spark_pool (슬롯: 5)
Spark submit task
클러스터 동시 job 수 제한
api_pool (슬롯: 10)
외부 API 호출 task
TPS 초과·429 에러 방지
pool_slots=2를 지정하면 task 하나가 슬롯 2개를 점유 — 메모리 집약적 task에 활용
Pool 기반 리소스 격리

task에 pool_slots 파라미터를 사용하면 슬롯 가중치를 조정할 수 있다. 예를 들어 Spark job이 일반 task보다 리소스를 4배 쓴다면 pool_slots=4로 지정해서, 5슬롯 pool에서 최대 1개만 동시에 실행되도록 제어한다.

spark_task = SparkSubmitOperator(
    task_id="run_spark_job",
    pool="spark_pool",
    pool_slots=4,   # spark_pool 슬롯 4개 점유
    ...
)

5. 핵심 파라미터 한눈에 보기

Airflow 운영에서 가장 자주 조정하는 파라미터를 계층별로 정리한다.

파라미터기본값의미튜닝 방향
parallelism32전역 동시 task 수worker 총 CPU/메모리에 맞게 확장
max_active_tasks_per_dag16DAG당 최대 동시 task특정 DAG의 DB/API 과부하 방지
max_active_runs_per_dag16DAG당 최대 동시 DagRuncatchup·backfill 시 급증 방지
scheduler_heartbeat_sec5스케줄러 루프 주기줄이면 응답성↑·CPU↑
dag_dir_list_interval300DAG 디렉터리 스캔 주기DAG 수 < 200이면 30으로 줄여도 무방
parsing_processes2DAG 파싱 병렬 프로세스 수vCPU × 2 권장
worker_concurrency (Celery)16worker당 동시 task 수task 성격(CPU/IO)에 맞게

6. Task 상태 전환 전체 흐름

운영 시 task가 특정 상태에서 막히는 상황을 자주 마주친다. 상태 전환 경로를 알면 원인을 빠르게 좁힐 수 있다.

none scheduled queued running success failed up_for_retry pool 확인 executor 실행 재시도 대기 후 scheduled로 복귀
Task 상태 전환 다이어그램
  • scheduled에서 수 분간 머무는 경우: pool 슬롯 소진, parallelism 한도 초과, 또는 scheduler 루프 지연이 원인.
  • queued에서 머무는 경우: Celery worker가 메시지를 처리하지 못하는 것이 원인. worker 수나 worker_concurrency 확인.
  • running 상태가 너무 오래 지속되는 경우: task 자체의 지연 또는 zombie task(worker 프로세스 사망 후 상태 미갱신). airflow tasks clear로 초기화한다.

마무리: 운영 지식이 설계 결정으로 이어진다

Scheduler 루프, executor 선택, pool 설계는 각각 독립된 주제처럼 보이지만 실제로는 하나의 흐름이다. "이 DAG에서 task가 왜 느리게 시작하는가"라는 질문은 pool 슬롯 부족인지, parallelism 한도인지, Celery worker queue 지연인지, metadata DB 응답 속도인지 순서대로 확인해야 한다. 다음 편에서는 이 흐름 위에서 backfill이 어떻게 작동하고, 어떻게 안전하게 재처리할 수 있는지를 다룬다.

References

  • Apache Airflow Documentation, "Scheduler": https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/scheduler.html
  • Apache Airflow Documentation, "Executor": https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/executor/index.html
  • Apache Airflow Documentation, "Pools": https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/pools.html
  • Apache Airflow Documentation, "Set up a Database Backend": https://airflow.apache.org/docs/apache-airflow/stable/howto/set-up-database.html
  • Astronomer Documentation, "Airflow Executors Explained": https://www.astronomer.io/docs/learn/airflow-executors-explained
  • Astronomer Documentation, "Scaling Airflow to Optimize Performance": https://www.astronomer.io/docs/learn/airflow-scaling-workers
  • Astronomer Documentation, "Understanding the Airflow metadata database": https://www.astronomer.io/docs/learn/airflow-database
  • AWS MWAA Documentation, "Performance tuning for Apache Airflow": https://docs.aws.amazon.com/mwaa/latest/userguide/best-practices-tuning.html
  • Apache Airflow Wiki, "AIP-15 Support Multiple-Schedulers for HA": https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=103092651
  • DeepWiki, "Airflow Executor System": https://deepwiki.com/apache/airflow/13-executor-system