[Troubleshooting] 00시 정각의 비극: 클라우드의 배신과 네트워크의 역습 (MWAA & Glue)

“클라우드는 무한하지 않다. 특히 내 지갑과 IP 주소, 그리고 오토스케일링 속도는 더더욱.”

1. 00시 정각, 대혼돈의 서막


우리 팀은 AWS MWAA(Managed Workflows for Apache Airflow)와 AWS Glue를 사용해 ODS 데이터를 적재한다.
테이블 500개, 관련 Task만 2,000개가 넘는 이 거대한 파이프라인이 매일 00시 00분 00초에 동시에 출발 신호탄을 쏘아 올린다.

이론상으로는 아름다워야 했다.
MWAA는 알아서 오토스케일링이 되고, Glue는 서버리스니까 그냥 돈만 내면 다 돌아가야 하는 거 아닌가?

하지만 현실은 시궁창이었다.

💡 팩트 체크: MWAA 오토스케일링은 왜 느린가?

MWAA는 백그라운드에서 AWS Fargate 기반의 컨테이너로 Celery Worker를 띄운다. 공식 문서에 따르면 오토스케일링은 Running TasksQueued Tasks의 비율을 모니터링하여 동작한다. 하지만 여기엔 함정이 있다.

  1. 반응 속도: 큐가 찼다고 바로 워커가 뜨지 않는다. 지표가 임계치를 넘어야 트리거된다.
  2. 콜드 스타트 (Cold Start): Fargate 컨테이너가 프로비저닝(Pending)되고, Airflow 이미지를 풀링하고, 초기화 스크립트가 돌고, 비로소 워커가 Ready 상태가 되어 작업을 받아가기까지 수 분에서 십수 분이 걸린다.
  3. 결과: 00시에 수백 개의 태스크가 큐에 쏟아지는데, 워커는 한 세월 뒤에나 도착한다. 그동안 태스크들은 큐에서 하염없이 기다린다.

설상가상으로 네트워크의 역습 (IP 고갈)이 시작됐다.
Glue Job 하나가 뜰 때마다 ENI(Elastic Network Interface)를 하나씩 먹는데, 지정된 서브넷의 IP가 동나버렸다.

  • 로그: ENICreateFailed: Subnet ip address limit exceeded
  • 결과: Glue 잡이 뜨지도 못하고 죽거나, 무한 재시도(Retry)에 빠짐.

결국 ODS 적재가 다 끝나는 데 2~3시간이 걸렸다.

2. 해결책: “줄을 서시오! (Feat. Async Sensor)”


가장 좋은 건 서브넷을 늘리고(IP 확보), MWAA 풀(Pool)을 세밀하게 쪼개서 제어하는 것이지만, 이미 운영 중인 환경에서 서브넷 변경은 대공사였고 Airflow Pool은 default_pool 하나로 뭉뚱그려져 있어 당장 손대기 어려웠다.

결국 해답은 “DAG 코드 레벨에서 똑똑하게 줄을 세우는 것”이었다.

🛠️ Priority Weight & TimeDeltaSensorAsync

1. 줄 세우기 (Priority Weight)
먼저 테이블의 중요도에 따라 priority_weight를 부여했다.

  • Tier 1 (핵심 거래): 1000
  • Tier 2 (마스터): 500
  • Tier 3 (기타): 100

이렇게 하면 큐가 터져나갈 때, 워커는 무조건 Tier 1 태스크부터 집어간다.

2. 대기실 만들기 (TimeDeltaSensorAsync)
모든 Task가 워커 슬롯을 차지하고 대기하면 데드락이 발생할 수 있다. 그래서 워커 리소스를 거의 쓰지 않는 Async Sensor를 활용했다.

  • 일반 TimeDeltaSensor는 대기하는 동안에도 워커 슬롯(1개)을 차지한다. (낭비)
  • TimeDeltaSensorAsync는 Triggerer에게 위임하고 워커 슬롯을 반환한다. (효율적)

테이블별로 계산된 우선순위에 따라 execution_delta를 다르게 주어, 물리적인 실행 시점을 강제로 분산(Throttling)시켰다.

3. 결과: “실패 비용(Retry Cost)을 없애니 2배 빨라졌다”


스로틀링을 적용하여 동시 실행 개수를 줄였는데, 아이러니하게도 전체 완료 시간은 2~3시간에서 1시간 이내로 단축되었다. 이유는 명확하다.

[Before: 악순환의 고리]

  1. 00시에 600개 동시 실행.
  2. 서브넷 IP 고갈로 100개 실패.
  3. Airflow가 실패한 100개를 재시도(Retry). (재시도 딜레이 발생)
  4. 재시도된 태스크가 다시 큐 맨 뒤로 가서 줄 섬.
  5. 그 사이 워커는 또 다른 태스크를 처리하려다 또 IP 고갈.
  6. (반복) 무한 재시도와 대기 시간의 누적.

[After: 선순환]

  1. 중요도 순으로 적정량만 실행.
  2. IP 여유가 있으니 실패 없이 한 번에 성공(Straight Pass).
  3. 재시도 비용(딜레이+큐 대기)이 0이 됨.

결국 “속도”를 갉아먹던 주범은 “실패와 재시도”였던 것이다. 시스템이 감당할 수 있는 속도로 던져주는 것이, 무작정 들이붓는 것보다 훨씬 빠르다는 리틀의 법칙(Little’s Law)을 증명한 사례였다.

📅 다음 편 예고: “왜 굳이 00시에 다 트리거하나요?”


이쯤 되면 드는 의문이 있을 것이다.
“애초에 DAG 스케줄(schedule_interval)을 00:05, 00:10 으로 분산시키면 되지 않나요? 왜 굳이 00시에 다 트리거하고 센서로 막나요?”

여기엔 피치 못할 사정, Data Dependency와 Monolithic DAG 구조의 비밀이 있다.
다음 편에서 계속…

  • 다음 주제: 2000개의 태스크를 하나의 DAG, 하나의 시점(00:00)으로 관리해야만 했던 이유 (데이터 의존성의 늪)


Posted

in

, ,

by

댓글 남기기

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