docker run -d --hostname my-rabbit --name some-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management
여기서 포트를 두 개 열었는데
5672 포트는 RabbitMQ와 통신하기 위한 포트이고,
15672 포트는 모니터링 툴을 사용하기 위한 포트이다.
로컬에 해당 이미지가 없어도 Docker hub에서 알아서 가져와 컨테이너를 만들어준다.
15672 포트로 접근하여 username : guest / pw : guest 로 로그인하면 위와 같은 모니터링 화면이 나온다.
원래는 Admin 계정을 생성해서 관리해야하지만, 현재는 실습을 위해 Guest 계정을 이용한다.
Queues 탭으로 이동해, 아래 Add a new queue 버튼을 클릭 하여 새로운 큐를 만들어준다.
나머지는 디폴트 설정을 그대로 가져가고 Name만 지정해준다.
생성된 큐를 클릭해 들어오면 위와 같은 화면이 나오는데, 여기서 Publish message를 클릭해
메시지를 넣어보자.
Payload에 값을 추가 후 Publish message 버튼을 누르자.
Get messages 항목에서 아까 넣은 메시지를 볼 수 있다.
Ack Mode를 Nack message requeue true로 하면 해당 메시지는 다시 큐로 들어간다.
postman을 이용해 요청을 날려보면 정상적으로 응답이 오는걸 볼 수 있다.
id값은 따로 지정해주지 않았기 때문에 null이 나오는게 정상이다.
다시 RabbitMQ 모니터링 화면으로 돌아가서 메시지를 뽑아보면
방금 넣은 데이터를 볼 수 있다.
이제 이 데이터를 Consume해서 DB에 넣어줘야 한다.
이 과정을 Consume이라고 한다.
@RabbitListener 어노테이션을 이용한다.
queues 값으로 아까 생성한 큐의 이름을 매핑시켜주면 큐를 계속 컨슘하고 있는 상태가 된다.
즉 큐에 메시지가 들어오면 handler() 메서드가 호출된다.
아까 큐에 메시지를 넣을 때 json 형태로 넣었기 때문에, 큐에서 메시지를 꺼내고 DB에 저장하기 위해서 다시 Post 클래스 형태로 변경해줘야 한다.
지금까지 로컬환경에서 RabbitMQ를 적용하고 테스트 해보았다.
다음은 배포 환경에서 동작시키기 위해 RabbitMQ 인스턴를 만들자.
sudo yum -y install docker // docker 설치
sudo systemctl start docker // docker 실행
sudo chmod 666 /var/run/docker.sock // docker 실행 권한 부여
docker run -d --hostname my-rabbit --name some-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management // rabbitmq 컨테이너 생성 및 실행
생성한 rabbitmq 인스턴스의 외부 ip로 접근해 로컬에서와 똑같이 Queue를 만들어준다.
이 때 Name은 이전과 동일하게 한다. 이미 애플리케이션 코드에 CREATE_POST_QUEUE 로 작성했기 때문이다. 물론 변경하고 싶으면 해도 된다.
인스턴스를 생성후, 해당 인스턴스에 RabbitMQ를 도커 컨테이너로 띄웠다.
이제 배포환경에서 동작시키기 위해 설정파일을 수정하고 원격지에 코드를 적용해보자.
datasource의 url을 로컬에서, GCP postgresql 인스턴스의 ip로 변경한다.
rabbitmq의 설정을 적용한다. 각 워커 인스턴스들은 내부에서 붙기 때문에 host에 rabbitmq 인스턴스의 내부 ip를 적어주면 된다.
merge 하는 과정은 이전 글에 많이 했기 때문에 생략한다.
위와 같이 로컬과 origin에 있는 main, develop 모두 적용된 걸 볼수있다.
로컬에서 개발한 기능을 origin에도 적용했다.
이제는 글 목록을 캐싱하는 기능을 추가하고 배포해보자.
@EnableScheduling 어노테이션을 메인 클래스에 추가해야 한다.
@Scheduled 어노테이션을 이용한다.
cron 표현식을 작성해주면 되는데 위에 작성한 cron표현식은 1초에 한번씩
updateFirstPostPage() 메서드가 실행된다.
핵심만 간단히 보면 1초마다 첫 번째 페이지의 글 목록을 조회한 후 firstPostPage 필드에 저장하는 것이다.
지금까지 첫 페이지를 조회하는 요청에대해 캐시처리를 했다.
이제 원격지에 코드 적용 후 jenkin로 배포하자.
어제와 같은 현상이다. 배포를 다시 해보자..
배포해도 계속 같은 상황이다. 로컬에서 테스트해보면 정상적으로 동작한다.
—> 코드에 문제가 있는것은 아니다.
그러던 도중 다시 Nginx로 요청 날려보니 갑자기 정상적으로 동작한다.
그냥 각 워커 인스턴스에 배포하고 애플리케이션이 실행되는데 시간이 좀 걸리나보다.
앞으로는 조금 기다려보자..
지금까지 io bound 애플리케이션에 RabbitMQ를 적용해 보았다.
다음에는 스트레스 테스트를 진행해 적용 전과 후의 차이점을 비교해보자.
위에서 RabbitMQ를 적용했을 때는 Producer와 Consumer가 한 애플리케이션에 존재하게 된다.
하지만 실무에서는 Producer 애플리케이션과 Consumer 애플리케이션을 분리해서 사용하는게좋다.
아래 처럼 말이다.
사용자 ↔ Producer ↔ RabbitMQ ↔ Consumer
분리 되지 않았을 때와 분리 되었을 때의 가장 큰 차이점은 Consumer 애플리케이션을 배포할 때 얼마나 까다롭냐이다. 분리된 형태에서는 Consumer를 배포할 때 앞쪽에 RabbitMQ가 존재하기 때문에 모든 Consumer 가 종료되어도 문제 없다.
하지만 분리되지 않은 형태의 경우 Consumer 기능을 배포하기 위해 애플리케이션을 배포할 때 무중단 배포를 위한 요소들이 고려되어야 한다.
캐싱 처리는 위 처럼 애플리케이션 내부에서 구현하는 것 보다 주로 Redis를 이용하는게 더 효율적이다.
현재는, 애플리케이션 서버(Producer, Consumer) ↔ MQ 서버 ↔ DB 서버 와 같은 구조이다.
MQ서버를 적용하기 전에는, 애플리케이션 ↔ DB 서버 와 같은 구조이다.
그러면 중간에 MQ서버와 통신하기 위한 네트워크 I/O가 늘어나는데 성능에 문제가 없을까?
우리가 MQ를 도입한 이유에 대해서 다시한번 생각해보아야 한다. 이전 글에서 말했듯이 어떠한 이유가 됬든 사용자가 보낸 요청을 처리하지 못하여 요청이 유실되는 경우를 방지 할 수 있다.
즉 요청 하나를 처리하는 시간 자체가 빨라진다기 보다는 더 많은 요청이 들어 왔을 때 유실 없이 처리 가능하다.
주로 비동기적인 작업을 처리할 때 좋다. 즉 사용자가 요청했지만 응답을 받을 필요가 없거나
즉시 받을 필요가 없는 경우에 해당된다.
클라이언트가 응답을 받을 필요가 없는 경우 : 여러 서버에서 발생한 로그를 쌓는 작업 → 로그를 정상적으로 발송했고, MQ에 넣었다면 그게 어떤 곳에 저장되는지, 저장이 잘 되었는지 클라이언트는 알 필요가 없다.
일단 하나의 메시지를 두 개의 Consumer가 동시에 Consume 하는 경우는 발생하지 않는다.
내부적으로 그렇게 구현되어 있다. 결과적으로 경쟁상태는 발생하지 않는다.
애플리케이션은 MQ에서 일정 개수 만큼 Consume 한다.(디폴트 설정은 250개)
애플리케이션은 Consume한 메시지를 들고있고 DB에 insert 한다.
메시지 하나를 insert하면 다음 메시지를 Consume 한다.
위 과정이 반복된다.
결과적으로 애플리케이션이 메시지를 Consume 하고 싶을때 한다. DB에 insert하는 속도가 느리다면 애플리케이션은 Consume을 느리게 할 것이다.
MQ가 애플리케이션에게 강제로 메시지를 보내지 않는다.
오해하지 말자 애플리케이션이 주체이다.