Airflow 3 환경설정 & 성능 최적화 가이드
설정 우선순위부터 동시성 3계층, Pool 자원 격리, 파싱·DB 튜닝, 병목 진단 체크리스트까지 실전 노브를 정리합니다.
이 글은 "Airflow 3 실전 연재"의 3편입니다. 앞 편 클러스터로 구성하기에서 Scheduler·API server·DAG processor·Triggerer·Worker를 띄웠다면, 이제 그 클러스터가 얼마나 일을 시킬지, 어디서 멈출지를 결정하는 노브들을 만질 차례입니다. 다음 편 DAG 작성의 정석으로 넘어가기 전에, 잘못 돌려두면 클러스터를 통째로 마비시키는 설정값들을 먼저 길들여 둡시다.
대부분의 Airflow 성능 문제는 "서버가 느려서"가 아니라 "내가 모르는 한도에 막혀서" 생깁니다. 태스크가 큐에 쌓이는데 워커는 놀고 있다면, 거의 항상 어딘가의 동시성 한도가 깔때기처럼 흐름을 조이고 있는 겁니다. 이 글의 목표는 그 깔때기들의 위치와 상호작용을 머릿속에 그려서, 증상을 보고 어떤 노브를 돌릴지 바로 떠올릴 수 있게 하는 것입니다.
성능 튜닝은 값을 키우는 게 아니라, 병목이 어디 있는지 아는 것에서 시작합니다.
설정은 어디서 오는가: 우선순위부터
Airflow의 설정 한 줄은 여러 출처에서 올 수 있고, 그중 어느 것이 이기는지를 모르면 "분명히 바꿨는데 안 먹힌다"는 함정에 빠집니다. 동일한 키에 대해 우선순위는 대략 다음 순서입니다(위가 강함).
- 환경변수
AIRFLOW__SECTION__KEY(대문자, 구분자 더블 언더스코어__) - 환경변수의
_CMD/_SECRET변형 (값을 명령 실행 결과나 시크릿 백엔드에서 가져옴) airflow.cfg파일에 명시한 값- Airflow 내장 기본값
핵심 규칙은 이렇습니다: airflow.cfg의 [core] 섹션 parallelism 키는 환경변수로는 AIRFLOW__CORE__PARALLELISM이 됩니다. 섹션명과 키를 대문자로 바꾸고 __로 잇기만 하면 됩니다.
# airflow.cfg 의 다음 두 줄과
# [core]
# parallelism = 64
# 정확히 동일하다 (이쪽이 우선)
export AIRFLOW__CORE__PARALLELISM=64컨테이너 배포라면 환경변수 방식이 사실상 표준입니다.
airflow.cfg를 이미지에 굽지 말고,AIRFLOW__...환경변수로 주입하면 환경별(dev/stage/prod) 분기가 깔끔해집니다.
운영 중 현재 적용값이 헷갈리면 명령으로 확인하세요. airflow config get-value core parallelism 처럼 섹션과 키를 주면 실제 반영된 값을 알려 줍니다(우선순위가 모두 반영된 결과).
동시성 3계층: 일이 막히는 깔때기들
Airflow의 동시성은 한 개의 숫자가 아니라 여러 단의 깔때기입니다. 각 단은 독립적으로 흐름을 조이며, 가장 좁은 단이 실제 처리량을 결정합니다. CeleryExecutor라면 워커 쪽 한도가 한 단 더 붙습니다.
| 노브 | 적용 범위 | 환경변수 | 무엇을 막나 |
|---|---|---|---|
parallelism | 클러스터 전체 | AIRFLOW__CORE__PARALLELISM | 한 스케줄러가 동시에 실행 상태로 둘 수 있는 태스크 인스턴스 총량 |
max_active_tasks_per_dag | DAG 하나당 | AIRFLOW__CORE__MAX_ACTIVE_TASKS_PER_DAG | 한 DAG가 동시에 굴리는 태스크 수 (DAG 인자 max_active_tasks로 개별 override) |
max_active_runs_per_dag | DAG 하나당 | AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG | 한 DAG의 동시 실행(run) 수 (DAG 인자 max_active_runs로 override) |
worker_concurrency | Celery 워커 하나당 | AIRFLOW__CELERY__WORKER_CONCURRENCY | 워커 프로세스 한 대가 동시에 잡는 태스크 수 |
세 개의 core 노브는 기본값(설정 파일/환경변수)이자 상한이고, DAG·태스크 단위 인자(max_active_runs, max_active_tasks, 그리고 Pool)로 더 좁게 조일 수 있습니다. 넓게는 못 벌립니다 — parallelism이 천장이니까요.
이 한도들이 하나의 태스크가 실제로 실행되기까지 어떻게 차례로 게이팅하는지를 깔때기로 그리면 이렇습니다.
여기서 가장 흔한 실수는 계층 간 산수가 안 맞는 것입니다. 예를 들어(예시 수치입니다):
- 워커 4대 ×
worker_concurrency=16= 동시 64 태스크를 처리할 수 있는데 parallelism=32로 묶여 있으면 → 워커 절반은 늘 논다.- 반대로
parallelism=256인데 워커 용량이 64뿐이면 → 스케줄러는 256개를 "실행 중"으로 보지만 브로커 큐에 192개가 적체된다.
대략의 출발점:
parallelism≈ (워커 수 ×worker_concurrency) 에 맞추고, 거기서 부하를 보며 조정하세요. 두 숫자가 따로 노는 게 가장 흔한 미스튜닝입니다.
Pool + priority_weight: 자원을 격리한다
max_active_tasks_per_dag는 "한 DAG 안에서" 조이는 노브지, 여러 DAG가 같은 외부 자원을 두드릴 때는 무력합니다. 30개 DAG가 동시에 같은 외부 API를 호출하면 그 API가 먼저 쓰러집니다. 이럴 때 쓰는 게 Pool입니다.
Pool은 이름 붙은 슬롯 묶음입니다. 태스크에 pool="external_api"를 지정하면, 그 풀의 슬롯이 빌 때까지 태스크는 queued 상태로 대기합니다. 어느 DAG에서 왔든 상관없이, 같은 풀을 공유하는 모든 태스크가 하나의 한도 아래로 모입니다.
from airflow.sdk import dag, task
import pendulum
@dag(schedule="@hourly", start_date=pendulum.datetime(2026, 1, 1), catchup=False)
def crm_sync():
# 외부 CRM API는 동시 5 요청까지만 견딘다 → 전용 풀로 격리
@task(pool="external_api", pool_slots=1, priority_weight=10)
def fetch_accounts():
...
fetch_accounts()
crm_sync()pool_slots: 무거운 태스크는 슬롯을 2개 이상 잡게 해서 "한 칸 = 한 작업"이 아니라 비용에 비례하게 만들 수 있습니다.priority_weight: 같은 풀에서 슬롯을 두고 경쟁할 때 누구를 먼저 꺼낼지를 정합니다. 숫자가 클수록 우선. 빈 슬롯이 생기면 큐에서 가중치가 높은 태스크부터 실행됩니다.
요청이 들어와 실행되기까지 Pool이 어떻게 게이트 역할을 하는지, 그리고 우선순위가 어떻게 끼어드는지 흐름으로 보면 이렇습니다.
외부 시스템(DB, 결제 게이트웨이, 사내 레거시 API)을 부르는 태스크는 거의 항상 전용 Pool에 넣으세요. Pool은 "이 자원은 동시에 N개까지만"이라는 약속을 코드 밖에서 강제하는 가장 단순한 안전장치입니다. 외부 시스템 연동의 더 깊은 패턴은 8편 외부 시스템 연동 & 싱크 호출에서 다룹니다.
DAG 파싱 성능: 스케줄러가 숨 쉴 틈
Airflow 3에서는 DAG 파싱이 DAG processor라는 독립 프로세스로 분리되어, 무거운 파싱이 더 이상 스케줄러의 스케줄링 루프를 직접 잡아먹지 않습니다(아키텍처 배경은 1편 아키텍처 해부 참고). 그래도 파싱이 느리면 새 DAG/변경 반영이 늦고, run 생성이 지연됩니다.
| 노브 | 환경변수 | 의미 | 튜닝 방향 |
|---|---|---|---|
min_file_process_interval | AIRFLOW__DAG_PROCESSOR__MIN_FILE_PROCESS_INTERVAL | 같은 DAG 파일을 다시 파싱하기까지 최소 간격(초) | DAG 수가 많고 자주 안 바뀌면 늘려서 파싱 부하를 줄임 |
dag_dir_list_interval | AIRFLOW__DAG_PROCESSOR__DAG_DIR_LIST_INTERVAL | DAG 디렉터리를 스캔해 새 파일을 찾는 주기(초) | 새 DAG가 자주 추가되지 않으면 늘려도 됨 |
parsing_processes | AIRFLOW__DAG_PROCESSOR__PARSING_PROCESSES | DAG 파일을 병렬 파싱하는 프로세스 수 | DAG 파일이 매우 많으면 늘려서 처리량 확보 |
하지만 가장 효과 큰 튜닝은 설정이 아니라 DAG 코드 작성 방식입니다. top-level 코드(함수 밖, import 직후에 실행되는 코드)는 DAG 파일이 파싱될 때마다 — 즉 위 간격마다 반복적으로 — 실행됩니다. 여기서 무거운 일을 하면 파싱이 통째로 느려집니다.
# 나쁨: top-level 에서 매 파싱마다 외부 호출이 실행된다
import requests
config = requests.get("https://config-server/limits").json() # 매 파싱마다 네트워크 호출!
# 좋음: 무거운 작업은 태스크 안으로 (실행 시점에만 1회)
from airflow.sdk import task
@task
def load_config():
import requests
return requests.get("https://config-server/limits").json()규칙: DAG 파일의 top-level에서는 DB 조회·API 호출·무거운 import를 하지 마세요. "DAG를 정의"하는 일만 하고, "일을 하는" 코드는 전부 태스크 안으로 넣습니다. 이 원칙은 4편 DAG 작성의 정석에서 더 파고듭니다.
메타데이터 DB 커넥션 풀 & 로그 보존
스케줄러·DAG processor·API server는 모두 메타데이터 DB를 두드립니다(Airflow 3에서 워커/태스크는 직접 DB에 붙지 않고 Task Execution API를 경유하므로, DB 커넥션 압박은 주로 스케줄러 쪽입니다). SQLAlchemy 커넥션 풀이 좁으면 컴포넌트가 커넥션을 기다리며 멈춥니다.
| 노브 | 환경변수 | 의미 |
|---|---|---|
sql_alchemy_pool_size | AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_SIZE | 컴포넌트가 유지하는 상시 커넥션 수 |
sql_alchemy_max_overflow | AIRFLOW__DATABASE__SQL_ALCHEMY_MAX_OVERFLOW | 피크 시 풀을 넘어 임시로 더 열 수 있는 커넥션 수 |
sql_alchemy_pool_recycle | AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_RECYCLE | 커넥션을 강제로 재생성하는 주기(초) — 죽은 커넥션 방지 |
주의: 여러 스케줄러를 띄우면 커넥션이 컴포넌트 수만큼 곱해집니다. DB의 max_connections가 (스케줄러 수 + DAG processor + API server) × (pool_size + max_overflow)를 감당하는지 확인하세요. 안 그러면 DB가 "too many connections"로 거절합니다.
로그도 방치하면 디스크를 채웁니다. Airflow 3에는 오래된 메타데이터·로그를 정리하는 airflow db clean 명령이 있고, 보존 기간을 정해 주기적으로 돌리는 것이 표준입니다.
# 90일보다 오래된 run/log 메타데이터를 정리 (cron 등으로 주기 실행)
airflow db clean --clean-before-timestamp "2026-03-27 00:00:00+00:00"흔한 병목 진단 체크리스트
증상을 보고 어떤 노브를 돌릴지 바로 찾는 의사결정 흐름입니다. 위에서 아래로 "어디가 좁은가"를 좁혀 갑니다.
빠르게 짚을 수 있는 체크리스트로 정리하면 이렇습니다.
- 큐 적체 + 워커 유휴 →
parallelism이 워커 용량보다 작지 않은지. (parallelism≈ 워커수 ×worker_concurrency) - 특정 풀만 항상 꽉 참 → 그 풀의 슬롯을 늘리거나, 정말 자원 한도면 그대로 두고
priority_weight로 급한 태스크를 앞당김. - 한 DAG만 직렬처럼 느림 → 그 DAG의
max_active_tasks/max_active_runs가 1이나 낮은 값으로 묶여 있지 않은지. - 새 DAG가 늦게 뜸 / 변경 반영 지연 → top-level 무거운 코드 제거가 1순위, 그다음
parsing_processes·min_file_process_interval점검. - 스케줄러 CPU 포화 / DB "too many connections" → 커넥션 풀과 DB
max_connections정합성 확인. - deferrable로 바꿀 수 있는 긴 대기(센서 등)가 워커 슬롯을 점유 → Triggerer로 옮겨 슬롯을 비움(자세한 패턴은 4편에서).
튜닝의 순서는 항상 관측 → 가장 좁은 깔때기 식별 → 한 번에 한 노브입니다. 여러 값을 동시에 키우면 무엇이 효과였는지 알 수 없고, 곧 다른 곳(DB·브로커)이 새 병목이 됩니다.
마무리
Airflow 3의 성능 튜닝은 마법 같은 단일 설정이 아니라, 여러 단의 깔때기를 정렬하는 일입니다. 설정 우선순위(AIRFLOW__... 환경변수 > airflow.cfg)를 알고, 동시성 3계층과 워커 한도의 산수를 맞추고, 외부 자원은 Pool로 격리하고, top-level 코드를 비워 파싱을 가볍게 하고, DB 커넥션과 로그를 관리하면 — 대부분의 "느려요"는 노브 하나로 풀립니다.
정확한 키 이름과 기본값은 버전에 따라 바뀔 수 있으니, 확정하기 전 Airflow 공식 설정 레퍼런스에서 한 번 더 확인하세요. 다음 편 DAG 작성의 정석에서는, 이 글에서 미룬 "top-level 코드를 어떻게 비우고 태스크를 어떻게 짜는가"를 본격적으로 다룹니다.