RabbitMQ 클러스터링 구축하기
RabbitMQ 클러스터링 구축작업에 대한 내용을 공유하고자합니다.
왜 클러스터링을 구축하게 되었나
현재 실무에서 알림 시스템 아키텍처를 개선하게 되면서 RabbitMQ 단일 서버가 장애 시 알림 시스템의 리스크가 크고 안정성에 문제가 있는걸로 판단되었습니다. 이를 해결하기 위해 RabbitMQ 클러스터링 환경을 구축하게 되었습니다. 구축하게 되면서 어떤점을 중점적으로 보았는지 공유할려고 합니다.
🤔 RabbitMQ 클러스터링이 뭔가요?
여러 대의 RabbitMq 노드를 논리적으로 하나의 브로커처럼 동작하도록 구성하는 것입니다. 각 노드는 메타데이터와 큐 정보를 공유하며, 클라이언트는 어느 노드에 연결하든 동일한 큐와 익스체인지에 접근할 수 있습니다.
구축하면 이점은?
1. 고가용성 (High Availability)
단일 노드 장애 대응
- 한 노드가 다운되어도 다른 노드에서 서비스 계속 제공
- 클라이언트는 자동으로 다른 노드로 재연결 가능
- 계획된 유지보수 시 무중단 배포 가능
1
2
3
3노드 클러스터에서 1개 노드 장애 발생
→ 나머지 2개 노드로 정상 서비스 유지
→ 장애 노드 복구 후 자동으로 클러스터에 재합류
2. 데이터 내구성 향상
RabbitMQ 3.8부터 도입된 쿼럼 큐는 Raft 합의 알고리즘을 기반으로 하여 미러링 큐보다 훨씬 안정적이고 예측 가능한 복제를 제공합니다.
주의사항
- 최소 3개 노드 필요 (쿼럼 형성을 위해)
- 메시지 우선순위(priority)는 지원하지 않음
- 비독점(non-exclusive) 큐만 가능
- 미러링 큐에서 쿼럼 큐로 마이그레이션 시 계획 필요
3. 부하 분산
자동 로드 밸런싱
- 클라이언트를 여러 노드에 분산 연결
- 각 큐가 다른 노드에 생성되어 부하 분산
- 네트워크 트래픽 분산
성능 특성 실제 처리량은 하드웨어 사양, 네트워크 대역폭, 메시지 크기, 퍼시스턴스 설정 등에 따라 크게 달라집니다.
- 쿼럼 큐: 안정성 우선, 약간의 처리량 감소 있을 수 있으나 일관된 성능
- 클래식 큐: 높은 처리량, 단 노드 장애 시 메시지 손실 가능
- 복제 오버헤드: 복제 팩터가 높을수록 쓰기 성능 저하, 읽기 성능 향상
1
2
3
4
5
# 실제 환경에서 성능 테스트 도구
rabbitmq-perf-test -h rabbit1 -u test -p test \
-x 1 -y 1 -z 60 \
--queue-pattern 'perf-test-%d' \
--queue-pattern-from 1 --queue-pattern-to 10
4. 수평적 확장성
노드 추가를 통한 확장
- 트래픽 증가 시 노드를 추가하여 용량 확장
- 애플리케이션 코드 변경 없이 확장 가능
- 점진적 확장이 가능하여 비용 효율적
5. 장애 복구 시간 단축
단일 노드 환경에서는 서버 장애 시 수동 복구 과정이 필요합니다
- 장애 감지 및 알람
- 서버 재시작 또는 교체
- 데이터 무결성 확인
- 애플리케이션 재연결
- 서비스 정상화 확인
클러스터 환경(쿼럼 큐 사용 시)에서는 자동 페일오버할 수 있습니다.
- 자동 장애 감지: Raft 합의 알고리즘이 노드 장애를 빠르게 감지
- 자동 리더 선출: 쿼럼(과반수) 노드가 살아있으면 자동으로 새 리더 선출
- 무중단 서비스: 클라이언트는 다른 노드로 자동 재연결
- 데이터 무손실: 쿼럼에 커밋된 메시지는 보존
6. Rolling Upgrade 가능
클러스터 환경에서는 무중단 업그레이드가 가능합니다
1
2
3
4
5
6
7
# 노드 1 업그레이드
rabbitmqctl stop_app
# 패키지 업그레이드
rabbitmqctl start_app
# 노드 2 업그레이드
# ...반복
이점
- 서비스 중단 없이 버전 업그레이드
- 문제 발생 시 롤백 가능
- 사용자 영향 최소화
7. 모니터링과 관리 효율성
통합 관리
- 하나의 Management UI에서 전체 클러스터 상태 확인
- 중앙화된 로그 및 메트릭 수집
- 일관된 정책 관리
모니터링 메트릭
- 노드별 메모리/CPU 사용률
- 큐별 메시지 적체 현황
- 리더 노드의 부하 분산 상태
- 커밋 인덱스와 스냅샷 인덱스 차이
- 디스크 I/O 성능
서버에서 클러스터링 작업 순서 및 주의 사항
1. 클러스터 통신에 사용되는 포트 및 서버간 방화벽 확인
RabbitMQ 클러스터링을 구축할 때는 여러 포트가 필요합니다. 사용하는 포트를 확인하여 서버간 방화벽이 열려있는지 확인이 작업전에 필요합니다.
4369 (epmd - Erlang Port Mapper Daemon)
1
목적: Erlang 노드 간 통신을 위한 포트 매핑
RabbitMQ는 Erlang으로 작성되었으며, Erlang 노드들이 서로를 찾기 위해 epmd를 사용합니다. epmd는 각 Erlang 노드가 사용하는 동적 포트를 추적하고, 다른 노드가 해당 정보를 조회할 수 있도록 합니다.
4369 포트는 왜 필요할까요?
- Erlang 노드는 시작 시 임의의 포트를 선택하여 통신합니다
- epmd는 “rabbit@hostname” 같은 노드 이름을 실제 통신 포트로 매핑해줍니다
- 클러스터 노드들이 서로를 발견하고 연결하는 첫 단계입니다
- 25672 (기본 Erlang 배포 포트)
1
목적: 실제 노드 간 데이터 통신
epmd를 통해 노드를 찾은 후, 실제 클러스터 데이터 교환은 이 포트를 통해 이루어집니다. RabbitMQ는 기본적으로 25672 포트를 사용하지만, 이는 설정으로 변경 가능합니다.
이 포트를 통해 전송되는 데이터
- 클러스터 메타데이터 동기화
- 큐 미러링 데이터
- 내부 통신 메시지
- 클러스터 상태 정보
2. Erlang Cookie 동일성 확인
가장 중요하고 자주 실수하는 부분입니다.
1
2
3
4
# 모든 노드에서 동일한 쿠키 값을 가져야 합니다
cat /var/lib/rabbitmq/.erlang.cookie
# 또는
cat ~/.erlang.cookie
주의사항
- 쿠키 파일의 권한은 반드시
400이어야 합니다 - 쿠키 값에 공백이나 개행 문자가 들어가면 안 됩니다
- 쿠키를 변경한 후에는 RabbitMQ를 재시작해야 합니다
- 보안을 위해 기본 쿠키 값을 반드시 변경하세요
1
2
3
4
# 올바른 쿠키 설정 방법
echo "your-secret-cookie-value" > /var/lib/rabbitmq/.erlang.cookie
chmod 400 /var/lib/rabbitmq/.erlang.cookie
chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
3. 호스트네임 설정
RabbitMQ는 호스트네임을 기반으로 노드를 식별합니다.
1
2
3
4
# /etc/hosts 파일에 모든 클러스터 노드 추가
192.168.1.101 rabbit@rabbit1
192.168.1.102 rabbit@rabbit2
192.168.1.103 rabbit@rabbit3
주의사항
- 모든 노드에서 서로의 호스트네임을 해석할 수 있어야 합니다
- DNS를 사용하는 경우 역방향 DNS도 올바르게 설정되어야 합니다
hostname명령어로 확인한 이름과 일치해야 합니다- FQDN을 사용하는 경우 모든 노드에서 일관되게 사용해야 합니다
4. 노드 이름 지정
1
2
3
# RabbitMQ 설정 파일에서 노드 이름 지정
# /etc/rabbitmq/rabbitmq.conf
NODENAME=rabbit@rabbit1
주의사항
- 노드 이름은 변경할 수 없습니다
- 형식은
name@hostname이어야 합니다
🤔 노드 이름은 왜 변경 할 수 없을까요?
RabbitMQ는 Erlang의 Mnesia 데이터베이스를 사용하는데, 노드 이름이 데이터 저장 구조의 핵심 식별자로 사용됩니다.
- Mnesia 데이터 디렉토리 구조
1 2 3 4 5 6 7 /var/lib/rabbitmq/mnesia/rabbit@rabbit1/ ├── DECISION_TAB.LOG ├── LATEST.LOG ├── nodes_running_at_shutdown ├── rabbit_durable_exchange.DCD ├── rabbit_durable_queue.DCD └── ...
- 노드 이름이 디렉토리 경로에 포함됨
- 모든 데이터베이스 파일이 이 경로에 저장됨
- 클러스터 메타데이터
- 클러스터 구성 정보에 노드 이름이 하드코딩됨
- 큐의 소유권, 복제 정보가 노드 이름으로 참조됨
- 쿼럼 큐의 멤버십 정보에 노드 이름 포함
- Erlang 분산 시스템
- Erlang 노드는 시작 시 이름으로 등록됨
- 다른 노드들이 이 이름으로 통신 채널 설정
- 이름 변경 시 기존 연결이 모두 무효화됨
5. 클러스터 조인 순서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 첫 번째 노드 (rabbit1)는 그대로 실행
# 두 번째 노드 (rabbit2)에서
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@rabbit1
rabbitmqctl start_app
# 세 번째 노드 (rabbit3)에서
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@rabbit1
rabbitmqctl start_app
주의사항
reset명령은 기존 데이터를 삭제합니다- 운영 중인 노드를 클러스터에 추가할 때는 신중해야 합니다
- 한 번에 하나의 노드씩 추가하는 것이 안전합니다
6. 클러스터 상태 확인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 클러스터 상태 확인
rabbitmqctl cluster_status
# 노드 상태 확인
rabbitmqctl node_health_check
# 쿼럼 큐 상태 확인
rabbitmqctl list_queues name type leader members online
# 쿼럼 큐 멤버 상태 확인
rabbitmqctl list_queues name policy members online \
| grep quorum
# 특정 쿼럼 큐의 상세 정보
rabbitmqctl list_queues name messages_ready messages_unacknowledged \
leader members online --formatter json
마치며
클러스터 환경에서는 단일 노드보다 약간의 성능 오버헤드가 있지만, 얻게 되는 안정성과 확장성은 그 이상의 가치가 있습니다. 특히 쿼럼 큐를 사용하면 Raft 합의 알고리즘을 통해 강력한 데이터 일관성을 보장받을 수 있어 안정적인 운영 시스템에 필요하다고 생각합니다.
이러한 작업 시나리오를 통해 저는 알림 시스템에 RabbitMQ 클러스터링적용하였고, Grafana로 모니터링하면서 문제가 없는지도 확인하고 있습니다. 만약 사용자수가 늘어 트래픽이 증가하여 RabbitMQ의 수평확장의 한계에 부딪힌다면 대규모 트래픽에 용이한 kafka로의 전환도 고려하면서 아키텍처를 개선할 수 있도록 노력 중입니다.