RabbitMQ 운영의 모든 실패는 어디서 오는가
처리량 병목부터 Flow Control, 모니터링 맹점, 운영 장애 패턴, 클러스터 복구까지 — RabbitMQ 실무 운영의 다섯 가지 핵심 축을 추적한다.
- 01 RabbitMQ는 왜 메시지 브로커인가
- 02 RabbitMQ Exchange는 어떻게 메시지를 찾아가게 하는가
- 03 RabbitMQ 메시지 유실은 왜 조용히 일어나는가
- 04 RabbitMQ 메시지 패턴의 공통 철학은 무엇인가
- 05 RabbitMQ 운영의 모든 실패는 어디서 오는가
- 06 RabbitMQ vs Kafka — 어떤 문제를 해결하려고 만들어졌는가
- 07 Spring AMQP의 컴포넌트들은 어떻게 연결되는가
“RabbitMQ가 느리다”는 말은 대부분 “설정이 잘못됐다”는 뜻이다. 그런데 잘못된 설정은 한 곳에서 오지 않는다. Publisher 측, Consumer 측, 브로커 메모리, 모니터링 부재, 클러스터 구조 — 다섯 곳 중 어느 하나라도 어긋나면 전체가 흔들린다. 왜 RabbitMQ 장애는 항상 “갑자기” 발생하는 것처럼 느껴지는가?
처리량은 Publisher와 Consumer 양쪽의 문제다
Publisher 쪽 병목은 대부분 Confirm 처리 방식에서 온다. 매 메시지마다 waitForConfirmsOrDie()로 동기 대기하면 1,000개 메시지에 10ms RTT 기준으로 약 10초가 걸린다. 비동기 ConfirmCallback으로 바꾸면 같은 1,000개를 100ms 수준에서 처리할 수 있다 — 100배 차이다.
Consumer 쪽 병목은 concurrency와 prefetch 설정에서 온다. 처리 시간이 50ms인 Consumer를 단일 스레드(concurrency=1)로 운영하면 초당 최대 20개 처리가 한계다. 목표 처리량이 초당 1,000개라면 필요한 스레드 수는 1000 / (1/0.05) = 50개다. Prefetch는 처리시간 / RTT 비율로 설정하는 것이 기준점이다.
압축은 조건부다. 메시지가 1KB를 넘고 네트워크가 병목일 때만 이득이다. 1KB 미만 메시지에 GZIP을 걸면 압축 결과가 원본보다 커지고 CPU만 낭비한다.
복합 튜닝 순서는 Prefetch 증가 → Concurrency 증가 → 비동기 Confirm → 배치 소비다. 앞 단계의 효과를 확인하고 다음 단계로 넘어가야 한다.
Flow Control은 버그가 아니라 설계다
Publisher가 갑자기 차단된다. 연결은 살아있다. 팀은 RabbitMQ를 재시작한다. 재시작 후 5분이 지나면 또 차단된다. 이것이 Flow Control을 모를 때 생기는 패턴이다.
RabbitMQ는 메모리 사용량이 vm_memory_high_watermark(기본 40%)를 초과하면 모든 Publisher Connection에 Connection.Blocked 프레임을 보내 발행을 차단한다. Consumer는 계속 동작한다 — Consumer가 메시지를 소비해서 메모리를 회수해야 차단이 풀리기 때문이다.
재시작은 해결책이 아니다. Consumer 처리 속도가 발행 속도보다 느린 구조적 문제가 남아있으면, 재시작 후 메모리가 다시 임계값에 도달하는 순간 똑같이 차단된다.
Lazy Queue는 이 문제의 예방책이다. 일반 Queue는 메시지를 메모리에 먼저 올리고 임계값 초과 시 디스크로 내리는(Paging) 반면, Lazy Queue는 메시지 도착 즉시 디스크에 저장하고 메타데이터만 메모리에 유지한다. 100만 개 메시지 기준으로 일반 Queue는 수백 MB에서 수 GB의 메모리를 쓰지만 Lazy Queue는 메타데이터 크기인 수백 MB 수준에 머문다.
권장 설정은 vm_memory_high_watermark=0.4, paging_ratio=0.5(20%에서 미리 Paging 시작), disk_free_limit=메모리×1.5다. 디스크 여유 공간이 임계값 아래로 내려가도 Publisher가 차단된다는 점을 간과하지 말 것.
모니터링 없이는 장애가 항상 “갑자기” 온다
핵심 지표는 세 가지다.
Queue Depth는 Ready와 Unacked를 구분해서 봐야 한다. Ready가 증가하면 Consumer 처리 속도 부족이다. Unacked가 증가하면 Consumer가 메시지를 받았지만 처리를 완료하지 못하는 상태다 — 보통 외부 I/O 블로킹이 원인이다.
Publish Rate vs Deliver Rate의 차이가 지속적으로 양수면 Queue가 쌓이고 있다는 신호다. ack/s가 실질적인 처리량 지표다.
Consumer Utilisation이 50% 미만이면 Consumer가 Prefetch 대기 시간 때문에 놀고 있다는 뜻이다. Prefetch를 늘리거나 Consumer 수를 줄일 수 있다. 100%에 가까우면 Scale Out을 검토해야 한다.
DLQ 메시지는 0이어야 정상이다. 0에서 1이 되는 순간이 가장 중요한 시점이다.
Management UI는 기본 내장이고 실시간 상태 확인에 충분하지만 알림 기능과 이력 저장이 없다. 프로덕션에서는 rabbitmq_prometheus 플러그인 + Grafana 조합이 필수다. 알림은 P1(즉각)/P2(30분)/P3(일일) 계층으로 나눠야 알림 피로를 피할 수 있다.
운영 장애는 세 가지 패턴으로 수렴한다
패턴 A — Queue 적체: Queue Depth Ready가 급증하고 Consumer는 있다. Consumer Utilisation이 100%이면 Scale Out이 답이다. Consumer가 특정 메시지에서 루프를 돌며 requeue=true로 반복 실패하는 경우에는 DLX 설정이 없으면 해당 메시지가 계속 순환한다.
패턴 B — Unacked 누적: Ready는 0인데 Unacked가 수만 건이다. Consumer는 살아있지만 Ack을 보내지 않는다. 외부 API 타임아웃이 없어서 스레드가 무한 대기 중인 경우가 가장 흔하다. Consumer를 재시작하면 Connection이 끊기며 Unacked가 모두 Ready로 복귀한다 — 임시 처치다. 근본 해결은 모든 외부 I/O에 타임아웃을 설정하는 것이다.
패턴 C — 연결 폭풍: RabbitMQ 재시작 후 서비스 100개가 동시에 재연결을 시도한다. 고정 간격 재연결(sleep(1000))은 모든 서비스가 1초 후 동시에 재시도해서 폭풍을 증폭시킨다. Exponential Backoff + Jitter가 답이다. 지수 증가(1, 2, 4, 8… 최대 30초)에 ±30% 랜덤 지터를 더하면 재연결 시도가 시간적으로 분산된다.
클러스터는 홀수 노드와 Quorum Queue가 기본이다
단일 노드 RabbitMQ는 SPOF다. 3노드 클러스터 + Quorum Queue를 구성하면 노드 1개 장애에서 자동으로 복구된다.
Quorum Queue는 Raft 합의 알고리즘 기반이다. Leader 노드가 다운되면 Election Timeout(기본 5초) 후 Follower들이 투표를 시작하고, 과반수가 새 Leader를 선출한다. 총 다운타임은 보통 5~15초다. 인간 개입 없이 자동 복구된다.
홀수 노드를 권장하는 이유는 Split-Brain 처리 방식에 있다. 4노드 클러스터가 2+2로 분리되면 어느 쪽도 과반수(>2)를 달성하지 못해 전체 쓰기가 불가능해진다. 3노드에서 2+1로 분리되면 2가 과반수를 유지해 부분 가용성이 보존된다.
Rolling Upgrade는 노드 하나씩 stop_app → 업그레이드 → start_app 순서로 진행한다. 메이저 버전 업그레이드는 한 단계씩만 허용된다 — 3.9에서 3.12로 직접 올리면 클러스터가 분열된다. 기능 플래그는 모든 노드가 신버전이 된 후에 활성화해야 한다.
정리
- 처리량은 Publisher(비동기 Confirm, 배치)와 Consumer(concurrency, prefetch) 양쪽을 함께 튜닝해야 한다.
- Flow Control은 메모리 임계값 초과 시 Publisher를 차단하는 설계다. 재시작이 아니라 Consumer 처리량 증가 또는 Lazy Queue가 해결책이다.
- 핵심 지표는 Queue Depth(Ready/Unacked 구분), Publish-Deliver Rate 차이, Consumer Utilisation, DLQ 메시지 수다.
- 운영 장애는 적체/Unacked 누적/연결 폭풍 세 패턴으로 수렴한다. 각각 임시 처치(Consumer 재시작, Scale Out, 순차 재시작)와 근본 해결(타임아웃 설정, Backoff+Jitter)이 다르다.
- 3노드 이상 홀수 클러스터 + Quorum Queue는 선택이 아니라 프로덕션의 기본값이다.
다음 글에서는 RabbitMQ와 Kafka의 아키텍처 차이 — Push 기반 vs Pull 기반, 메시지 삭제 vs 로그 보존 — 를 구체적인 설계 결정으로 추적한다.