Skip to content

Design Distributed Message Queue

현대 소프트웨어 아키텍처는 작고 독립적인 서비스로 구성되며, 메시지 큐는 서비스 사이의 통신과 조율을 담당하게 되면서 다음과 같은 이점을 제공한다.

  • 결합도 완화(decoupling): 컴포넌트 사이 강한 결합을 제거하고, 각 컴포넌트들을 독립적으로 갱신 가능
  • 규모 확장성 개선: 메시지 큐에 데이터를 생산하는 생산자와 큐에서 메시지를 소비하는 소비자 시스템 규모를 트래픽 부하에 맞게 조정 가능
  • 가용성 개선: 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 통신 가능
  • 성능 개선: 생산자는 응답을 기다리지 않고 메시지를 전송 할 수 있고, 소비자는 메시지가 있을 때만 처리하게 되어 비동기 통신을 원활하게 함
  • 메시지 형태: 텍스트
  • 메시지 평균 크기: 수 KB
  • 하나의 메시지가 하나의 소비자 / 여러 소비자에게 전달 설정 가능
  • 생산된 순서대로 소비
  • 데이터 지속성 2주 보장
  • 메시지 전달 방식 최소 한 번(at-least-once) / 최대 한 번(at-most-once) / 정확히 한 번(exactly-once) 설정 가능

가장 널리 쓰이는 메시지 모델은 일대일(point-to-point)과 발행-구독(publish-subscribe)이 존재한다.

  • 일대일 모델
    • 각 메시지는 오직 한 소비자에게만 전달
    • 어떤 소비자가 메시지를 가져갔다는 사실을 큐에 알리면(acknowledge) 해당 메시지는 큐에서 삭제
    • 큐에 저장됐던 데이터 보관을 지원하지 않음
  • 발행-구독 모델
    • 해당 토픽을 구독하는 모든 소비자에게 전달
    • 메시지를 주고 받을 때 토픽에 보내고 받는 방식(토픽 = 메시지의 주제 개념)

발행-구독 모델은 메시지가 토픽에 저장되는데, 보관되는 데이터의 양이 커지게 되면 파티션을 나누어 해결할 수 있다.

  1. 하나의 토픽을 여러 파티션으로 분할
  2. 메시지를 모든 파티션에 균등하게 나누어 전송

트래픽이나 데이터 양이 많아질 수록 파티션이 증가하게 되는데, 파티션을 유지하는 서버를 보통 브로커라고 부른다.

메타데이터 저장소 조정 서비스
↕ ↕
생산자 -> 브로커 (데이터 저장소, 상태 저장소) -> 소비자 (소비자 그룹)
  • 생산자: 메시지를 특정 토픽으로 전송
  • 소비자 그룹: 토픽을 구독하고 메시지 소비
  • 브로커: 파티션 유지
  • 데이터 저장소: 메시지를 파티션 내 데이터 저장소에 보관
  • 상태 저장소: 소비자의 상태 보관
  • 메타데이터 저장소: 토픽 설정 / 토픽 속성 등 저장
  • 조정 서비스
    • 서비스 탐색: 어떤 브로커가 살아있는지 감지
    • 리더 선출: 브로커 가운데 하나를 컨트롤러 역할로 선출(한 클러스터에는 하나 이상의 컨트롤러가 필요)

메시지 큐의 트래픽 패턴은 다음과 같다.

  • 읽기와 쓰기 빈번
  • 순차적 읽기/쓰기가 대부분
  • 갱신/삭제 연산 발생 X

생각해 볼 수 있는 선택지로는 관계형/비관계형 데이터베이스가 존재하지만, 읽기/쓰기가 대규모로 빈번하게 발생하기 때문에 적합하지 않다.

쓰기 우선 로그(Write-Ahead Log, WAL)

Section titled “쓰기 우선 로그(Write-Ahead Log, WAL)”

WAL은 새로운 항목이 추가되기만 하는 일반 파일로, 메시지 큐에 적합한 데이터 저장소로 사용할 수 있다.

  • 새로운 메시지가 파티션 꼬리 부분에 추가되는 방식
  • 접근 패턴이 순차적이기 때문에 디스크 I/O 최소화
  • 순차 접근이기 때문에 회전식 디스크 환경에서도 빠른 데이터 접근 가능

메시지 구조는 생산자와 메시지 큐, 소비자 사이의 계약이라고 볼 수 있다.

필드데이터 자료형설명
keybyte[]파티션을 정하는 키
valuebyte[]메시지의 내용(=payload)
topicstring메시지가 속한 토픽
partitioninteger메시지가 속한 파티션
offsetlong파티션 내 메시지의 위치
timestamplong메시지 생성 시간
sizeinteger메시지 크기
crcinteger순환 중복 검사의 약자로, 데이터 무결성 보장에 사용

생산자 / 소비자 / 메시지 큐는 메시지를 가급적 일괄 처리하게 되는데, 일괄 처리는 시스템 성능에 많은 영향을 미친다.

  • 한 번의 네트워크 요청으로 처리하여 네트워크 왕복 비용 완화
  • 여러 메시지가 한 번에 로그에 기록되면, 큰 규모의 순차 쓰기 연산이 발생하여 디스크에 연속된 공간으로 기록됨(대역폭 상승)

하지만 한 번에 많은 양을 처리할수록, 메시지 큐의 지연 시간이 증가하게 되기 때문에 적절한 균형을 찾아야 한다.

메시지 큐는 푸시(push)와 풀(pull) 방식으로 메시지를 소비할 수 있다.

  • 푸시 모델: 브로커가 소비자에게 메시지를 전달하는 방식
    • 즉시 소비자에게 전달하여 지연 시간 감소
    • 소비자 메시지 처리 속도가 생산자 생성 속도보다 느린 경우 높은 부하 가능성 존재
    • 생산자 생성 속도에 맞춰 소비자의 컴퓨팅 자원을 준비해 두어야 함
  • 풀 모델: 소비자가 메세지를 가져가는 방식
    • 메시지 소비 속도를 알아서 결정하여 실시간 / 일괄 처리 선택 가능
    • 소비 속도가 느리더라도 부하가 생기지 않음
    • 쌓인 모든 메시지를 한 번에 가져가 일괄 처리 가능
    • 브로커에 메시지가 없어도 불필요한 풀링 요청으로 자원 낭비 가능성 존재(롱 풀링으로 문제 완화)

메시지 전달 방식은 최소 한 번(at-least-once) / 최대 한 번(at-most-once) / 정확히 한 번(exactly-once)으로 나뉜다.

  • 최대 한 번: 메시지가 전달 과정에서 소실되더라도 다시 전달하지 않음
  • 최소 한 번: 메시지가 브로커에게 전달되었음을 반드시 확인하는 방식으로, 메시지 손실되지 않음
  • 정확히 한 번: 성능 및 구현 복잡도가 높은 방식으로, 중요한 데이터 전달에 사용

Last updated:

Large-Scale System