Resumable Tasks와 Durable Execution — 세 가지 접근법
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation core-concepts/resumable-tasks.rst 전체 (Airflow 3.3.0에 추가)
이 모듈을 다 읽으면
- Durable execution이 무엇을 보장하는 개념인지 설명할 수 있다
- Deferrable operator, Resumable task, Async task 세 방식의 트레이드오프를 비교할 수 있다
- 각 방식이 언제 적합한지 판단할 수 있다
Spark 잡, BigQuery 쿼리, K8s 배치 파드처럼 외부 시스템에 작업을 제출하고 폴링하는 태스크는 워커가 죽으면 진행 상황과 함께 중복 제출 위험을 안는다. 이 모듈은 이 문제를 durable execution이라는 개념으로 정의하고, 이를 달성하는 세 가지 접근법(Deferrable Operator, Resumable Task, Async Task)의 트레이드오프를 비교한다.
문제 상황: 외부 시스템에 작업을 제출하고 기다리는 태스크
많은 데이터 엔지니어링 워크플로는 외부 시스템에 작업을 제출하고 완료를 기다린다. Spark 잡, BigQuery 쿼리, Kubernetes 배치 파드, EMR 스텝이 그 예다 — 실제 작업은 Airflow 바깥에서 일어나고, 오퍼레이터의 역할은 대체로 제출·폴링·결과 수집이다.
이런 태스크들은 공통된 실패 패턴을 갖는다. 고전적인 오퍼레이터 방식에서는 폴링이 지속되는 동안 워커 슬롯이 계속 점유되고, 만약 워커 프로세스가 재시작되거나 호스트가 선점(preempt)되면 태스크는 처음부터 재시도되어 그동안의 진행 상황을 모두 잃는다. 오퍼레이터에 따라서는 외부 작업이 다시 제출되어, 외부 시스템 관점에서 중복 실행이 생기기도 한다.
Durable execution(내구성 있는 실행)이란 이 실패 모드를 견뎌내는 것을 뜻한다 — 태스크가 자신을 실행하던 프로세스의 손실을 딛고 살아남아, 이미 끝낸 작업을 반복하거나 외부 시스템에 중복 작업을 제출하는 대신 멈춘 지점에서 이어가거나 재접속할 수 있는 것이다. task state store가 이를 달성하는 메커니즘이다. 이는 워커 크래시 이후에도 살아남고 다음 시도에서 읽을 수 있는 유일한 태스크 인스턴스별 저장소로, 이전 시도가 남긴 체크포인트나 외부 작업 식별자를 재시도가 복구할 수 있게 해준다. Durable execution이 결과라면, task state store는 그것을 얻는 수단이다.
provider 생태계 전반의 오퍼레이터 문서는 이 메커니즘 위에 구축된 오퍼레이터 레벨 기능을 같은 용어로 부르며, 보통 durable 파라미터로 노출한다. Spark, Kubernetes, Databricks, Snowflake, BigQuery, Redshift, Glue 오퍼레이터는 각각 무엇을 저장하고 재시도 시 어떻게 재연결하는지 설명하는 'Durable execution' 섹션을 갖고 있다.
핵심 포인트
- 외부 시스템에 작업을 제출하고 폴링하는 태스크는 워커가 죽으면 진행 상황을 잃고 외부 작업을 중복 제출할 위험이 있다
- Durable execution은 프로세스 손실을 딛고 이전 지점에서 이어가는 것을 의미하며, 그 메커니즘이 task state store다
- 여러 provider 오퍼레이터가 durable 파라미터로 이 기능을 노출한다
Deferrable Operator — 가장 자원 효율적인 방법
Deferrable operator는 폴링을 시작할 지점에서 스스로를 일시정지(defer)하고, 폴링 작업을 Triggerer 컴포넌트에 넘긴 뒤 자신의 워커 슬롯을 반환한다. 외부 조건이 충족되면 Triggerer가 태스크를 깨우고, 워커가 오퍼레이터가 멈췄던 지점부터 다시 시작한다.
이 방식은 가장 자원 효율적인 선택지다. 단일 Triggerer 프로세스가 수천 개의 조건을 동시에 감시할 수 있어, 나머지 워커풀은 다른 태스크를 위해 자유롭게 남는다.
트레이드오프는 다음과 같다. Triggerer 컴포넌트가 반드시 실행 중이어야 한다 — Triggerer가 없는 배포에서는 이 패턴을 쓸 수 없다. 커스텀 deferrable operator를 작성하려면 오퍼레이터 자체 외에 전용 Trigger 클래스도 구현해야 한다. 폴링 로직은 Triggerer의 비동기 이벤트 루프 안에서 동작하므로, Trigger 안에서 블로킹 호출을 하면 Triggerer 프로세스 전체가 멈춰버린다.
이미 자신의 사용 사례에 맞는 deferrable operator가 있거나 팀이 직접 구현할 여유가 있다면, 자원 효율성을 고려했을 때 이 방식이 권장되는 경로다. Airflow 2.2부터 사용 가능하다.
핵심 포인트
- Deferrable operator는 폴링을 Triggerer에 넘기고 워커 슬롯을 반환하는, 가장 자원 효율적인 방식이다
- Triggerer가 없는 배포에서는 사용할 수 없고, Trigger 안의 블로킹 호출은 Triggerer 전체를 멈출 수 있다
- Airflow 2.2부터 사용 가능하다
Resumable Task — 체크포인트 기반 크래시 복구
Resumable task는 진행 상황을 잃기 전에 task state store에 체크포인트를 남겨두는 방식이다. 재시도 시 그 체크포인트를 읽어 멈췄던 지점부터 이어간다. 워커 슬롯은 일반적인 동기 오퍼레이터와 마찬가지로 태스크 실행 내내 점유된다 — 이 방식의 이점은 자원 효율이 아니라 크래시 안전성과 연속성이다.
Resumable task는 다음과 같은 경우에 유용하다: 사용하려는 외부 시스템에 맞는 deferrable operator가 없고 직접 만드는 것도 현실적이지 않을 때, Triggerer를 따로 운영하지 않고도 크래시 복구를 원할 때, 태스크가 증분적으로(예: 파일 목록을 순회하거나 API 결과를 페이지네이션하며) 작업을 처리해 마지막으로 완료한 배치부터 재개할 수 있어야 할 때.
일반적인 패턴은 태스크가 시작 시 task_state_store에서 체크포인트를 읽고, 작업을 수행한 뒤, 갱신된 체크포인트를 쓰고, 계속하거나 종료하는 것이다. 다음 실행(크래시 후 재시도든, 의도적인 재스케줄이든)에서는 체크포인트를 다시 읽어 그 지점부터 이어간다.
외부 작업을 다루는 resumable operator에는 추가 문제가 있다: 태스크가 외부 시스템에 작업을 제출하고 완료를 폴링할 때, 재시도가 발생하면 첫 번째 작업이 아직 실행 중일 수도 있는데 두 번째 작업을 제출해버릴 수 있다. ResumableJobMixin은 폴링을 시작하기 전에 외부 작업 식별자를 먼저 저장해두고, 재시도 시 새 작업을 제출하는 대신 기존 작업에 재연결함으로써 이를 수동으로 처리할 필요를 없앤다.
태스크를 clear하는 것은 재시도와 동일하게 취급된다: 태스크 인스턴스를 clear해도 그 task_state_store 행은 삭제되지 않는다 — dag_run 자체가 삭제되거나 `airflow state-store clean` 명령을 실행할 때만 지워진다. 체크포인트된 태스크에서는 대개 이것이 바람직하다 — clear가 처음부터 다시 시작하는 대신 마지막 체크포인트에서 재개하기 때문이다. durable execution을 쓰는 오퍼레이터의 경우, 이는 이미 외부 작업이 성공한 태스크를 clear하면 저장된 결과를 그대로 읽어 즉시 반환하고 재제출하지 않는다는 뜻이다. 이전 성공 여부와 무관하게 clear가 항상 재제출하게 하려면 `[state_store] clear_on_success = True`를 설정하면 되는데, 이는 태스크가 SUCCESS로 전환될 때 그 태스크의 state store 행을 자동으로 삭제한다. 다만 이것이 재연결할 외부 작업이 여전히 남아있음을 보장하지는 않는다 — 실행 중인(deferrable=False) 태스크를 clear하면 워커 프로세스가 멈추면서 오퍼레이터의 on_kill이 실행되는데, durable execution을 쓰는 대부분의 오퍼레이터는 여기서 기본적으로 외부 작업을 취소하므로 다음 시도는 이미 멈춰진 작업을 만나게 된다 — kill 시 기본적으로 작업을 계속 실행 상태로 두는 오퍼레이터는 예외적이므로 각 문서를 확인해야 한다. 디퍼된 태스크(deferrable=True)는 clear가 방해할 능동적으로 폴링 중인 워커 프로세스가 없으므로 이 문제가 발생하지 않는다.
핵심 포인트
- Resumable task는 워커 슬롯을 계속 점유하지만 task state store 체크포인트로 크래시에서 복구한다 — 목적은 자원 효율이 아니라 크래시 안전성
- ResumableJobMixin은 외부 작업 ID를 먼저 저장해두어 재시도 시 새로 제출하지 않고 기존 작업에 재접속하게 해준다
- 태스크를 clear해도 task_state_store 행은 지워지지 않는다 — dag_run 삭제나 state-store clean 명령으로만 지워지며, [state_store] clear_on_success=True로 성공 시 자동 삭제를 켤 수 있다
- 실행 중(deferrable=False)인 태스크를 clear하면 워커 프로세스가 멈추며 on_kill이 실행되고, 대부분의 durable 오퍼레이터는 여기서 외부 작업을 취소한다
Async Task와 종합 비교
Async task 지원은 Python 태스크 전용이다 — async def로 정의된 @task 데코레이터 함수와 BaseAsyncOperator를 상속하는 클래스형 오퍼레이터에만 적용된다. Airflow는 데코레이트된 콜러블이 async 함수이면 이벤트 루프 안에서 실행하는데, 이를 통해 하나의 태스크 실행 안에서 여러 I/O 작업(HTTP 요청, DB 쿼리, 파일 읽기)을 이벤트 루프를 블로킹하지 않고 동시에 팬아웃할 수 있다.
워커 슬롯은 태스크 실행 내내 점유된다. Async task는 오래 걸리는 외부 대기를 위한 설계가 아니라, 하나의 실행 안에서 완결되는 고처리량 I/O 작업을 위한 설계다. 워커 크래시에서 살아남으려면 async task도 task state store에 aget/aset으로 진행 상황을 체크포인트할 수 있다.
세 방식을 비교하면 다음과 같다: 외부 대기 동안 워커 슬롯 유지 여부는 Deferrable=반환, Resumable=유지, Async=유지. Triggerer 필요 여부는 Deferrable=필요, Resumable=불필요, Async=불필요. 크래시 복구 지원은 Deferrable=지원(Triggerer 경유), Resumable=지원(task state store 체크포인트), Async=미지원. 중복 작업 제출 방지는 Deferrable=오퍼레이터 구현에 따라 다름, Resumable=ResumableJobMixin으로 지원, Async=해당 없음. 동시 I/O 팬아웃 적합성은 Deferrable=부적합, Resumable=부적합, Async=적합. 도입 시점은 Deferrable=Airflow 2.2, Resumable=Airflow 3.3, Async=Airflow 3.2.
핵심 포인트
- Async task는 Python 전용(async def @task, BaseAsyncOperator)이며 이벤트 루프로 동시 I/O 팬아웃에 특화되어 있다 — 외부 대기용이 아니다
- 크래시 복구가 필요한 async task는 aget/aset 비동기 접근자로 체크포인트해야 한다
- 세 방식 중 크래시 복구를 제공하는 것은 Deferrable(Triggerer 경유)과 Resumable(체크포인트)이며, 워커 슬롯을 반환하는 것은 Deferrable뿐이다