RabbitMQ를 활용한 메시지 처리 속도 향상과 유실 방지

프로젝트에서 무중단 배포 전략 중 Blue/Green 방식을 채택하여 구성했었습니다.

Blue/Green 과정은 다음과 같습니다.

  • Blue 환경: 현재 운영 중인 버전의 애플리케이션 배포 환경

  • Green 환경: 새로운 버전의 애플리케이션의 배포 환경

  1. Green 서버를 실행합니다.

  2. Green 컨테이너의 실행을 확인합니다.

  3. health-check API에서 200 status code가 반환되면 Blue 서버를 종료합니다.

발생할 수 있는 문제점

새로운 버전의 애플리케이션이 배포되어 기존 서버가 종료되는 동안, 이전 클라이언트의 요청을 처리하지 못하는 경우가 생길 수 있습니다. 따라서 프로젝트에서는 WebSocket과 STOMP 프로토콜을 사용하기 때문에, 클라이언트가 보낸 채팅 메시지가 유실될 가능성이 있습니다.

그 이유는 다음과 같습니다.

  • WebSocket은 연결 지향적 프로토콜이며, 서버가 중단되면 기존 연결이 강제로 끊어집니다.

  • WebSocket과 STOMP는 실시간 통신을 위해 설계되었으며, 기본적으로 메시지 영속성을 제공하지 않습니다.

특히, 프로젝트에서는 Spring의 기본 내장 Simple Broker(In-Memory Broker)를 사용했습니다. 이는 메시지를 서버 메모리에만 저장하기 때문에, 서버가 종료되면 메모리에 있던 메시지들도 함께 유실되는 구조였습니다.

즉, 영속성을 보장할 수 있는 별도의 큐잉 메커니즘이 마련되어 있지 않습니다.

위 시나리오에서 유실이 발생하는 지점은 기존 Blue 서버가 종료되는 순간입니다. 서버가 클라이언트로부터 메시지를 수신했지만, 이를 처리하거나 클라이언트에게 확인 응답을 보내기 전에 종료되면 해당 메시지는 처리되지 못하고 그대로 유실됩니다.

STOMP 프로토콜 사용

stomp subscribe 과정

테스트를 여러 번 진행하여 평균적으로 수신되는 메시지(received) 수를 측정하고 비교했습니다.

1. 서버 교체가 없는 경우

서버 교체가 없는 경우, 기존 서버에서 안정적으로 운영되는 상태이므로 평균적으로 수신되는 메시지의 수치가 일정하게 유지됩니다.

2. blue/green 배포가 진행되는 경우

Blue/Green 배포가 진행되면, 평균 received의 오차 값이 첫 번째 상황에 비해 벗어나는 것을 확인할 수 있었습니다.

Blue/Green 과정에서 클라이언트의 연결이 끊기게 되고, 이때 발생하는 오차는 클라이언트가 수신했어야 할 메시지를 유실하게 되었음을 나타낸다고 판단했습니다.

해결 방법

1. Graceful Shutdown

첫 번째 시도로 Graceful Shutdown을 적용했습니다. Graceful Shutdown은 서버나 애플리케이션이 종료될 때, 현재 진행 중인 작업을 안전하게 마무리하고 클라이언트와의 연결을 정리하는 방식입니다. 이를 통해 이미 요청으로 들어온 HTTP Request 등을 클라이언트에게 응답하고 서버를 종료할 수 있습니다.

  • Graceful Shutdown이 시작되면 새로운 요청은 더 이상 받지 않고, 진행 중인 작업만 완료 후 종료합니다.

이 방법은 서버가 갑작스럽게 종료되는 것을 방지하고 데이터 유실이나 서비스 중단을 최소화하는 데 도움을 줍니다.

하지만 Spring과 Docker에 Graceful Shutdown을 적용했음에도 불구하고, 테스트 결과는 크게 달라지지 않았습니다.

앞서 언급했듯이 WebSocket/STOMP 프로토콜과 내장 브로커는 기본적으로 메시지 영속성을 보장하지 않습니다. 따라서 Graceful Shutdown이 진행 중인 연결을 정리할 시간을 주더라도, 서버 메모리에만 존재하던 미처리 메시지는 서버 종료와 함께 유실될 수밖에 없음을 의미합니다.

물론 Graceful Shutdown의 대기 시간(period)을 충분히 길게 잡는다면 메시지 유실을 상당 부분 줄일 수는 있겠지만, 이는 새로운 서버로의 부하 전환을 지연시키고 전체 배포 효율성을 떨어뜨릴 수 있습니다.

따라서 이는 근본적인 메시지 유실 문제의 해결책이 될 수 없다고 판단했습니다.

2. RabbitMQ

메시지 유실을 근본적으로 방지하기 위해 외부 메시지 브로커인 RabbitMQ 도입을 결정했습니다. 그 이유는 다음과 같습니다.

  • 메시지 큐잉과 영속성: RabbitMQ는 메시지를 큐에 저장하여 클라이언트가 전송한 메시지를 안전하게 보관할 수 있습니다. 서버가 종료되거나 문제가 발생해도 소비되지 못한 메시지는 큐에 남아있습니다.

  • 디스크 기반 영속화(Durable): RabbitMQ의 큐(Queue)와 메시지(Message)에 'Durable' 속성을 부여하면, 브로커가 재시작되더라도 메시지가 디스크에 안전하게 보관되어 유실을 방지할 수 있습니다.

  • 재시도 메커니즘: 클라이언트가 메시지를 전송하고 서버가 처리할 준비가 될 때까지 메시지를 큐에서 대기시킬 수 있습니다.

이처럼 외부 메시지 브로커를 이용하면 소비되지 못한 메시지를 영속화하여 유실을 최소화할 수 있다고 판단했습니다.

구현 흐름 (Retry Queue를 활용한 재시도)

구현은 위 흐름도와 같이 진행했습니다. 특히 메시지 유실 방지를 위해, Blue/Green 배포 등으로 서버 연결이 끊겨 메시지 소비에 실패할 경우를 대비한 Retry Queue(재시도 큐)를 활용한 재시도 로직을 포함했습니다.

  • 이미지의 DLQ는 Retry Queue 입니다.

전체적인 메시지 흐름과 재시도 로직은 다음과 같습니다.

  1. 메시지 전송 및 처리:

  • 사용자가 서버로 메시지를 전송하고, 서버는 이 메시지를 RabbitMQ 브로커(Main Exchange)로 전달합니다.

  1. 메시지 소비 시도: Main Queue를 구독하는 리스너가 메시지를 소비하려고 시도합니다.

  1. 소비 실패 (재시도 로직 발동):

  • 만약 Blue 서버가 종료되는 등의 이유로 리스너가 메시지를 정상 처리하지 못하면, basicNack을 호출하여 메시지 처리를 거부합니다.

  • 실패한 메시지가 Retry Queue로 이동합니다.

  1. Retry Queue로 라우팅:

  • RabbitMQ는 거부된 메시지를 Retry Exchange로 라우팅하고, 이는 다시 연결된 Retry Queue로 메시지를 전달합니다.

  1. 재시도 및 최종 처리:

  • Retry Queue를 구독하는 별도의 Retry 리스너가 이 메시지를 받습니다.

  • Retry 리스너는 총 6회에 걸쳐 메시지 재처리를 시도합니다.

  • (재시도 성공): 재시도 중 Green 서버가 활성화되어 클라이언트 연결이 복구되면, WebSocket을 통해 메시지를 성공적으로 재전송합니다.

  • (최종 실패): 6회의 재시도에도 불구하고 처리가 불가능한 경우, 해당 메시지는 영구 폐기하여 무한 루프를 방지합니다.

이 구현을 통해서 클라이언트의 평균 재연결 시간을 분석해서 재시도 주기와 횟수를 적절하게 설정하면 Blue/Green 배포 시 발생하는 대부분의 메시지 유실을 방지할 수 있다고 생각하지만, 완전한 메시지 보장은 어려운 상황들이 여전히 존재할 수 있습니다.

  • 재시도 횟수 초과, 네트워크 장애 지속, 클라이언트 애플리케이션 장애

만약 더 엄격한 메세지 보장이 필요할 경우에는 DLQ를 활용하는 방법도 존재합니다.

  • 재시도에 실패한 메세지를 폐기하는 대신 최종 DLQ로 전송

    • DLQ로 전송되었다는 것은 "시스템이 스스로 복구하지 못한 예외 상황"이라고도 생각할 수 있습니다.

  • DLQ에 쌓인 메세지를 통해 에러 원인을 분석하고 수동으로 처리하는 등 후속 조치

RabbitMQ 사용 결과

이번에도 여러 번의 테스트를 통해 평균 값을 측정했습니다.

1. 서버 교체가 없는 경우

이전 테스트와 마찬가지로 서버 교체가 발생하지 않으면 안정적으로 운영되며, 테스트 결과 지표에도 평균 값과 오차가 거의 없었습니다.

RabbitMQ 모니터링 그래프를 통해서도 모든 메시지가 정상적으로 수신되고 있음을 확인할 수 있었습니다.

2. Blue/Green 배포가 진행되는 경우

Blue/Green 배포가 진행되고 작업이 끝난 시점의 로그를 기록했는데, 13:37:30에 서버 교체가 이루어졌습니다.

이 시간에 RabbitMQ 모니터링 그래프를 살펴보면, 서버가 교체되는 시간 동안 일시적으로 소비되지 못한 메시지들이 큐에 쌓이는 것을 확인했고, 이를 통해 서버 교체 간 수신되지 못하는 메시지들이 유실되지 않고 큐에 대기하고 있음을 알 수 있었습니다.

결과 지표에서 클라이언트가 수신한 메시지에 약간의 오차값이 있지만, 이는 테스트 스크립트의 불완전성에 기인한 것으로 추정됩니다. 그 이유는 모니터링을 통해 실제로 메시지의 유실이 없었음을 확인했기 때문입니다.

Summary

처음에는 메시지 유실 방지를 주목적으로 RabbitMQ를 도입했지만, 테스트를 통해 STOMP 프로토콜만을 사용하는 것보다 RabbitMQ를 사용할 때 시스템의 전반적인 처리 능력까지 크게 향상되는 부가적인 효과도 얻을 수 있었습니다.

  • 연결 수와 세션 수는 66.47% 증가

  • 수신된 메시지 수 94.50% 증가

  • 전송된 메시지 수 66.47% 증가

이는 RabbitMQ가 다음과 같은 역할을 수행하기 때문이라고 생각합니다.

  • 메시지 브로커 역할: 메시지의 효율적인 라우팅과 분배를 담당하여 전반적인 처리량 증가에 도움을 줍니다.

  • 비동기 처리: 메시지 전송과 수신을 비동기적으로 처리하여 애플리케이션의 응답성이 향상될 수 있습니다.

  • 부하 분산: 메시지를 분배하여 부하를 균등하게 분산시킴으로써 더 많은 메시지를 처리할 수 있도록 합니다.

특히 수신된 메시지 수의 증가율이 가장 높은 것으로 보아, RabbitMQ가 메시지 수신 및 처리에 있어 큰 성능 향상을 가져왔다고 볼 수 있었습니다.

Last updated