RabbitMQ 서버 구축 및 사용기
1. 왜 RabbitMQ를 사용했는가?
요즘 핫한 Kafka를 사용하지 않고 RabbitMQ를 사용한 이유는 그 정도의 성능이 필요하지 않아서입니다. Kafka를 사용하는 이유를 개인적으로 생각해봤을때 다음의 이유라고 생각합니다.
- 분산된 환경에서 브로커를 구축하고 클러스터로 묶은 뒤, 주키퍼(요즘은 쿠버네티스도 있다고 하지만)를 이용하여 오케스트레이션하여 고가용성과 팔로워 노드의 편리한 관리 ( 리더 선정, IRS 관리 )
- 특정 토픽에 대해서 각 파티션 마다의 소비자 오프셋을 이용하여 안정적인 단 한번만 전송(이름이 뭐더라?) 시스템 보장
- 1번과 비슷하지만, 쉬운 확장성
- 다양한 플러그인과 최적화
- 페이지 캐시나 압축 등 다양한 최적화
그럼에도 RabbitMQ를 사용 한 이유는 다음과 같습니다.
- 리소스 부족
- Kafka의 성격 상 소비자 오프셋을 통한 개발 1회 전송 시스템 구축과 고가용성 보장을 위해서는 적어도 3개 이상의 브로커가 필요합니다.
- JSON 기반의 단순한 문자열 전송
- 단순한 형태의 문자열을 전송하기 때문에 압축이나 캐싱, 다양한 플러그인 등이 크게 필요하지 않았습니다.
- 지연 전송의 필요
- 지연 전송은 카프카로 구축하기 어려운 부분입니다. 하지만 rabbitmq는 exchange와 queue의 역할이 분할 되어 있기 때문에 exchange에 입력이 들어온 데이터를 queue로 전송하기 까지의 시간을 조절 할 수 있는 플러그인이 있습니다.
2. RabbitMQ란?
RabbitMQ는 기본적으로 AMQP 프로토콜을 구체화 한 메시지 브로커입니다. 그 과정에서 추가 된 부분도 있지만, 거의 동일한 프로세스를 가지고 있기 때문에 AMQP에 대해서 이해 할 필요가 있습니다.
2-1. AMQP란?
AMQP는 Advanced Message Queuing Protocol로 메시지 지향 미들웨어의 표준 프로토콜입니다.
구현체가 아닌 프로토콜이기 때문에 “우리는 이렇게 통신을 할거야” 정도의 가이드라인을 정의 한 것이고 그 구성요소는 다음과 같습니다.
- Producer : 메시지를 생성해서 exchange에 전달하는 클라이언트 입니다.
- Exchange : 메시지를 받아서 라우팅 규칙에 따라 큐에 전달하는 메시징 시스템의 구성 요소입니다. 유형으로는 direct, fanout, topic, header 가 있습니다.
- Queue : 메시지를 저장하는 메시징 시스템의 구성 요소입니다. 기본적으로 메모리에 저장되지만, 디스크에 영구 보관 할 수 있습니다.
- Consumer : 큐에서 메시지를 가져와서 처리하여 소비하는 클라이언트 입니다.
2-2. RabbitMQ는 왜?
RabbitMQ는 위에서 소개 한 AMQP를 구체화 한 메시지 브로커로 기본적으로 RPC 프로토콜을 사용하고 있습니다. 또한 관리와 모니터링을 위해 간단한 웹 서비스를 제공해주고 있습니다.
왜 AMQP를 쓰는 것인가?
여기는 개인적인 생각입니다.
카프카의 등장 전 RabbitMQ와 ActiveMQ 등 몇가지 메시지 브로커가 등장하고 쓰이는데에 AMQP 프로토콜을 제대로 구축 한 메시지 브로커는 RabbitMQ라고 단언 할 수 있습니다.
AMQP를 쓴 이유를 보면 적은 서버 수에서 최적의 결과를 낼 수 있는 시스템에 가장 가까운게 아닐까 생각했습니다. 물론 exchange라는 전달 된 메시지를 큐에 넣기 전 단계의 유용함도 있지만요.
카프카의 경우에는 기본적으로 컨슈머 그룹에 속한 컨슈머 수 만큼의 파티션은 존재해야 합니다. 1(브로커):1(컨슈머)의 원리를 유지함으로써 컨슈머 오프셋을 이용한 메시지 안전성을 보장 할 수 있기 때문이죠.
그에 비해 RabbitMQ는 메시지를 소비하면 사라집니다. 디스크에 데이터를 보관한다고는 하지만, 과거 데이터를 보관하는게 아닌 현재 큐에 담긴 메시지를 보관하여 문제가 발생하더라도 언제든 읽어 올 수 있는 정도이죠. 카프카처럼 오프셋을 관리해서 과거 데이터도 오프셋 0만 찍어주면 다시 사용 할 수 있는 시스템은 아닙니다. 안전성은 상대적으로 떨어지죠.
그렇다고해서 REST API처럼 요청을 날리면 큐에서 삭제하는 방식은 아닙니다. basic_ACK라는 “나 정상적으로 실행 됐어!”라는 문자를 받아야 지웁니다. 파이썬(pika 라이브러리)에서는 basic_publish 함수에서 Exception이 나오면 NACK를 리턴이 되면 ACK를 보내더라구요.
그렇다고 안 좋은 면만 있는 건 아닙니다. 큐의 안전성이 조금 떨어지는 대신 exchange는 다수의 큐에 fanout으로 데이터를 복제 할 수 있습니다. 즉 1개 서버에서 1개의 토픽을 여러 컨슈머에게 전달 할 수 있죠.
최소한의 안전성을 보장하면서 최대한의 효율을 내려는 노력으로 AMQP프로토콜을 따르는게 아닌가 싶습니다.
왜 REST API가 아닌 RPC를 쓰는 것 인가?
RabbitMQ를 사용하면서 가장 궁금했던 점이였습니다. 백엔드 프로그래밍을 하다보면 Rest API의 장점에 취하게 되는데 왜 Rest API 방식이 아닌 RPC 방식을 사용하는건지 궁금했었고, ChatGPT와 많은 블로거분들의 지식으로 어느정도 알게 되었습니다.
첫 번째로 인터페이스 입니다.
REST API는 특정 URL에 Request body를 Json이나 xml과 같은 문자열을 포함시켜서 서버에 요청을 하는 방식입니다. 어떤 요청인지는 주로 URL 경로를 통한 Listening 하는 API로 넘겨주는데 결국 사용하는 클라이언트가 이러한 내용을 모두 알고 있어야 한다는 문제가 있습니다.
예를들면 ‘/main/alert/{event}’라는 Listening 중인 API가 열려 있으며, 그 URL로 그에 맞는 HTTP Method와 해당 요청에게 기대되어지는 변수들을 포함한 JSON 문자열이 입력되어야 정상적으로 동작합니다.
범용성은 확실히 높아진 부분이 있지만, 클라이언트 입장에서는 처리 중간중간의 액션에 대해서 특정한 이벤트를 활용하고 싶거나 업데이트를 통해 파라미터가 변하거나 할 경우 입력 변수가 달라지는 문제가 생기면 처리하기 어렵습니다.
액션 중간중간에 이벤트를 하기 위해서는 서버측에서도 별도의 변수로 열어주어야겠죠.
하지만 RPC는 다릅니다. RPC는 클라이언트와 서버가 각자의 함수를 리턴해주는 방식으로 A 서버에서 B 서버의 C 함수에 D 파라미터 값으로 실행시킨다는 느낌이라고 볼 수 있습니다. 이 경우에 에러가 발생하면 B 서버의 호스팅, C 함수의 파라미터 부정확함, A 서버의 콜백 부재 등 몇 가지의 확실한 에러 위치를 추정해낼 수 있죠.
이러한 인터페이스는 RabbitMQ에서는 AMQP 프로토콜의 기본 골자를 사용하고 있고, 그러기 때문에 RabbitMQ를 지원하는 라이브러리를 사용 할 때 해당하는 함수를 오버라이딩해주면 실행 중간의 콜백마다 원하는 이벤트를 추가 할 수 있습니다.
예를들면 A 메시지를 받을 때 이 메시지가 에러에 관련한 로그인지, 아니면 디버깅인지 파악해서 에러 일 경우만 필터링 할 수 있고, stdout으로 출력 할 수도 있죠. 콜백도 정형화 되어 있기 때문에 exchange에서 큐로 데이터를 전송 할 때 제대로 전달 되지 않으면 다른 서버로 전달하는 등 다양한 액션도 가능합니다.
두 번째는 효율 입니다.
REST API는 기본적으로 JSON이나 XML과 같은 특정한 유형의 텍스트를 네트워트를 통해 서버로 전달합니다. 하지만 RPC는 바이너리로 데이터를 전달하기 때문에 대부분의 경우 적은 용량의 데이터가 네트워크를 통해 옮겨지고 이런 부분은 상당한 효율성을 만들어냅니다.
개발이 조금 어려운 부분은 있지만, 저도 RabbitMQ를 사용하면서 데이터를 전달 할 때 gzip으로 압축을 한 뒤 옮기는 것이 생활화되어서 상당히 효율적인 것 같습니다. 문제는 받은 뒤에서 디코딩을 해줘야하긴하는데 그래도 네트워크 전송보단 CPU가 고생하는게 낫죠.
3. RabbitMQ 설정
rabbitMQ의 설정 파일은 컴퓨팅 리소스에 관련한 설정은 거의 하지 않았습니다. 대부분 로그 파일과 GC threshold 정도이고, 기본적으로 rabbitMQ에서 사용되는 아이디와 비밀번호 설정 정도 입니다.
# Log settings
log.ra.level = info
log.ra.rotation.size = 10485760
log.file.rotation.size = 10485760
log.file.rotation.count = 3
log.ra.rotation.count = 3
log.file = rabbitmq.log
log.dir = /var/log/rabbitmq
log.console.level = info
log.channel.file = channel.log
log.exchange = true
log.exchange.level = info
# mnesia config
socket_writer.gc_threshold = 32768
Mnesia라는 건 Erlang 기반의 분산 데이터베이스 관리 시스템으로 rabbitMQ는 기본적으로 Mnesia를 이용해서 메타데이터를 다루고 있습니다. 이 메타데이터에는 exchange, queue, binding, user, auth, cluster 등이 포함되어 있으며 실질적으로 Queue에 담겨있는 데이터는 디스크나 메모리에 저장되게 됩니다.
아무래도 메모리를 다루는 이상 GC에 대해서는 자유로울 수 없는데 얼마나 많은 양의 데이터가 쌓이면 GC를 수행 할 것인지가 gc_threshold 값 입니다.
4. RabbitMQ 구성 시 유의점
사실 RabbitMQ의 구성에는 큰 어려움이 없습니다. 아마 대부분의 사람은 docker나 kubernetes를 이용하여 서버를 구축 할 것이라고 예상되는데 메시지 브로커의 특성 상 암호화 혹은 RabbitMQ에서 계정과 권한 정도에 관한 내용 빼고는 서버 설정시에 큰 어려움은 없습니다.
4-1. 자원 할당
메시지 브로커 외에 다른 프로세스가 서버에 떠있다면 얼마나 CPU, Memory를 할당 할 것인지는 설정해두는 편이 좋습니다. RabbitMQ는 기본적으로 컴퓨터 리소스를 모두 사용해서(docker에 별도의 제한이 없다면) 메시지 브로커의 역할을 수행합니다. 하지만 서버의 특성 상 여러가지의 프로세스가 떠있을 수 있는데 너무 많은 메모리를 RabbitMQ가 사용해버리면 다른 프로세스의 동작에도 문제가 발생 할 수 있으므로 주의 해야 합니다.
4-2. 계정 관리
RabbitMQ는 계정을 사용하여 서비스를 사용합니다. 특정 언어에서 지원하는 라이브러리(Python의 pika등)를 사용 할 때에도 계정과 비밀번호를 입력해야 합니다.
그러기때문에 계정을 생성 관리하고, 또한 권한도 설정해주어야 하므로 이런 부분을 구성 시 유의해야 합니다.
4-3. exchange와 queue의 관리
pika 라이브러리와 같이 서비스를 사용하면 Exchange(Queue)Declare 와 같은 함수와 ExchangeBinding, QueueBinding 등의 함수들을 보게 됩니다. Declare는 실제로 해당 Exchange(Queue)를 생성하는 것이고 Binding은 source Exchange에 target Exchange(Queue)를 연결하여 메시지를 전달(routing_key로 구분되거나 그렇지 않은)하는 역할을 합니다.
만약 이미 만들어져 있다면 exchange_type이 다르거나 몇몇 속성이 다름에도 만드려고하면 에러가 생길 수 있으니 유의하여 관리 생성 등을 해야 합니다.
4-4 극악의 도큐먼트
주황색과 흰색의 조합은 눈을 해롭게 합니다.
제발 목차나 주요 주제마다 선 하나만 그어주세요.
4-5 Callback 메소드를 잘 활용
Callback 메소드를 등록 할 수 있도록 설정되어 있습니다.