Web 스택

Web 스택 (Web Stack)

이 페이지는 Airflow의 API 서버(Web Stack)를 배포하는 방법을 다뤄요. URL 경로 접두사(base_url) 구성, Core API Server와 Execution API Server 분리하기, uvicorn(기본)과 gunicorn 두 서버 타입, 그리고 알려진 이슈(PYTHONASYNCIODEBUG/PYTHONDEVMODE 비호환)를 설명해요.

출처: 문서

본문

설정

때로는 백엔드와 프론트엔드를 변수 URL 경로 접두사 뒤에 배포하고 싶을 수 있어요. 그러려면 base_url을 구성하면 돼요. 예를 들어 http://localhost:28080/d12345로 설정해요. 이제 모든 API 라우트가 그 추가 d12345 접두사를 통해 사용 가능해져요. 프론트엔드를 다시 빌드하지 않고도 XHR 요청과 정적 파일 쿼리는 접두사가 붙은 URL로 전달되어 성공적으로 서빙돼요.

또한 Task가 새 접두사로 API에 도달할 수 있도록 execution API 서버 url인 execution_api_server_url도 업데이트해야 해요.

API 서버 분리하기

기본적으로 Core API Server와 Execution API Server는 함께 서빙돼요:

airflow api-server
# same as
airflow api-server --apps all
# or
airflow api-server --apps core,execution

Core API Server와 Execution API Server를 분리하고 싶다면, 각각 따로 실행할 수 있어요. 이는 독립적으로 확장하거나 다른 머신에 배포할 때 유용할 수 있어요.

# serve only the Core API Server
airflow api-server --apps core
# serve only the Execution API Server
airflow api-server --apps execution

알려진 이슈

PYTHONASYNCIODEBUGPYTHONDEVMODE 비호환

Warning

환경 변수 PYTHONASYNCIODEBUG=1가 설정되거나 Python 3.12 이상에서 PYTHONDEVMODE로 실행할 때 API 서버가 segmentation fault로 크래시할 수 있어요. 이는 uvloop(성능 향상을 위해 Uvicorn이 사용)와 Python의 asyncio 디버그 모드 사이의 비호환 때문이에요.

API 서버에서 asyncio 이슈를 디버깅해야 한다면 다음을 고려해 보세요:

  • PYTHONASYNCIODEBUGPYTHONDEVMODE에 의존하기보다 애플리케이션 레벨에서 디버깅하기
  • uvloop이 설치되지 않은 개발 환경을 설정하기

이는 issue #61214에서 추적되는 알려진 제한 사항이에요.

서버 타입

API 서버는 uvicorn(기본)과 gunicorn 두 가지 서버 타입을 지원해요.

Uvicorn (기본)

Uvicorn은 기본 서버 타입이에요. 설정이 간단하고 Windows를 포함한 모든 플랫폼에서 작동해요.

airflow api-server

Gunicorn

Gunicorn은 production 배포를 위한 추가 기능을 제공해요:

  • 메모리 공유: fork 후 copy-on-write로 worker가 메모리를 공유해 총 메모리 사용량을 줄여요.
  • 롤링 워커 재시작: 메모리 누적을 방지하는 zero-downtime worker 재활용.
  • 올바른 시그널 처리: SIGTTOU가 가장 오래된 worker(FIFO)를 종료해 진정한 롤링 재시작을 가능하게 해요.

Note

Gunicorn은 gunicorn extra가 필요해요: pip install 'apache-airflow-core[gunicorn]'

Gunicorn은 Unix 전용이며 Windows에서는 작동하지 않아요.

gunicorn 모드를 활성화하려면:

export AIRFLOW__API__SERVER_TYPE=gunicorn
airflow api-server

롤링 워커 재시작

주기적인 worker 재활용을 활성화하려면(메모리 누적을 방지하려는 장기 실행 프로세스에 유용):

export AIRFLOW__API__SERVER_TYPE=gunicorn
export AIRFLOW__API__WORKER_REFRESH_INTERVAL=43200  # Restart workers every 12 hours
export AIRFLOW__API__WORKER_REFRESH_BATCH_SIZE=1   # Restart one worker at a time
airflow api-server

롤링 재시작 과정:

  1. 새 worker를 먼저 띄운 후 오래된 worker를 종료해요 (zero downtime)
  2. 새 worker가 준비될 때까지 기다려요 (프로세스 제목 확인)
  3. 워커가 요청을 서빙할 수 있는지 HTTP 상태 확인을 수행해요
  4. 오래된 worker를 종료해요 (가장 오래된 것부터)
  5. 모든 원래 worker가 교체될 때까지 반복해요

설정 옵션

[api] 섹션에서 다음 설정 옵션을 사용할 수 있어요:

  • server_type: uvicorn(기본) 또는 gunicorn
  • worker_refresh_interval: worker 새로고침 주기 사이의 초 (0 = 비활성화, 기본값)
  • worker_refresh_batch_size: 주기마다 새로고침할 worker 수 (기본값: 1)
  • dag_cache_size: API 서버에서 캐시할 최대 SerializedDAG 버전 수 (기본값: 64, 0 = 크기 제한 없음)
  • dag_cache_ttl: 캐시된 DAG의 TTL(초) (기본값: 3600, 0 = TTL 없음; 둘 다 0 = 제거 없음)

Gunicorn을 언제 사용할까요

다음이 필요할 때 gunicorn을 사용해요:

  • 메모리 누적이 우려되는 장기 실행 API 서버 프로세스
  • 메모리 공유가 중요한 멀티 워커 배포
  • zero-downtime worker 재활용이 필요한 production 환경

다음의 경우에는 기본 uvicorn을 사용해요:

  • Windows에서 실행
  • 개발·테스트 환경에서 실행
  • 수명이 짧은 컨테이너 실행 (예: 재활용되는 Kubernetes pod)

Kubernetes에서 Uvicorn 실행하기

Kubernetes에서 server_type = uvicorn으로 API 서버를 실행하면, API 서버는 pod당 단일 장기 실행 프로세스로 실행되며 gunicorn처럼 롤링 워커 재시작을 지원하지 않아요.

장기 실행 Kubernetes 배포에서 이는 시간이 지나면서 점진적인 메모리 증가나 오래된 내부 상태를 초래할 수 있어요. 이런 이유로 uvicorn을 사용할 때는 API 서버 pod를 주기적으로 재시작하는 것을 권장해요.

권장 접근 방식:

  • API 서버 Deployment의 Kubernetes 롤링 재시작으로 중단 없이 pod를 재활용.
  • Helm 업그레이드 중 rollout을 트리거하거나 restart annotation을 바꾸는 것 같은 Helm 기반 재시작.
  • uvicorn을 오래 실행할 때 클러스터 레벨 매커니즘(예: 예약된 재시작).

예를 들어 API 서버 pod의 롤링 재시작을 트리거하려면:

kubectl rollout restart deployment airflow-api-server

API 서버는 또한 dag_cache_sizedag_cache_ttl을 통해 캐시된 SerializedDAG 객체를 제거하는데, 이는 서버 타입과 무관하게 Dag 버전 누적으로 인한 메모리 증가를 줄여요. dag_cache_size만 메모리를 확실히 한정한다는 점을 주의하세요. 캐시 항목의 TTL은 매 요청마다가 아니라 [core] min_serialized_dag_update_interval 후에 항목이 데이터베이스에 대해 확인될 때만 갱신돼요. TTL이 그 간격보다 짧으면, 자주 요청되는 항목도 확인 사이에 만료되어 다시 로드될 수 있어요.

많은 Kubernetes 환경에서 Kubernetes OOM kill이나 크래시 재시작에만 의존하는 것은 권장되지 않아요. 메모리 증가가 항상 OOM 이벤트를 트리거하지는 않을 수 있기 때문이에요. pod 재시작 없이 자동 worker 재활용이 필요한 production 배포에는 server_type = gunicorn을 사용하는 것을 고려해 보세요.

더 알아보기 (Learn more)