
현재 경매 종료의 경우, RabbitMQ의 Delayed Queue를 이용해 경매 종료 시점을 감지할 수 있도록 설계되었습니다.
그러나 RabbitMQ가 단일 노드로 구성되어 있어 장애 발생 시 경매 등록과 종료 처리에 문제가 생기며, 메시지 부하로 인한 성능 저하의 가능성이 있어 성능 개선이 필요하다고 판단했습니다.
RabbitMQ를 Cluster로 구성하는 방법을 고민하며, Docker Compose를 활용하는 방식과 여러 개의 서버를 배포하는 방안을 고려했습니다.
장애 발생 시 신속한 복구와 노드 대체를 위해 여러 지역에 서버를 구축하고 RabbitMQ 노드를 분산하는 것이 이상적이지만, 비용적인 부담이 커 현실적으로 어려웠습니다.
이에 대한 대안으로, 단일 서버에서 Docker Compose를 활용해 RabbitMQ Cluster를 구성하는 방법을 채택했습니다.
일반 큐의 경우에는 해당 큐가 있는 노드가 다운되면 다른 노드에서 다운된 큐의 메시지에 접근할 수 없습니다. 따라서 다운된 노드를 다시 복구하기 전까지는 메시지를 처리할 수 없습니다.
그러나 쿼럼 큐의 경우에는 특정 노드가 리더(Leader) 역할을 하며, 다른 노드들이 해당 큐의 복제본을 유지하는 팔로워(Follower)로 동작합니다. 리더 노드가 다운되더라도 팔로워 중 하나가 새로운 리더로 승격되므로, 다운된 노드를 복구하지 않더라도 메시지 처리가 지속될 수 있습니다.
이를 알게 되고, 쿼럼 큐로 노드를 구성하는 방법을 생각했었지만 현재 Docker Compose로 RabbitMQ Cluster를 구축한 만큼, 단일 서버에서 Docker Compose로 운영하는 구조상, 서버에 장애가 발생하면 모든 노드가 영향을 받기 때문에 쿼럼 큐를 사용해도 장애 대응이 어렵다고 판단했습니다.
따라서 쿼럼 큐 대신에 일반 큐로 노드를 구성했습니다.
원래는 아래처럼 단일 큐를 생성하는 로직이었지만 Cluster를 구축한 후에는 모든 노드에 큐가 고르게 분산 생성이 되어야 합니다.
@Bean
public Queue auctionQueue() {
return QueueBuilder.durable("auction.queue")
.deadLetterExchange("auction.dlx")
.build();
}
@Bean
public Binding auctionBinding(
Queue auctionQueue,
CustomExchange auctionExchange
) {
return BindingBuilder
.bind(auctionQueue)
.to(auctionExchange)
.with("auction")
.noargs();
}
모든 노드에 큐를 고르게 분산하기 위해 x-queue-master-locator, min-masters를 argument로 선언해주었습니다.
위처럼 큐와 바인딩을 등록하게 되면 똑같은 코드가 반복되므로, 반복을 줄이기 위해 Declarables로 큐, 바인딩을 한 번에 등록하도록 로직을 수정했습니다.
@Bean
public Declarables auctionQueuesAndBindings(CustomExchange auctionExchange) {
List<Declarable> declarableList = new ArrayList<>();
for (int i = 0; i < queueNames.length; i++) {
Queue queue = QueueBuilder.durable(queueNames[i])
// 노드에 큐 분산
.withArgument("x-queue-master-locator", "min-masters")
.deadLetterExchange(AUCTION_DLX)
.build();
declarableList.add(queue);
Binding binding = BindingBuilder.bind(queue)
.to(auctionExchange)
.with(routingKeys[i])
.noargs();
declarableList.add(binding);
}
return new Declarables(declarableList.toArray(new Declarable[0]));
}
메시지는 다양한 분산 방법이 있겠지만, 노드의 상태를 보고 메시지를 분산시키는 방식과 해시 기반으로 메시지를 분산하는 방식 중에 고민했습니다.
노드의 상태를 보고 메시지를 분산시키는 것을 우선으로 두었지만 시간이 부족해 간단하게 구현할 수 있는 해시 기반 방식을 선택했습니다.
그렇기 때문에 auctionEvent의 해시 값을 기반으로 라우팅 키를 결정하여 메시지가 분산되도록 합니다.
private String selectRoutingKey(Object auctionEvent) {
int queueNumber = auctionEvent.hashCode() % routingKeys.length + 1;
return AUCTION_ROUTING_KEY_PREFIX + queueNumber;
}
메시지를 1600여개 정도 보낸 결과, 아래와 같이 분산되었습니다.
모든 노드에 메시지가 완전히 균일하게 분배되지는 않지만, 단일 노드 대비 각 노드의 부담이 평균 약 1/3로 줄었습니다.
