airflow 2.0 에서 공식적(?)으로 HA 를 지원한다. 1.x 버전에서는 scheduler 프로세스를 동시에 여러개 구동하면 작업이 중복 실행될 수 있는 위험이 있었서 무조건 single 로 운영해야했고 이 때문에 가용성을 보장할 수 없었는데 2.x 에서부터는 row-level locking 을 이용해서 multiple scheduler 이용이 가능하도록 개선이 됐다. (대신 SKIP LOCKED
또는 NOWAIT
구문을 지원하는 mysql8.0 이상을 써야하는 제약조건이 있다.)
어쨋든 1.x 에서는 기본 기능만으로는 완벽한 고가용성 구성을 지원하지 않았기때문에 억지로 구성하자면 할 수 있었던 HA 구성을 미뤄왔었지만.. 2.x 이 출시되면서 더이상 미룰 수 없게 되었고 우리 팀에서도 관련 PoC 를 진행해봤다.
airflow 1.x 에서는 teamclairvoyant 에서 제공하는 플러그인을 활용하면 active-standby 형태의 가용성을 확보할 수 있다.
https://github.com/teamclairvoyant/airflow-scheduler-failover-controller
아무튼 그동안 Webserver
, Scheduler(LocalExecutor)
두 프로세스만으로 운영하던 airflow 서비스 구성이 최종적으로 다음과 같이 변했다.
docker-compose.yml
, airflow.cfg
등 전체 설정 파일은 github.com/oboki/analytics-airflow/tree/HA 에 정리해놨고
위 구성이 나오기 까지 겪었던 고민과 시행착오가 몇가지 있어 간단히(?) 적어봤다.
- Celery Broker 선정 (SQLALchemy vs. Redis)
- Redis HA 구성 옵션 (Cluster vs. Sentinel)
- 멀티 노드 도커 네트워크 설정
SQLAlchemy vs. Redis
아직 사내에서 kubernetes
를 쓰지는 못하기 때문에 celery worker
를 쓰는건 정해져 있었는데 이 celery 에서 메시지 브로커로 이용할 수 있는 옵션이 rabbitMQ
, redis
, SQLAlchemy
이렇게 있다. 팀 내에서 활용하는 rabbitMQ
, redis
가 따로 있진 않아서 추가 구성을 해야하기도 했고, 어차피 meta DB 로 활용하고있는 mysql
을 queue 로 재활용하면 더 좋을 것 같아 먼저 SQLAlchemy
를 이용해서 구성해봤었다.
[celery]
broker_url = sqla+mysql://airflow:airflow@airflow-1:3306/airflow
설정에는 크게 어려움도 없었고 몇가지 간단한 가용성 테스트 시나리오에서도 문제가 없었지만 3 버전 문서에 Experimental Status 가 눈에 거슬렸고 최신버전 문서에서는 Using SQLAlchemy 페이지가 존재하지도 않는 상태여서 아무래도 적용하기에는 부담스러운 것 같아 논의 끝에 sqla broker 는 포기하기로 했다.
나머지 rabbitMQ
와 redis
중에서는 redis
가 HA 구성이 좀 더 낫다고 해서 redis
로 결정했다.
Redis Cluster vs. Sentinel
레디스에서는 고가용성 구성 옵션이 두 가지 있는데 active-active cluster 또는 master-slave 구성이다. 그 중에서 처음에는 cluster를 고민했었는데 quorum 구성을 위한 3 노드에, 각 노드마다 master×slave pair 에다가 HAProxy 까지 해서 3개 프로세스를 구성해야해서 레디스 구성만을 위해서 최소 9개 프로세스가 추가되어야 한다.
SQLAlchemy 브로커 구성에서는 필요한 전체 프로세스가 6개 밖에 안 됐는데 여기에 메시지 브로커로만 9개 프로세스가 더 추가된다고 하니.. 배보다 배꼽이 큰 것 같아 cluster 구성은 포기했다.
다른 옵션은, sentinel
을 이용한 active-standby 구성인데 celery 에서 broker_url 로 sentinel url list 를 지정할 수 있기때문에 별도의 HAProxy가 필요없어서 최소 5개 프로세스로 구성할 수 있다. slave 노드를 하나 더 추가해서 6개 프로세스로 구성하긴 했지만, cluster 보다 구성이 단순하고 관리에도 용이한 것 같다.
airflow.cfg
에는 다음과 같은 설정이 필요하다.
[celery]
broker_url = sentinel://airflow-1:26379/0;sentinel://airflow-2/0:26379;sentinel://airflow-3:26379/0;
result_backend = db+mysql://airflow:airflow@airflow-1:3306/airflow
[celery_broker_transport_options]
master_name = mymaster
멀티 노드 도커 네트워크 설정
로컬 프로세스로 구동하던걸 이번에 업그레이드 하면서 도커 기반으로 운영하기로 결정했기때문에 docker-compose 로 구성해봤는데 원격지 머신의 컨테이너들 간의 통신이 가능하도록 하는 네트워크 설정을 찾는데서 삽질을 많이 했다. macvlan
과 같은 네트워크를 이용하면 docker service composition 자체는 쉬워지겠지만, 배정 받아야하는 real ip 가 늘어나는 등 인프라 설정이 추가돼 관리 비용이 커질 것 같아 기본 네트워크에 포트 맵핑으로만 해결하고자 했다. Simple is Best.
기본 bridge
네트워크에서, 도커 컨테이너들은 가상 네트워크 인터페이스를 생성해 그 안에서 놀기때문에 호스트 외부에 서비스를 열어주기 위해서는 NAT를 이용해 포트포워딩을 해줘야하는데, 각 컨테이너 서비스들이 자기 자신을 ip 주소로만 알려주면 포트포워딩을 해주더라도 소용이 없다.
다행히 redis 6.2 버전부터 hostname을 ANNOUNCE_IP
를 설정할 수 있도록 개선돼 docker 위에서 각 노드들이 서로를 식별할 수 있었고 다음과 같이 환경변수를 사전 설정해서 PoC 용 redis 를 구성할 수 있었다.
redis-sentinel:
image: 'bitnami/redis-sentinel:latest'
environment:
- ALLOW_EMPTY_PASSWORD=yes
- REDIS_MASTER_HOST=airflow-3
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=5000
- REDIS_SENTINEL_ANNOUNCE_HOSTNAMES=yes
- REDIS_SENTINEL_ANNOUNCE_IP=airflow-1
ports:
- 26379:26379
그리고 각 taskinstance 의 작업 로그는 worker node 에만 존재하기때문에 webserver 에서 해당 로그를 확인하려면 celery worker 에서 8793 포트로 서비스하는 미니 웹서버에 접근할 수 있어야한다.
airflow-worker:
<<: *airflow-common
command: celery worker
hostname: worker-1
ports:
- 8793:8793