RabbitMQ를 이용한 SNS 글쓰기 성능 개선


톰캣은 사용자의 요청을 어떻게 처리할까?

사용자의 요청은 우선 큐에 들어가고 큐에 들어간 요청이 늘고있는 스레드가 있다면

그 스레드에 할당되어 처리된다.

톰캣 기본 설정은 큐 사이즈는 100, 스레드 사이즈는 200이다.

모든 스레드가 사용 중이면 새로운 요청이 들어왔을 때 그 요청은 큐에서 대기하는 것이다.


큐 사이즈를 모두 채우고 나서도 계속 요청이 들어오면 그 요청들은 버려진다.

큐에 들어온 요청도 30초가 지나면 타임아웃 처리된다. (기본설정이 30초)


물론 이 기본 설정들을 변경할 수 있지만 결과적으로 해결 방법이 되는 것은 아니다.

실제 처리 속도를 올리지 않으면 결국 요청이 큐에 쌓일 것이다.



Message Queue

이전 게시글에서 CQRS 구현 방법에 대해 알아보면서 메시징 수단을 이용해 DB에 반영할 데이터를 전달할 수 있다고 했다.

여기서 메시징 수단이 바로 Message Queue이다.

MQ 사용 목적은 비동기로 요청을 처리하고 큐에 저장하여 Consumer의 명목을 줄이는 것에 있다.



RabbitMQ

RabbitMQ는 AMQP(Advanced Message Queueing Protocol)을 구현한 오픈소스 메세지 브로커(중개자)이다.

Rabbit MQ는 데이터를 일단 어딘가에 쌓아두고 나중에 비동기적으로 적절한 처리를 하고 싶은 경우를 위한 데이터 저장소이다.


엠큐


위 이미지가 AMQP을 나타낸 것이다.

예) 업무 내용을 분류해서 메신저로 보내주시면 처리 후에 결과 알려드리겠습니다.



Message와 Queue 보존

메시징이 일시적으로 문제가 발생하게 되면 쿼리 디비를 반영해야 할 데이터가 유실될 수 있다고 한다.

RabbitMQ가 종료되면 Queue와 안에있는 message는 모두 제거된다.

하지만 Queue를 선언할 때 durable 속성을 true로 설정하면 RabbitMQ가 종료된 후 다시 시작될 때 해당 Queue는 다시 자동으로 생성된다.

하지만 이렇게 해도 Queue 내부의 메세지는 여전히 삭제된다.

이를 방지하려면 Publisher가 message를 Exchange로 보낼 때 persistent 속성을 부여하면 된다.

그러면 메세지도 다시 생성될 것이다.



스크린샷 2021-11-14 오후 12 48 03


현재 개발하고 있는 SNS 프로젝트에 Message Queue를 적용하면 위와 같은 모습이다.

DB에 데이터를 저장하기 전에 사용자의 글 작성 요청을 모두 Queue에 넣었다가 처리한다.

이렇게 한다고 톰캣 큐를 사용하지 않는 것이 아니다.

여전히 Nginx에게 요청을 받을 때는 여전히 톰캣 큐를 사용하고 있다.


Q. 그렇다면 Tomcat의 Queue에 넣었다가 처리하는 것과 무슨 차이가 있는걸까?

A. Tomcat 큐에 넣는건 메모리에 저장된 데이터로 애프리케이션 강제 종료시 전부 날아갈 수 있다.

반면 Message 큐를 별도로 사용하면 디스크에 저장하는 등 여러가지 옵션을 줄 수 있다.



Message Queue 특징 & 장점

비동기(Asynchronous)

요청이 몰릴 때에도 저장했다가 처리할 수 있다.

즉 DB 속도와 무관하게 모든 요청을 처리할 수 있다는 것이다.

앞쪽 애플리케이션은 실제 로직이 수행되는 것과 무관하게

단순히 큐에 넣고 다음 요청을 받을 수 있는 상태가 되는 것이다.


애플리케이션간 의존성 제거(비동조, Decoupling)

API를 직접 호출하는 것과 중간에 큐가 있는 것 중 뒷쪽에 있는 애플리케이션이 중단되었을 때에도 메시지가 유실되지 않는다.


과잉(Redundancy)

실패할 경우 재실행 가능하다.


보증(Guarantees)

Queue에 따로 적재된 작업들을 모니터링 할 수 있다.


확장성(Scalable)

다양한 애플리케이션이 message를 생산할 수 있다.


이중화

큐도 결국 애플리케이션이다. 큐도 죽을 수 있는데 이중화도 가능하다.

큐끼리 동기화하기 때문에 우리는 하나의 큐인 것처럼 사용하지만 실제 이중화된 큐를 사용할 수 있다.


신뢰성

실패한 메시지는 큐로 Ack하지 않기 때문에 그 메시지는 큐에서 빠져나가지 않는다.

하지만 절대로 유실되지 않는다고 보장할 수는 없다.

유실되면 안되는 메시지는 로깅을 철저히하여 유실되더라도 복구할 수 있게 준비해야 한다.


확장성

애플리케이션이 스케일 아웃 하더라도 메시지큐에서 따로 처리해줄 필요는 없다.

사용하던 큐를 그대로 사용할 수 있다.



RabbitMQ 사용법

설치

docker run -d --hostname my-rabbit --name some-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management


Consumer 예제 코드

@Component
public class Consumer {

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    PostRepository postRepository;

    @RabbitListener(queues = "CREATE_POST_QUEUE")
    public void handler(String message) throws JsonProcessingException {
        Post post = objectMapper.readValue(message, Post.class);
        postRepository.save(post);
    }
}


Producer 예제 코드

@Component
public class Producer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendTo(String message) {
        this.rabbitTemplate.convertAndSend("CREATE_POST_QUEUE", message);
    }

}



Producer, Consumer 애플리케이션 분리

스크린샷 2021-11-14 오후 1 13 35


위에서 mq를 적용한 것은 하나의 애플리케이션에 producer와 consumer가 존재하는 구조이다.

하지만 producer 애플리케이션과 cunsumer 애플리케이션을 분리해서 사용하는 것이 좋다.

분리되지 않는 것과 분리된 것의 차이는 consumer 애플리케이션을 배포할 때 얼마나 까다롭냐이다.

분리된 형태에서는 consumer를 배포할 때 앞쪽에 mq가 존재하기 때문에 모든 consumer가 종료되어도 문제가 없다.

무중단 배포도 쉽게 구축할 수 있다.

하지만 분리되지 않은 형태의 경우 consumer 기능을 배포하기 위해

애플리케이션을 배포할 때 무중단 배포를 위한 요소들이 고려되어야 한다.



MQ 적용하면 요청 처리 시간이 감소할까?

MQ 서버와 통신하기 위한 네트워크 I/O가 늘어나는데 성능에 문제가 없을까?

현재 MQ를 도입하는 이유는 사용자가 보낸 요청을 처리하지 못해 요청이 유실되는 경우를 방지하는 것이 주목적이다.

즉 요청 하나를 처리하는 시간 자체가 빨라진다기 보다는 더 많은 요청이 들어 왔을 때 유실 없이 처리 가능하다.



MQ는 어느 상황에 필요한가?

비동기 작업을 처리할 때 좋다.

즉 사용자가 요청했지만 응답을 받을 필요가 없거나 즉시 받을 필요가 없는 경우에 해당한다.

예) 클라이언트가 응답을 받을 필요가 없는 경우   
여러 서버에서 발생한 로그를 쌓는 작업   
→ 로그를 정상적으로 발송했고 MQ에 넣었다면 저장이 잘 되었는지 클라이언트는 알 필요가 없다.   



메시지를 Consume 하는 주체는 애플리케이션이다.

1. 애플리케이션은 MQ에서 일정 개수 만큼 Consume한다. 
2. 애플리케이션은 Consnume한 메시지를 들고있고 DB에 insert한다. 
3. 메시지 하나를 insert하면 다음 메시지를 consume한다. 

위 과정이 반복된다.

결과적으로 애플리케이션이 메시지를 Consume하고 싶을 때 한다.

DB에 insert하는 속도가 느리다면 애플리케이션은 Consume을 느리게 할 것이다.

MQ가 애플리케이션에 강제로 메시지를 보내지 않는다.

애플리케이션이 주체이다.



MQ가 하나이고 Consume가 여러개인 경우 경쟁상태가 발생하는가?

하나의 메시지를 두 개의 Consumer가 동시에 coonsume하는 경우는 발생하지 않는다.

내부적으로 그렇게 구현되어 있기 때문에 경쟁상태는 발생하지 않는다.