← 학습 카테고리

Learn

Airflow

151개 모듈 · 현재 93번째

Airflow 모듈 93/151 airflow-learn-93

스케줄러 성능 튜닝

Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation administration-and-deployment/scheduler.rst (Fine-tuning your Scheduler performance 절)

이 모듈을 다 읽으면

  • 스케줄러 성능에 영향을 주는 자원(DB 연결, CPU, 메모리)별 병목 신호를 설명할 수 있다
  • 주요 스케줄러 튜닝 파라미터가 각각 어떤 트레이드오프를 만드는지 설명할 수 있다
  • PgBouncer가 필요한 상황을 판단할 수 있다

스케줄러 튜닝은 배포 리소스, Dag 구조의 복잡도, 스케줄러 설정이라는 세 축을 두고 병목을 관찰한 뒤 반복적으로 개선하는 작업이다. 문서는 특정 도구를 강권하지 않는 대신, 어떤 자원을 봐야 하는지와 각 설정 파라미터가 어떤 트레이드오프를 만드는지를 안내한다.

튜닝에 영향을 주는 요인

스케줄러 성능에 영향을 주는 요인은 크게 세 가지다. 배포 종류(가용 메모리, CPU, 네트워킹 처리량), Dag 구조의 논리와 정의(Dag 개수, 태스크·의존성 개수로 나타나는 복잡도), 스케줄러 설정(스케줄러 개수, 한 루프에서 처리하는 태스크 인스턴스 수, 루프당 생성/스케줄링할 새 Dag run 수, 정리 작업과 고아 태스크 확인 주기)이다.

튜닝 접근 방식은 일반적인 성능 개선과 동일하다: 평소 쓰는 도구로 시스템을 모니터링하고, 개선하고 싶은 성능 측면을 정하고, CPU/메모리/I/O 같은 병목을 관찰한 뒤, 관찰 결과를 바탕으로 다음 개선안을 정하고 다시 관찰하는 과정을 반복한다.

핵심 포인트

  • 성능은 배포 리소스, Dag 구조 복잡도, 스케줄러 설정 세 축의 조합으로 결정된다
  • 튜닝은 모니터링 → 우선순위 결정 → 병목 관찰 → 개선 → 재관찰의 반복 과정이다

자원별 병목 포인트

DB 연결과 DB 사용량은 병렬 처리량을 늘릴수록 문제가 될 수 있다. Airflow는 "DB 연결에 굶주린" 편이라, Dag가 많고 병렬로 처리하려는 양이 많을수록 더 많은 DB 연결이 열린다. MySQL은 연결 처리가 스레드 기반이라 대체로 문제가 되지 않지만, Postgres는 프로세스 기반이라 중간 규모 이상의 Airflow 설치에서는 문제가 될 수 있다 — 이런 경우 PGBouncer를 DB 앞단 프록시로 두는 것이 일반적인 해법이며, Helm 차트는 PGBouncer를 기본 지원한다.

스케줄러는 인스턴스를 늘릴수록 거의 선형적으로 확장되므로, CPU에 병목이 있다면 스케줄러를 더 추가하는 것도 방법이다. 메모리를 볼 때는 "총 사용 메모리"가 아니라 "작업 메모리(working memory)"를 주의 깊게 봐야 한다(배포 방식에 따라 명칭은 다를 수 있음).

핵심 포인트

  • Airflow는 DB 연결을 많이 소비하며, Postgres는 프로세스 기반 연결 처리라 중간 규모 이상에서 PGBouncer 프록시가 일반적으로 필요하다
  • 스케줄러는 인스턴스 추가에 거의 선형적으로 확장되므로 CPU 바운드일 때 스케줄러를 늘리는 것이 유효하다
  • 메모리는 총 사용량이 아니라 working memory를 기준으로 관찰해야 한다

개선 방향 정하기

자원 사용량을 파악했다면 고려할 수 있는 개선 방향은 다음과 같다. 리소스 활용도 개선: 시스템에 여유 용량이 있는데 활용되지 않고 있다면 스케줄러를 늘리거나 특정 작업의 주기를 더 짧게 해서 성능을 끌어올릴 수 있지만, 그만큼 리소스 이용률은 높아진다. 하드웨어 용량 증설: CPU가 실제로 부족한 경우가 많은데, 이럴 때는 하드웨어를 늘리는 것이 유일한 답일 수 있다 — 예를 들어 머신의 CPU를 이미 다 쓰고 있다면 새 머신에 두 번째/세 번째 스케줄러를 추가하는 것이 스케줄링 용량을 대개 선형적으로 늘려준다(공유 DB 등이 병목이 아닌 한). 튜너블 값 실험: 성능 튜닝은 결국 서로 다른 성능 측면 사이의 균형을 잡는 예술이므로, 여러 "스케줄러 튜너블"을 바꿔가며 효과를 실험하는 것도 방법이다.

핵심 포인트

  • 여유 용량을 더 쓰는 개선(스케줄러 추가, 주기 단축)은 리소스 이용률을 높이는 대가가 따른다
  • CPU가 실제로 부족하면 하드웨어 증설(스케줄러 추가 포함)이 유일한 해법일 수 있으며 대개 선형적으로 확장된다
  • 성능 튜닝은 여러 측면 사이의 트레이드오프를 실험하며 균형을 잡는 과정이다

주요 설정 파라미터와 트레이드오프

``scheduler__max_dagruns_to_create_per_loop``는 스케줄러가 새 Dag run을 만들 때 한 번에 잠그는 Dag 수를 바꾼다. 태스크가 1만 개 이상인 거대한 Dag를 여러 스케줄러로 돌린다면 한 스케줄러가 모든 작업을 독차지하지 않도록 이 값을 낮추는 것을 고려할 수 있다.

``scheduler__max_dagruns_per_loop_to_schedule``는 스케줄러가 한 번에 검사(및 잠금)할 DagRun 개수를 정한다. 이 한도를 늘리면 작은 Dag의 처리량은 늘지만 큰 Dag(예: 500개 이상 태스크)의 처리량은 오히려 느려질 수 있고, 다중 스케줄러 환경에서 너무 높게 설정하면 한 스케줄러가 모든 DagRun을 가져가 다른 스케줄러에는 일이 남지 않게 될 수 있다.

``scheduler__use_row_level_locking``은 관련 쿼리에서 스케줄러가 ``SELECT ... FOR UPDATE``를 발행할지를 정한다. False로 설정하면 한 번에 하나의 스케줄러만 실행해야 한다.

``scheduler__pool_metrics_interval``과 ``scheduler__ti_metrics_interval``은 각각 풀 사용량 통계와 태스크 인스턴스(scheduled/queued/running/deferred) 통계를 StatsD로 보내는 주기(초)다. 계산 비용이 상대적으로 크므로 StatsD의 롤업 주기와 맞추는 것이 좋다.

``scheduler__orphaned_tasks_check_interval``은 고아 태스크나 죽은 SchedulerJob을 확인하는 주기(초)로, 죽은 스케줄러가 얼마나 빨리 감지되어 그것이 감독하던 태스크가 다른 스케줄러에 "입양(adopt)"되는지를 좌우한다. 태스크 자체는 계속 실행 중이므로 이를 늦게 감지해도 큰 문제는 없다. SchedulerJob이 (``scheduler_health_check_threshold``로 판단해) 죽었다고 감지되면, 그 죽은 프로세스가 띄운 실행 중/큐잉된 태스크는 다른 스케줄러가 입양해 모니터링한다.

``scheduler__max_tis_per_query``는 메인 스케줄링 루프 쿼리의 배치 크기로, ``core.parallelism``보다 크게 잡으면 안 된다 — 너무 높으면 쿼리 조건의 복잡도나 과도한 락으로 SQL 성능이 나빠질 수 있고, DB가 허용하는 최대 쿼리 길이에 걸릴 수도 있다. 0으로 설정하면 ``core.parallelism`` 값을 그대로 쓴다.

``scheduler__scheduler_idle_sleep_time``은 루프에서 할 일이 없었을 때 스케줄러가 다음 루프까지 얼마나 잠들지를 정한다(뭔가 스케줄링했다면 바로 다음 루프를 시작한다). 이 파라미터는 이름이 잘못 붙어 있어(역사적 이유) 나중에 이름이 바뀔 예정이다.

핵심 포인트

  • max_dagruns_to_create_per_loop / max_dagruns_per_loop_to_schedule는 거대 Dag와 다중 스케줄러 환경에서 한 스케줄러의 독점을 막기 위해 낮출 수 있다
  • use_row_level_locking=False면 다중 스케줄러 실행이 금지된다(단일 스케줄러만 가능)
  • pool_metrics_interval/ti_metrics_interval은 계산 비용이 커서 StatsD 롤업 주기에 맞추는 것이 좋다
  • max_tis_per_query는 core.parallelism을 넘지 않아야 하며 0이면 core.parallelism 값을 그대로 쓴다
  • orphaned_tasks_check_interval은 죽은 스케줄러의 태스크가 다른 스케줄러에 재입양되는 지연을 결정하지만, 태스크 자체는 계속 실행되므로 지연 자체는 무해하다