트리거러 고가용성과 큐 기반 호스트 할당, reschedule과의 비교
Apache Airflow Official Documentation (in-repo snapshot) — Apache Software Foundation authoring-and-scheduling/deferring.rst - High Availability, Balance the workload for HA Triggerers, Controlling Triggerer Host Assignment Per Trigger, Difference between Mode='reschedule' and Deferrable=True in Sensors (약 472-607줄)
이 모듈을 다 읽으면
- 트리거러 HA 구성에서 트리거 재배치와 중복 실행 처리 방식을 설명할 수 있다
- max_trigger_to_select_per_loop와 capacity 설정의 관계를 설명할 수 있다
- --queues 옵션으로 트리거를 특정 트리거러 호스트 집합에 고정하는 방법을 설명할 수 있다
- mode='reschedule'과 deferrable=True의 차이를 시나리오별로 구분할 수 있다
트리거러는 여러 호스트에서 HA로 실행되며 하트비트로 서로를 감시해 죽은 호스트의 트리거를 자동 재배치하고, max_trigger_to_select_per_loop로 루프당 가져가는 트리거 수를 제한해 부하를 고르게 분산하며, --queues 옵션으로 특정 트리거를 특정 트리거러 호스트 집합에만 고정할 수 있다.
트리거러 HA와 재배치
트리거는 고가용성(HA) 아키텍처에서 동작하도록 설계되어 있다. HA로 운영하려면 여러 호스트에서 `triggerer`를 여러 개 띄우면 되며, `scheduler`처럼 올바른 락킹과 HA로 자동으로 서로 공존한다.
트리거가 하는 작업량에 따라 트리거러 한 대가 수백에서 수만 개의 트리거를 동시에 처리할 수 있다. 기본적으로 각 `triggerer`는 한 번에 최대 1000개의 트리거를 시도할 수 있는 용량(capacity)을 가지며, `--capacity` 인자로 이를 바꿀 수 있다. 모든 트리거러의 용량 합계보다 실행하려는 트리거가 더 많으면, 일부 트리거는 다른 트리거가 끝날 때까지 실행이 지연된다.
Airflow는 각 트리거를 오직 한 곳에서만 실행하려 시도하며, 현재 실행 중인 모든 `triggerer`에 하트비트를 유지한다. `triggerer`가 죽거나 Airflow DB가 있는 네트워크로부터 파티션되면, Airflow는 자동으로 해당 호스트에 있던 트리거들을 다른 곳으로 재스케줄링한다 - 이때 머신이 다시 나타나기를 `(2.1 * triggerer.job_heartbeat_sec)`초만큼 기다린 뒤 재배치한다.
이 때문에 드물지만 트리거가 여러 곳에서 동시에 실행될 가능성이 있는데, 이는 트리거 계약에 원래 설계된, 예상된 동작이다. Airflow는 트리거가 여러 곳에서 동시에 실행될 때 발생하는 이벤트를 중복 제거하므로, 이 과정은 오퍼레이터 입장에서는 투명하게 처리된다. 트리거러를 하나씩 추가로 띄울 때마다 DB에 대한 영구 연결이 하나씩 늘어난다는 점도 감안해야 한다.
핵심 포인트
- 트리거러가 죽거나 네트워크 파티션되면 Airflow는 2.1 * triggerer.job_heartbeat_sec초를 기다린 뒤 해당 호스트의 트리거들을 다른 트리거러로 재배치한다
- 재배치 과정에서 같은 트리거가 드물게 두 곳에서 동시에 실행될 수 있으나, 이는 설계상 허용된 동작이며 이벤트 중복 제거로 오퍼레이터에는 투명하게 처리된다
- 트리거러를 추가로 띄울 때마다 DB에 대한 영구 연결이 하나씩 늘어난다는 비용을 감안해야 한다
부하 분산 (max_trigger_to_select_per_loop)
3.2.0부터, 트리거러는 HA 배포에서 다른 트리거러들이 굶주리지 않도록 한 루프당 `[triggerer] max_trigger_to_select_per_loop` 개수만큼만 트리거를 선택한다. 부하를 고르게 유지하려면 이 값을 `[triggerer] capacity`보다 충분히 낮게 설정하는 것이 권장된다. 현재 `max_trigger_to_select_per_loop`의 기본값은 50이고, 기본 `capacity`는 1000이다.
벤치마크에 따르면, 기본 설정에서 두 대의 트리거러는 1초 안에 1000개의 트리거를 거의 균등하게 나눠 가져갈 수 있다고 한다. 실제 배포 환경에 맞는 값은, 많은 수의 Deferrable 태스크를 가진 Dag를 트리거해 트리거를 대량으로 만든 뒤 트리거러들 사이의 부하 분산과 전체 트리거를 다 가져가는 데 걸리는 시간을 직접 관찰해서 정하는 것이 권장된다.
핵심 포인트
- max_trigger_to_select_per_loop(기본 50)는 한 트리거러가 한 루프에서 가져가는 트리거 개수를 제한해, 다른 트리거러들이 굶주리지 않고 골고루 트리거를 나눠 갖게 한다
- 이 값은 capacity(기본 1000)보다 충분히 작게 설정하는 것이 권장되며, 실제 배포 환경에서는 트리거를 대량 생성해 분산과 처리 시간을 직접 관찰해 적정값을 정해야 한다
--queues로 트리거러 호스트 지정하기
3.2.0부터, 특정 트리거를 특정 트리거러 호스트 집합에만 배정하고 싶을 수 있다 - 예를 들어 팀마다 별도의 트리거러 집합을 운영하는 멀티테넌트 환경이나, 트리거러 집합마다 서로 다른 클라우드 권한을 가진 별개의 트리거 연산을 처리하도록 구성한 환경이다.
이를 위해서는 먼저 `triggerer__queues_enabled` 설정값을 `true`로 켜서, 태스크가 defer될 때 자신이 배정된 태스크 큐를 새로 등록되는 트리거 인스턴스에 함께 넘기도록 해야 한다. 그리고 특정 트리거러 호스트(들)의 시작 명령에 `--queues=<큐 이름들의 콤마 구분 문자열>`을 추가하면, 그 트리거러는 지정된 태스크 큐(들)에서 온 태스크가 defer한 트리거 인스턴스만 가져간다.
트리거 타입별로 할당 방식이 다르다 - 태스크가 생성한(task-created) 트리거는 태스크의 큐가 `--queues`에 포함된 트리거러가 실행하고, 이는 Celery처럼 태스크 `queue` 개념을 쓰는 executor에서만 동작한다. 이벤트 기반 트리거(Event-Driven Triggers)는 태스크 큐에 묶이지 않지만, `BaseEventTrigger` 서브클래스가 `super().__init__()`에 `queue=`를 넘겨 executor와 무관하게 명시적으로 큐를 지정할 수 있다. 비동기 콜백 트리거도 마찬가지로 `AsyncCallback`에 `queue=`를 넘겨 큐를 지정한다. 큐가 지정되지 않은 이벤트/콜백 트리거는 `--queues` 없이 실행되는 트리거러가 가져간다.
큐가 지정된 트리거와 지정되지 않은 트리거를 (어떤 타입이든) 함께 운영한다면, `--queues` 없이 실행되는 트리거러를 최소 하나는 반드시 남겨둬야 큐 미지정 트리거들이 실행될 수 있다.
Multi-Team 모드의 `--team-name` 옵션은 모든 트리거 타입(태스크 생성, 이벤트 기반, 콜백)에 대해 네이티브한 팀 스코프 트리거러 할당을 제공하는, `--queues`와는 별개의 메커니즘이며 필요하면 둘을 함께 쓸 수 있다.
핵심 포인트
- --queues 큐 할당은 태스크가 생성한 트리거에 대해서는 Celery처럼 태스크 큐 개념을 쓰는 executor에서만 동작하며, 이벤트 기반 트리거나 콜백 트리거는 실행기와 무관하게 queue= 인자로 직접 지정한다
- 큐가 지정된 트리거와 지정되지 않은 트리거를 함께 운영한다면, --queues 없이 실행되는 트리거러를 최소 하나는 반드시 남겨둬야 큐 미지정 트리거들이 실행된다
- Multi-Team 모드의 --team-name은 이 --queues 메커니즘과 별개의 네이티브 팀 스코프 할당 방식이며 둘을 함께 쓸 수 있다
mode='reschedule' vs deferrable=True
센서가 유휴 기간을 관리하는 두 가지 방식을 비교하면 다음과 같다. `mode='reschedule'`은 BaseSensorOperator에 실제로 내장된 파라미터로, 조건이 충족되지 않으면 계속 자신을 재스케줄링한다 - 리소스 소모가 크지만 (반복 실행) Airflow에 내장된 기능이라 별도 구현이 필요 없고, 파일 생성처럼 시간이 지나면서 조건이 바뀌는 상황에 적합하다.
반면 `deferrable=True`는 일부 오퍼레이터가 관례적으로 쓰는 값일 뿐, Airflow에 내장된 실제 파라미터나 모드가 아니다 - 유휴 시 실행을 완전히 멈췄다가 조건이 바뀌면 재개하므로 리소스 소모가 적지만, 태스크를 defer하고 외부 변화를 처리하는 커스텀 로직이 필요하며 실제 재시도 동작은 오퍼레이터 구현마다 다르다. API 응답처럼 외부 이벤트나 리소스를 기다리는 상황에 적합하다.
핵심 포인트
- mode='reschedule'은 BaseSensorOperator에 내장된 실제 파라미터인 반면, deferrable=True는 오퍼레이터들이 관례적으로 쓰는 값일 뿐 Airflow에 내장된 모드가 아니며 실제 동작은 오퍼레이터 구현마다 다르다
- reschedule은 고정 간격 재실행이라 리소스 소모가 크고 파일 생성처럼 시간이 지나면 조건이 바뀌는 경우에 적합하고, deferrable은 유휴 시 완전히 대기하다 조건 충족 시 재개해 리소스 소모가 작고 외부 이벤트/API 응답 대기에 적합하다