이번 장에서는 시스템 설계 면접에서 자주 마주하는 주제인, 분산 메시지 큐 설계에 대해 알아본다. 현대적 소프트웨어 아키텍처를 따르는 시스템은 잘 정의된 인터페이스를 경계로 나뉜 작고 독립적인 블록들로 구성된다.

메세지 큐는 이 블록 사이의 통신과 조율을 담당한다. 그럼 메세지 큐를 사용했을 때의 장점은 무엇일까 ?

  • 결합도 완화(decoupling): 메시지 큐를 사용하면 컴포넌트 사이의 강한 결합이 사라지므로 각각을 독립적으로 갱신할 수 있다.
  • 규모 확장성 개선: 메시지 큐에 데이터를 생산하는 생산자(producer)와 큐에서 메시지를 소비하는 소비자(consumer) 시스템 규모를 트래픽 부하에 맞게 독립적으로 늘릴 수 있다. 예를 들어 트래픽이 많이 몰리는 시간에는 더 많은 소비자를 추가하여 처리 용량을 늘릴 수 있다.
  • 가용성 개선: 시스템의 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 상호작용을 이어갈 수 있다.
  • 성능 개선: 메시지 큐를 사용하면 비동기 통신이 쉽게 가능하다. 생산자는 응답을 기다리지 않고도 메시지를 보낼 수 있고, 소비자는 읽을 메시지가 있을 때만 해당 메시지를 소비하면 된다. 서로를 기다릴 필요가 없다.

메시지 큐 대 이벤트 스트리밍 플랫폼

엄밀하게 말하면 아파치 카프카(Apache Kafka)나 펄사(Pulsar)는 메시지 큐가 아니라 이벤트 스트리밍 플랫폼(event streaming platorm)이다. 하지만 메시지 큐(RocketMQ, ActiveMQ, RabbitMQ, ZeroMQ 등)와 이벤트 스트리밍 플랫폼(카프카, 펄사) 사이의 차이는 지원하는 기능이 서로 수렴하면서 점차 희미해지고 있다.

예를 들어 전형적인 메시지 큐 RabbitMQ는 옵션으로 제공되는 스트리밍 기능을 추가하면 메시지를 반복적으로 소비할 수 있는 동시에 데이터의 장기 보관도 가능하다. 그리고 그 기능은 데이터 추가(append)만 가능한 로그(1og)를 통해 구현되어 있는데, 이벤트 스트리밍 플랫폼 구현과 유사하다.
아파치 펄사는 기본적으로 카프카의 경쟁자이지만, 분산 메시지 큐로도 사용 이 가능할 정도로 유연하고 성능도 좋다.

이번 장에서는 데이터 장기 보관(long data retention), 메시지 반복 소비(re-peated consumption of messages) 등의 부가 기능을 갖춘 분산 메시지 큐를 설계해 볼 것이다. 지금 언급한 부가 기능은 통상적으로는 이벤트 스트리밍 플랫 폼에서만 이용 가능하다.

1단계 문제 이해 및 설계 범위 확정

메세지 큐의 기본 기능은, 생산자는 메세지를 큐에 보내고, 소비자는 큐에서 메세지를 꺼낼 수 있으면 된다. 하지만 이 기본 기능 이외에도 성능, 메세지 전달 방식, 데이터 보관 기간 등 고려할 사항은 다양하다. 적절한 질문을 통해 요구사항을 분명히 밝히고 설계 범위를 좁혀야 한다.

기능 요구사항

  • 생산자는 메시지 큐에 메시지를 보낼 수 있어야 한다.
  • 소비자는 메시지 큐를 통해 메시지를 수신할 수 있어야 한다.
  • 메시지는 반복적으로 수신할 수도 있어야 하고, 단 한 번만 수신하도록 설정 될 수도 있어야 한다.
  • 오래된 이력 데이터는 삭제될 수 있다.
  • 메시지 크기는 킬로바이트 수준이다.
  • 메시지가 생산된 순서대로 소비자에게 전달할 수 있어야 한다.
  • 메시지 전달 방식은 최소 한 번, 최대 한 번, 정확히 한 번 가운데 설정할 수 있어야 한다.

비기능 요구사항

  • 높은 대역폭과 낮은 전송 지연 가운데 하나를 설정으로 선택 가능하게 하는 기능
  • 규모 확장성. 이 시스템은 특성상 분산 시스템일 수밖에 없다. 메시지 양이 급증해도 처리 가능해야 한다.
  • 지속성 및 내구성(persisteney and durability). 데이터는 디스크에 지속적으 로 보관되어야 하며 여러 노드에 복제되어야 한다.

전통적 메시지 큐와 다른 점

RabbitMQ와 같은 전통적인 메시지 큐는 이벤트 스트리밍 플랫폼처럼 메시지 보관 문제를 중요하게 다루지 않는다. 전통적인 큐는 메시지가 소비자에 전달되기 충분한 기간 동안만 메모리에 보관한다. 처리 용량을 넘어선 메시지는 디스크에 보관하긴 하는데 이벤트 스트리밍 플랫폼이 감당하는 용량보다는 아주 낮은 수준이다. 전통적인 메시지 큐는 메시지 전달 순서도 보존하지 않는다. 생산된 순서와 소비되는 순서는 다를 수 있다. 그런 차이를 감안하면 설계는 크게 단순해질 수 있다.

2단계 개략적 설계안 제시 및 동의 구하기


메세지 큐의 기본 기능은 아래와 같다.

  • 생산자는 메시지를 메시지 큐에 발행
  • 소비자는 큐를 구독(subscribe)하고 구독한 메시지를 소비
  • 메시지 큐는 생산자와 소비자 사이의 결합을 느슨하게 하는 서비스로, 생산자와 소비자의 독립적인 운영 및 규모 확장을 가능하게 하는 역할 담당
  • 생산자와 소비자는 모두 클라이언트/서버 모델 관점에서 보면 클라이언트고 서버 역할을 하는 것은 메시지 큐이며 이 클라이언트와 서버는 네트워크를 통해 통신

메세지 모델

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

일대일 모델

이 모델은 전통적인 메시지 큐에서 흔히 발견되는 모델이다. 일대일 모델에서 큐에 전송된 메시지는 오직 한 소비자가 가져갈 수 있다. 소비자가 아무리 많아도 각 메시지는 오직 한 소비자만 가져갈 수 있다. 아래 그림을 보면 메세지 A를 가져가는 것은 소비자 1뿐이다.

어떤 소비자가 메시지를 가져갔다는 사실을 큐에 알리면(acknowledge) 해당 메시지는 큐에서 삭제된다. 이 모델은 데이터 보관(data retention)을 지원하지 않는다. 반면 본 설계안은 메시지를 두 주 동안은 보관할 수 있도록 하는 지속성 계층(persistence layer)를 포함하며, 해당 계층을 통해 메시지가 반복적으로 소비될 수 있도록 한다.

비록 본 설계안은 일대일 모델도 지원할 수 있기는 하지만, 그 기능은 발행-구독 모델 쪽에 좀 더 자연스럽게 부합한다.

발행 - 구독 모델

발행-구독 모델을 설명하려면 토픽(topic)이라는 새로운 개념을 도입해야 한다. 토픽은 메시지를 주제별로 정리하는 데 사용된다. 각 토픽은 메시지 큐 서비스 전반에 고유한 이름을 가진다.

메시지를 보내고 받을 때는 토픽에 보내고 받게 된다.

이 모델에서 토픽에 전달된 메시지는 해당 토픽을 구독하는 모든 소비자에 전달된다. 아래 그림을 보면 메시지 A는 소비자 1과 2 모두에 전달된다.

본 설계안이 제시할 분산 메시지 큐는 방금 살펴본 두 가지 모델을 전부 지원 한다. 발행•구독 모델은 토픽을 통해 구현할 수 있고, 일대일 모델은 소비자 그룹(consumer group)을 통해 지원할 수 있다.

토픽, 파티션, 브로커

앞서 언급했듯이 메시지는 토픽에 보관된다. 토픽에 보관되는 데이터의 양이 커져서 서버 한 대로 감당하기 힘든 상황이 벌어지면 어떻게 될까?

이 문제를 해결하는 한 가지 방법은 파티션(partition), 즉 샤딩(sharding) 기법을 활용하는 것이다. 아래 그림 같이, 토픽을 여러 파티션으로 분할한 다음에 메시지를 모든 파티션에 균등하게 나눠 보낸다. 파티션은 토픽에 보낼 메시지의 작은 부분집합으로 생각하면 좋다. 파티션은 메시지 큐 클러스터 내의 서버에 고르게 분산 배치한다. 파티션을 유지하는 서버는 보통 브로커(Broker)라 부른다. 파티션을 브로커에 분산하는 것이 높은 규모 확장성을 달성하는 비결이다. 토픽의 용량을 확장하고 싶으면 파티션 개수를 늘리면 되기 때문이다.

각 토픽 파티션은 FIFO(first in, finst Out) 큐처럼 동작한다. 같은 파티션 안에서는 메시지 순서가 유지된다는 뜻이다. 파티션 내에서의 메시지 위치는 오프셋(offset)이라고 한다.

생산자가 보낸 메시지는 해당 토픽의 파티션 가운데 하나로 보내진다. 메시지에는 사용자 ID 같은 키를 붙일 수 있는데, 같은 키를 가진 모든 메시지는 같은 파티션으로 보내진다. 키가 없는 메시지는 무작위로 선택된 파티션으로 전송된다.

토픽을 구독하는 소비자는 하나 이상의 파티션에서 데이터를 가져오게 된다. 토픽을 구독하는 소비자가 어럿인 경우, 각 구독자는 해당 토픽을 구성하는 파티션의 일부를 담당하게 된다. 이 소비자들을 해당 토픽의 소비자 그룹 (consumer group)이라 부른다.

소비자 그룹

앞서 언급한 대로 본 설계안은 일대일 모델과 발행-구독 모델을 전부 지원해야 한다. 소비자 그룹 내 소비자는 토픽에서 메시지를 소비하기 위해 서로 협력 한다.

하나의 소비자 그룹은 여러 토픽을 구독할 수 있고 오프셋을 별도로 관리한다. 예를 들어, 큐 용레에 따라 과금(billing)용 그룹, 회계(accounting)용 그룹 등으로 나눌 수 있을 것이다.

같은 그룹 내의 소비자는 메시지를 병렬로 소비할 수 있다.

  • 소비자 그룹 1은 토픽 A를 구독한다.
  • 소비자 그룹 2는 토픽 A와 토픽 B를 구독한다.
  • 토픽 A는 그룹-1과 그룹-2가 구독하므로, 해당 토픽 내 메시지는 그룹-1과 그룹-2 내의 소비자에게 전달된다. 따라서 발행-구독 모델을 지원한다.

🚨 하지만 문제가 하나 있다. 데이터를 병렬로 읽으면 대역폭(throughput) 측면에서는 좋지만 같은 파티션 안에 있는 메시지를 순서대로 소비할 수는 없다. 가령 소비자-1과 소비자-2가 같은 파티션-1의 메시지를 읽어야 한다고 하자. 파티션-1 내의 메시지 소비 순서를 보장할 수 없게 된다.

한 가지 제약사항을 추가하면 이 문제는 해결할 수 있다. 즉, 어떤 파티션의 메시지는 한 그룹 안에서는 오직 한 소비자만 읽을 수 있도록 하는 것이다. 다만 그 경우, 그룹 내 소비자의 수가 구독하는 토픽의 파티션 수보다 크면 어떤 소비자는 해당 토픽에서 데이터를 읽지 못하게 된다. 예를 들어 위의 그림의 그룹-2에 있는 소비자-3은 토픽 B의 메시지를 수신할 수 없다. 같은 그룹 내의 소비자-4가 이미 소비하도록 되어 있기 때문이다.
이 제약사항을 도입한 후에 모든 소비자를 같은 소비자 그룹에 두면 같은 파 티션의 메시지는 오직 한 소비자만 가져갈 수 있으므로 결국 일대일 모델에 수렴하게 된다. 파티션은 가장 작은 저장 단위이므로 미리 충분한 파티션을 할당해 두면 파티션의 수를 동적으로 늘리는 일은 피할 수 있다. 처리 용량을 늘리 려면 그냥 소비자를 더 추가하면 된다.

개략적 설계안

다음 그림은 수정된 개략적 설계안이다.

클라이언트

  • 생산자: 메시지를 특정 토픽으로 보낸다.
  • 소비자 그룹: 토픽을 구독하고 메시지를 소비한다.

핵심 서비스 및 저장소

  • 브로커: 파티션들을 유지한다. 하나의 파티션은 특정 토픽에 대한 메시지의 부분 집합을 유지한다.

  • 저장소

    • 데이터 저장소: 메시지는 파티션 내 데이터 저장소에 보관된다.
    • 상태 저장소: 소비자 상태는 이 저장소에 유지된다.
    • 메타데이터 저장소: 토픽 설정, 토픽 속성(property) 등은 이 저장소에 유지된다.
  • 조정 서비스(coordination service)

    • 서비스 탐색(service cliscovery): 어떤 브로커가 살아 있는지 알려준다.
    • 리더 선출(leader election): 브로커 가운데 하나는 컨트롤러 역할을 담당 해야 하며, 한 클러스터에는 반드시 활성 상태 컨트롤러가 하나 있어야 한다. 이 컨트롤러가 파티션 배치를 책임진다.
    • 아파치 주키퍼(Apache ZooKeeper)나 etcd가 보통 컨트롤러 선출을 담당 하는 컴포넌트로 널리 이용된다.
profile
코딩 해라 스리스리 예스리 얍!

0개의 댓글