본문 바로가기
Airflow

Airflow DAG가 매번 다른 코드로 실행됐다 — pycache가 아닌 진짜 원인

by Soono991 2026. 5. 5.
TL;DR
- 증상: 새 코드를 배포했는데 실행 시 새 코드와 옛 코드가 랜덤하게 실행
- 처음 의심: pycache의 stale 바이트코드
- 진짜 원인: 배포 스크립트가 파일 복사(replace) 방식이라 rename된 옛 파일이 그대로 잔존
           → Airflow가 동일 dag_id 파일 2개 중 랜덤 선택
- 해결: rsync --delete로 동기화 + (예방) PYTHONDONTWRITEBYTECODE=1

이런 분께: Airflow를 docker-compose로 운영하면서 DAG 배포 후 이상한 동작을 본 적 있는 분

 

상황 (Context)

  • 환경: EC2 + docker-compose Airflow, dags 디렉토리는 호스트 볼륨 마운트
  • 배포 방식: Jenkins → docker stop → rm → run. dags 디렉토리는 파일 단위 복사(replace) 방식으로 갱신
  • 시점: 특정 DAG의 호출 방식을 SSH → curl 도메인 으로 리팩토링한 직후

마주한 문제

배포 직후 Airflow UI에서는 새 코드(curl)가 정상적으로 표시됐다. 그런데 DAG를 실제로 돌려보면:

  • 어떤 실행에서는 curl 방식으로 정상 동작
  • 다른 실행에서는 옛날 SSH 방식으로 동작 (대부분 실패)
  • 같은 DAG가 매번 다른 코드로 도는 것처럼 보임

UI는 새 코드를 보여주는데 실행은 옛 코드 — 이게 가장 헷갈리는 지점이었다.

가설과 시도 — what didn't work

가설 1. Python 바이트코드 캐시(pycache)

docker stop → rm → run 으로 컨테이너는 새로 뜨지만, 호스트 볼륨에 마운트된 pycache의 .pyc 파일은 유지된다는 점이 의심됐다. 컨테이너 재시작이 파이썬 import 캐시는 비워줘도, 디스크의 .pyc 자체는 그대로 남으니까.

확인 명령:

# .pyc 존재 확인
find /opt/airflow/dags -name "*.pyc" | wc -l

# .pyc 안에 옛날 코드(SSH) 흔적이 남아있는지
strings /opt/airflow/dags/__pycache__/<dag>.cpython-*.pyc | grep -i "ssh"

→ 결과: .pyc 안에 SSH 문자열이 없거나, .pyc 자체가 없는 경우도 있었음. 이 가설로는 "랜덤하게 두 코드가 번갈아 실행되는" 현상이 설명되지 않았다. .pyc가 stale이면 항상 옛 코드가 나와야지, 둘 사이를 오갈 이유가 없다.

가설 2. scheduler/worker 메모리 캐시

scheduler·worker 컨테이너가 옛 모듈을 들고 있는 것 아닐까? 했지만 컨테이너를 완전히 재시작하면 파이썬 인터프리터 자체가 새로 뜬다. 그런데도 옛 코드가 나온다는 건 디스크 어딘가에 옛 코드가 살아있다는 뜻.

이 시점에 dags 디렉토리를 직접 들여다봤다.

원인 분석

dags 디렉토리에 동일한 dag_id를 가진 파일이 2개 존재했다.

리팩토링하면서 파일을 rename한 게 있었다:

a.py  →  a-1.py

문제는 배포 스크립트가 파일 단위 복사(replace) 방식이었다는 점:

  • 신규 파일 → 생성 ✓
  • 동일 경로의 기존 파일 → 덮어쓰기 ✓
  • 삭제/rename으로 사라진 파일 → 처리 안 함 → 그대로 잔존

배포 후 dags 디렉토리 실제 상태:

dags/
  a.py     ← 삭제됐어야 하지만 그대로 남음 (옛 SSH 코드)
  a-1.py   ← 새로 생성됨 (새 curl 코드)

두 파일이 같은 dag_id를 선언하고 있었기 때문에:

  • Airflow는 dag_id 중복을 silent하게 처리한다. 에러를 띄우지 않고
  • 실행 시 둘 중 하나를 랜덤하게 선택해서 돌린다.

이게 "매번 다른 코드로 실행"의 정체였다. pycache는 무관했다.

해결

배포 스크립트가 dags 디렉토리를 동기화(sync) 방식으로 갱신하도록 변경.

rsync로 동기화

rsync -av --delete ./dags/ /home/ubuntu/airflow/dags/

--delete: source에 없는 파일은 destination에서도 삭제. 이게 핵심.

또는 디렉토리 완전 교체

rsync를 못 쓰는 환경이면:

rm -rf /home/ubuntu/airflow/dags/*
cp -r ./dags/* /home/ubuntu/airflow/dags/

배포 후 ls dags/로 의도한 파일만 있는지 한번 확인하면 더 안전.

DAG 작성 방법 변경 — rename 자체를 없애기

운영 측 해결(rsync --delete)에 더해, 코드 작성법 자체를 바꿔서 rename이 발생하지 않게 만드는 것이 근본적 예방이다.

기존엔 test_beta.py, test_live.py 처럼 환경·실행시각별로 파일을 분리하다 보니, 시각이 추가되거나 환경이 바뀔 때마다 파일이 새로 생기거나 rename되곤 했다. 이번 사고도 거기서 비롯됐다. 이걸 test.py 한 파일 안에서 리스트로 환경·시각·dag_id를 관리하도록 바꾸면 파일명 자체가 바뀔 일이 사라진다.

# (dag_id, schedule_interval(UTC), env)
dag_configs = [
    # KST: 07:00 월~토 → UTC: 0 22 * * 1-6
    ("daily_report_0700_beta", "0 22 * * 1-6", "BETA"),
    ("daily_report_0700",      "0 22 * * 1-6", "LIVE"),
    # KST: 08:00 월~토 → UTC: 0 23 * * 1-6
    ("daily_report_0800_beta", "0 23 * * 1-6", "BETA"),
    ("daily_report_0800",      "0 23 * * 1-6", "LIVE"),
    # KST: 09:30 월~토 → UTC: 30 0 * * 1-6
    ("daily_report_0930_beta", "30 0 * * 1-6", "BETA"),
    ("daily_report_0930",      "30 0 * * 1-6", "LIVE"),
]

이 리스트를 순회하면서 환경별·시각별로 DAG 객체를 동적으로 생성한다. 파일은 항상 test.py 하나로 유지되고, 새 스케줄이나 환경 추가는 리스트에 항목 한 줄 추가로 끝난다 — 파일 단위 복사 방식 배포에서도 정상적으로 replace 처리됨.

두 해결책은 상호 보완적: rsync --delete는 pycache 같은 다른 종류의 stale file도 막아주고, 코드 작성법 변경은 rename 빈도 자체를 낮춰 사고 발생률을 줄인다.

배운 점

1. Airflow는 dag_id 중복을 silent하게 처리한다.
다른 프레임워크와 달리 에러를 띄우지 않고 둘 중 하나를 랜덤 선택한다. UI에는 둘 중 한 버전만 노출되니까 더 헷갈림. 배포 후에는 dags 디렉토리에 의도한 파일만 남았는지 직접 확인하는 습관이 필요하다.

2. 배포 스크립트는 "동기화(sync)"여야 한다 — "복사(copy)"가 아니라.
파일이 추가/수정만 되는 워크로드라면 copy도 무방하지만, rename·삭제가 한 번이라도 발생하는 순간 함정이 된다. rsync --delete나 디렉토리 완전 교체가 정공법.

3. (예방) PYTHONDONTWRITEBYTECODE=1
이번 사건의 직접 원인은 아니었지만, pycache stale 바이트코드는 별도의 함정으로 분명히 존재한다. docker-compose에 환경변수 추가해서 .pyc 생성 자체를 막아두는 걸 권장.

environment:
  &airflow-common-env
  PYTHONDONTWRITEBYTECODE: "1"

기존 .pyc까지 정리하려면 배포 스크립트에:

find /home/ubuntu/airflow/dags -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null

참고

댓글