[Airflow] 스케줄링의 미학: 00시 정합성과 리소스 분산을 모두 잡는 설계

airflow seamless scheduling with deferrable operators and data intervals

(부제: 논리적 시간(Logical)과 물리적 시간(Physical) 분리하기)

개발자에게 “00시”는 하루의 끝이 아니다. 하루의 시작이자, 가장 예민한 시간이다.

500개가 넘는 배치가 동시에 “나부터 실행시켜줘!”라고 아우성치는 그 시간, 우리 데이터 팀의 슬랙은 항상 불타고 있었다.

1. The Dilemma: 정합성이냐, 효율성이냐 (죽느냐 사느냐)


데이터 엔지니어라면 누구나 겪는 가불기(딜레마)가 있다.

  • 비즈니스 (Logical): “어제 매출 데이터는 오늘 00시 기준으로 마감되어야 하잖아요.” (Data Consistency)
  • 인프라 (Physical): “500개를 동시에 돌린다고? DB 터트릴 일 있어?” (Resource Efficiency)

보통 여기서 우리는 타협한다.

“그래, ODS는 00시, DM은 01시, 리포트는 02시에 돌리자.”

하지만 우리는 이 타협을 거부했다. 대신 Airflow의 메커니즘을 극한으로 쥐어짜서 두 마리 토끼를 다 잡는 변태(?) 같은 아키텍처를 설계했다.

2. Anti-Pattern: 물리적 시간 분산의 함정 (시간차 공격)


가장 쉬운 해결책은 “시간 찢기“다. 하지만 이건 “기술 부채(Technical Debt)“를 일시불로 땡겨 쓰는 것과 같다.

왜냐고? 시간이 달라지는 순간 의존성 지옥(Dependency Hell)이 열리기 때문이다.

  • Sensor 복잡도 떡상: ExternalTaskSensor를 쓸 때마다 족보 정리를 해야 한다.
  • *”이 테이블은 1시간 전, 저 테이블은 30분 전…”* -> DAG 코드가 스파게티가 된다.
  • SQL 쿼리의 오염: Jinja Template에서 시간을 비틀어 계산해야 한다.
  • {{ execution_date - macros.timedelta(hours=1) }} -> 쿼리 짤 때마다 날짜 계산기 두드려야 한다. (오타 나면 대참사)

우리는 이 꼴을 보기 싫어서 “스케줄링 시간은 통일하되, 실행 시점만 지연시키는 패턴“을 도입했다.

3. Architectural Decision: “게으른 실행(Lazy Execution)” 패턴


우리의 핵심 전략은 간단하다.

논리적으로는 00시에 시작하되, 몸뚱아리(Worker)는 필요한 순간까지 움직이지 않는다.

이를 위해 Deferable OperatorData Interval 개념을 섞어 찌개로 끓였다.

💡 잠깐! Data Interval이란?

Airflow 2.2부터 도입된 이 개념은 기존의 헷갈리던 execution_date를 대체하는 “데이터의 시간 범위“에 대한 표준이다.

  • 과거: execution_date가 실제 실행 시간인지, 데이터 기준 시간인지 혼란스러웠음.
  • 현재: data_interval_start ~ data_interval_end로 명확하게 정의됨.
  • 예: 00시부터 01시까지의 데이터를 처리한다면?
  • start: 00:00, end: 01:00
  • 왜 중요한가? 이 시간 범위가 “불변의 기준점(Anchor)“이 되기 때문이다.
  • Sensor가 데이터를 찾을 때도, SQL 쿼리가 날짜를 필터링할 때도 모두 이 data_interval을 기준으로 삼으면, 실제 DAG가 몇 시에 실행되든 상관없이 항상 “00시의 데이터“를 바라볼 수 있다.

Implementation Detail

모든 DAG의 schedule_interval00:00으로 고정한다. (이걸로 논리적 시간은 통일!)

대신, DAG의 가장 앞단에 TimeDeltaSensorAsync를 문지기로 세워둔다.

# 모든 DAG는 00시에 트리거되지만,
# 실제 Worker Slot을 점유하는 Heavy Task는 설정된 delta만큼 지난 후에 시작됨.
wait_task = TimeDeltaSensorAsync(
    task_id="wait_until_target_time",
    delta=timedelta(hours=2) # "02시까지 좀 쉬고 있을게요"
)

이 패턴의 강력함은 “대기하는 동안 Worker Slot을 먹지 않는다“는 점이다.

Airflow Triggerer가 가볍게 상태만 툭툭 체크하다가, 시간이 되면 그제야 Worker에 “야, 일해!” 하고 Task를 던져준다.

4. Key Benefits (이게 왜 개꿀인가?)


  1. Clean Dependency (의존성 관리의 단순화)
  • 모든 DAG가 논리적으로 같은 시간(00:00)을 공유한다. ExternalTaskSensor 쓸 때 delta 계산? 그딴 거 필요 없다. 그냥 바라보면 된다.
  1. Clean Code (SQL/Template의 명확성)
  • 쿼리 내에서 {{ data_interval_start }}를 그대로 쓰면 된다.
  • 억지로 시간을 비틀었다면 SQL 안에서 {{ data_interval_start - macros.timedelta(hours=1) }} 같은 더러운 연산을 덕지덕지 발랐어야 했다.
  1. Resource Efficiency (리소스 효율성)
  • 수백 개의 DAG가 동시에 뜨더라도, 실제 Worker CPU를 사용하는 시점은 00시 ~ 06시로 고르게 퍼진다. (Load Flattening)
  • 00시 정각의 CPU 피크가 사라지고, 아름다운 평지 그래프가 모니터에 그려진다. (마음의 평화는 덤이다.)

5. Conclusion


편하자고 시간을 쪼개지 말자.

당장의 리소스 병목을 피하기 위해 논리적 시간(Logical Time)을 포기하면, 나중에 더 큰 운영 비용을 치르게 된다. (미래의 내가 울고 있을 것이다.)

Airflow의 Deferable Operator를 활용하면, 정합성과 효율성이라는 두 마리 토끼를 우아하게 잡을 수 있다.

나중에 거대한 스파게티 괴물을 마주하고 싶지 않다면, 지금부터 ‘지속 가능한 오케스트레이션(Sustainable Orchestration)‘을 치열하게 고민해야 한다.

6. Reference



Posted

in

, ,

by

댓글 남기기

이 사이트는 Akismet을 사용하여 스팸을 줄입니다. 댓글 데이터가 어떻게 처리되는지 알아보세요.