왜 지금 Quorum 큐인가

Classic 미러링 큐(ha-mode 정책 기반)는 RabbitMQ 3.8부터 문제로 지적되어 왔다. 네트워크 파티션 상황에서 마스터가 바뀔 때 미러 동기화가 완전하지 않으면 확정(confirm)된 메시지가 유실될 수 있고, 미러 재동기화는 큐 전체를 블로킹한다. 이 미러링 기능은 3.9에서 deprecated 되었고 4.0에서 완전히 제거됐다. Quorum 큐는 Raft 합의 알고리즘을 사용해 다수 노드가 기록을 확정해야 커밋되므로, 파티션·노드 장애에서 데이터 안전성이 훨씬 명확하다. 미러링을 계속 쓰고 있다면 전환은 선택이 아니라 예정된 작업이다.

전환이 까다로운 이유: 큐 타입은 불변

핵심 제약은 이미 선언된 큐의 타입은 바꿀 수 없다는 점이다. x-queue-type은 큐 선언 시점에 고정되며, 같은 이름으로 다른 타입을 다시 선언하면 PRECONDITION_FAILED 오류가 난다. 즉 기존 큐를 지우고 새로 만들어야 하는데, 그 순간 큐에 남아 있던 메시지와 바인딩이 사라진다. 무중단으로 하려면 "기존 큐를 삭제하고 재선언" 방식이 아니라, 새 큐를 병렬로 띄우고 트래픽과 잔여 메시지를 옮긴 뒤 컷오버하는 접근이 필요하다.

무중단 전환 전략

블루-그린 방식으로 접근한다. 순서는 다음과 같다.

  • 새 이름의 Quorum 큐를 만들고, 기존과 동일한 라우팅 키로 exchange에 바인딩한다. 이 시점부터 신규 메시지는 양쪽 큐에 모두 들어간다.
  • 컨슈머를 새 Quorum 큐로 배포한다(구·신 컨슈머 병행 운영).
  • 기존 Classic 큐에 남은 잔여 메시지를 Shovel로 새 큐에 드레인한다.
  • Classic 큐 depth가 0이 되면 바인딩을 해제하고 큐를 삭제한다.

퍼블리셔 코드를 건드리지 않아도 되는 것이 장점이다. 바인딩만으로 신규 트래픽을 새 큐로 흘려보내고, 옛 큐는 잔여분만 비우면 된다.

Quorum 큐 선언과 바인딩

애플리케이션에서 선언할 때는 x-queue-type 인자만 추가하면 된다. Python(pika) 예시다.

import pika

conn = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
ch = conn.channel()

# 신규 quorum 큐 선언 (durable 필수, exclusive/auto_delete 불가)
ch.queue_declare(
    queue="orders.q.quorum",
    durable=True,
    arguments={
        "x-queue-type": "quorum",
        "x-quorum-initial-group-size": 3,   # 복제본 수(홀수 권장)
    },
)

# 기존 exchange에 동일 라우팅 키로 바인딩 → 신규 메시지가 양쪽으로 유입
ch.queue_bind(
    exchange="orders.ex",
    queue="orders.q.quorum",
    routing_key="orders.created",
)

복제 계수는 클러스터 크기에 맞춰 홀수(3 또는 5)로 두어야 과반 계산이 명확하다. 대규모 클러스터에서 모든 노드에 복제하면 오버헤드가 크므로 3으로 고정하는 편이 보통 낫다.

Shovel로 잔여 메시지 이관

Classic 큐에 이미 쌓여 있던 메시지는 동적 Shovel로 옮긴다. 소스를 비우면서 목적지로 전달하며, ack 기반이라 유실 없이 이관된다.

rabbitmqctl set_parameter shovel drain-orders '{
  "src-protocol": "amqp091",
  "src-uri": "amqp://localhost",
  "src-queue": "orders.q.classic",
  "dest-protocol": "amqp091",
  "dest-uri": "amqp://localhost",
  "dest-queue": "orders.q.quorum",
  "ack-mode": "on-confirm",
  "src-delete-after": "queue-length"
}'

src-delete-after: queue-length는 시작 시점의 메시지 수만큼만 옮기고 Shovel을 자동 종료한다. 드레인이 끝나면 파라미터를 clear_parameter로 정리한다. depth 확인은 rabbitmqctl list_queues name messages로 한다.

Classic과 Quorum 기능 차이

전환 전 애플리케이션이 쓰는 기능이 Quorum에서 지원되는지 반드시 확인한다.

기능Classic(미러링)Quorum
복제 방식비동기 미러Raft 합의(과반 확정)
메시지 우선순위지원미지원
비영속/exclusive 큐지원미지원(항상 durable)
Lazy/메모리 정책정책으로 제어기본 디스크 기반
Poison 메시지 처리수동delivery-count 기반 자동

주의점

첫째, 우선순위 큐를 쓰고 있었다면 Quorum으로 그대로 옮길 수 없으므로 별도 설계가 필요하다. 둘째, Quorum 큐는 메시지를 항상 디스크에 쓰므로 초당 처리량이 매우 높은 워크로드는 디스크 I/O를 미리 측정해야 한다. 셋째, 반복 실패 메시지가 무한 재처리되는 것을 막기 위해 x-delivery-limit를 정책으로 설정하고 DLX를 함께 구성하는 것을 권장한다. 넷째, 컷오버 직전 반드시 옛 큐 depth가 0이고 인플라이트(unacked) 메시지가 없는지 확인한 뒤 바인딩을 해제해야 유실이 없다. 롤백 대비로 옛 큐는 삭제 대신 며칠간 바인딩만 끊은 채 남겨두는 것이 안전하다.