RabbitMQ는 설정이 간편하면서도 강력한 성능을 보여주는 메시지 브로커이다.
메시지, 큐의 디스크 저장으로 프로세스에 장애가 발생해도 발행된 메시지를 복원할 수 있다.
또한 큐에서 소비자에게 전달된 메시지를 확인하는 메커니즘을 통해 최소 한 번 이상의 메시지 전달을 보장한다.
네트워크 문제나 장애 상황 등 다양한 이유로 이 보장이 깨지거나, 실제로는 유실된 것처럼 보이는 상황이 발생한다.
1. 메시지 유실
1) Producer(발행자) 측면
[시나리오1] Publisher Confirms 미사용 또는 처리 미흡
발행자가 메시지를 RabbitMQ 브로커로 전송했지만, 네트워크 문제로 메시지가 브로커에 도달하지 못했다.
Publisher Confirms 메커니즘을 사용하지 않으면, 발행자는 이 사실을 알지 못하고 메시지가 성공적으로 전송되었다고 가정한다.
브로커는 메시지를 받지 못했으므로 당연히 소비자에게 전달할 수 없다.
Publisher Confirms를 사용하더라도, ack(긍정 응답) 또는 nack(부정 응답)을 제대로 처리하지 않고 nack을 무시하거나 재시도 로직이 없다면 메시지가 유실될 수 있다.
[시나리오2] Mandatory/Immediate 플래그와 ReturnListener 처리 미흡
발행자가 mandatory 플래그를 설정하여 메시지를 전송했지만, 해당 라우팅 키로 바인딩 된 큐가 없어서 라우팅될 수 없는 상황이다.
브로커는 이 메시지를 ReturnListener를 통해 발행자에게 반환한다.
만약 발행자 측에 이 반환된 메시지를 처리하는 로직(재시도, 로깅, 다른 큐로 전송 등)이 없다면 메시지는 유실된다.
- immediate 플래그는 거의 사용되지 않는다.
설정 상에서 문제가 있는지 다시한번 확인하고, 장애에 대응하기 위한 알림 등을 적극적으로 도입한다.
2) Broker(RabbitMQ 서버) 측면
[시나리오1] 메시지 또는 큐의 Durable(영속성) 설정 누락
메시지와 큐 모두 durable 하게 설정되어 디스크에 저장되어 한다.
만약 메시지가 non-persistent(비영속성) 으로 발행되었거나, 메시지가 라우팅된 큐가 durable하지 않은 경우, 브로커가 재시작되면 해당 메시지나 큐의 메시지들은 메모리에서 삭제되고 유실된다.
[시나리오2] 리소스 고갈(디스크 공간, 메모리 부족)
영속성 메시지가 계속 쌓여 디스크 공간이 가득 차거나, 처리되지 않은 메시지가 너무 많아 메모리가 부족해질 수 있다.
이런 경우 RabbitMQ는 resource alarm 상태가 되고, 새로운 메시지 발행을 차단(flow control)하거나, 메시지를 받지 못한다.
만약 발행자가 이러한 상황을 제대로 감지하지 못하면 메시지가 유실될 수 있다.
또한 이미 메모리에 있지만 아직 디스크에 기록되지 못한 메시지가 서버 강제 종료 시 유실될 수도 있다.
[시나리오3] 클러스터 환경에서의 문제
클래식 미러 큐에서 모든 미러가 동기화되기 전에 마스터 노드가 다운되고, publisher confirm이 발행자에게 전송된 경우 해당 메시지가 유실될 수 있다. 네트워크 파티션 발생 시에도 pause_minority와 같은 전략이 제대로 동작하지 않으면 데이터 정합성이 깨지며 메시지 유실 가능성이 있다.
** 네트워크 파티션: 기존에 하나로 연결되어 있던 네트워크가 어떤 이유로 인해 두 개 이상의 독립적인 하위 네트워크(파티션)로 나뉘어, 각 파티션에 속한 노드들끼리는 통신이 가능하지만 서로 다른 파티션에 있는 노드들 간 통신은 불가능하게 되는 상
[시나리오4] 관리자의 실수로 인한 큐/익스체인지 삭제
운영 중인 큐나 익스체인지가 실수로 삭제되면, 해당 큐에 저장되어 있던 모든 메시지도 영구적으로 삭제된다.
3) Consumer (소비자) 측면
[시나리오1] autoAck=true (자동 Acknowledgement) 사용
소비자가 자동 승인 모드로 메시지를 구독하면, 브로커는 소비자에게 메시지를 전달하자마자 해당 메시지를 큐에서 제거한다.
만약 소비자가 메시지를 받고 실제 처리를 완료하기 전에 장애로 종료되면, 해당 메시지는 이미 큐에서 제거되어 유실된다.
[시나리오2] 수종 승인 처리 시점 오류
소비자가 메시지의 실제 처리가 성공적으로 완료되기 전 basicAck를 브로커에 전송하고, 그 직후 실제 처리 중 에러가 발생하여 소비자 로직이 종료되는 경우, 브로커는 ack을 받았으므로 큐에서 메시지를 제거하며 메시지가 유실된다.
[시나리오3] 처리 실패한 메시지에 대한 부적절한 nack 또는 reject 처리
소비자가 메시지 처리 중 일시적이지 않은 오류를 발견하고 basicNack 또는 basicReject를 호출할 때, requeue 파라미터를 false로 설정해야 한다. 만약 이 메시지를 DLX(Dead Letter Exchange)로 보내는 설정이 없거나 DLX에서 해당 메시지를 수집하는 큐가 없다면, 이 메시지는 버려지게 된다.
만약 영구적으로 처리 불가능한 메시지를 DLX 설정 없이 재큐잉(requeue=true)하면 무한 반복 소비가 유발되므로 주의한다.
DLX로 보내더라도 최종적으로 해당 메시지를 관리하지 않으면 사실상 유실되는 것과 동일하다.
[시나리오4] 소비자 로직 버그로 인한 메시지 처리 누락 후 ack
소비자 코드에 버그가 있어서 특정 조건의 메시지를 처리하지 않거나 건너뛰면서도, finally 블록 등에서 무조건 ack를 보내는 경우, 메시지는 처리되지 않고 ack되었으므로 유실된다.
RabbitMQ 사용 시 메시지가 유실되는 상황은 다양하다. 메시지 전송을 보장하기 위해서는 발행자-브로커-소비자의 전체 플로우에 걸쳐 각 컴포넌트가 자신의 역할을 정확히 수행해야 한다. 또한 장애가 발생할 수 있는 포인트들을 올바르게 처리할 수 있도록 견고하게 설계해야 한다.
이렇게 대비를 하다보면 메시지가 중복 처리되는 가능성은 오히려 높아지게 된다.
소비자가 메시지를 처리 후, ack를 보내기 직전 장애가 발생하면 브로커는 ack를 받지 못하여 메시지를 재전송하게 된다.
이런 경우 소비자는 동일 메시지를 두번 소비하게 되는 문제가 발생한다.
따라서 소비자 측에서는 멱등성(idempotency)를 확보하는 것이 권장된다.
** 멱등성: 어떤 연산을 여러 번 수행하더라도 그 결과가 한 번 수행한 결과와 동일하게 나오는 성질
- 회원번호 100의 계좌 잔액에 +1000 연산
- 회원번호 100의 계좌 잔액 = 5000으로 설정(멱등성)
[확보 예시]
- 각 메시지 또는 작업 요청에 고유한 ID를 부여한다, 소비자가 메시지 처리 전 해당 메시지의 ID가 이미 처리된 것인지 확인한다.
- 특정 상태일 때문 연산을 수행하도록 설정한다. 주문 상태가 [결제 대기] 인 경우에만 [결제 완료]로 변경한다.
'공부' 카테고리의 다른 글
[모니터링] Prometheus + Grafana (0) | 2025.06.06 |
---|---|
[BE] RabbitMQ 장애 - 메시지 중복 처리, 메모리 부족, 디스크 공간 부족 (0) | 2025.06.05 |
[BE] Publish & Subscribe 구조 (0) | 2025.06.04 |
[알고리즘] 문제 분석 (1) | 2025.06.03 |
[FE] Lazy Loading & Code Splitting & Tree Shaking (0) | 2025.06.03 |