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)

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

  • 새로운 메시지가 파티션 꼬리 부분에 추가되는 방식

  • 접근 패턴이 순차적이기 때문에 디스크 I/O 최소화

  • 순차 접근이기 때문에 회전식 디스크 환경에서도 빠른 데이터 접근 가능

메시지 자료 구조

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

필드
데이터 자료형
설명

key

byte[]

파티션을 정하는 키

value

byte[]

메시지의 내용(=payload)

topic

string

메시지가 속한 토픽

partition

integer

메시지가 속한 파티션

offset

long

파티션 내 메시지의 위치

timestamp

long

메시지 생성 시간

size

integer

메시지 크기

crc

integer

순환 중복 검사의 약자로, 데이터 무결성 보장에 사용

일괄 처리

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

  • 한 번의 네트워크 요청으로 처리하여 네트워크 왕복 비용 완화

  • 여러 메시지가 한 번에 로그에 기록되면, 큰 규모의 순차 쓰기 연산이 발생하여 디스크에 연속된 공간으로 기록됨(대역폭 상승)

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

푸시 vs 풀

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

  • 푸시 모델: 브로커가 소비자에게 메시지를 전달하는 방식

    • 즉시 소비자에게 전달하여 지연 시간 감소

    • 소비자 메시지 처리 속도가 생산자 생성 속도보다 느린 경우 높은 부하 가능성 존재

    • 생산자 생성 속도에 맞춰 소비자의 컴퓨팅 자원을 준비해 두어야 함

  • 풀 모델: 소비자가 메세지를 가져가는 방식

    • 메시지 소비 속도를 알아서 결정하여 실시간 / 일괄 처리 선택 가능

    • 소비 속도가 느리더라도 부하가 생기지 않음

    • 쌓인 모든 메시지를 한 번에 가져가 일괄 처리 가능

    • 브로커에 메시지가 없어도 불필요한 풀링 요청으로 자원 낭비 가능성 존재(롱 풀링으로 문제 완화)

메시지 전달 방식

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

  • 최대 한 번: 메시지가 전달 과정에서 소실되더라도 다시 전달하지 않음

  • 최소 한 번: 메시지가 브로커에게 전달되었음을 반드시 확인하는 방식으로, 메시지 손실되지 않음

  • 정확히 한 번: 성능 및 구현 복잡도가 높은 방식으로, 중요한 데이터 전달에 사용

참고자료

Last updated

Was this helpful?