////
Search
🪀

4장. 분산 메시지 큐

시스템 설계 면접에서 단골로 출제되는 문제인 분산 메시지 큐 설계는, 현대적 소프트웨어 아키텍처에서의 작고 독립적인 블록들 사이의 통신과 조율을 담당한다.
결합도 완화 : 메시지 큐를 사용하면 컴포넌트 사이의 강한 결합이 사라지므로 각각을 독립적으로 갱신할 수 있다.
규모 확장성 개선 : 메시지 큐에 데이터를 생산하는 생산자와 큐에서 메시지를 소비하는 소비자 시스템 규모를 트래픽 부하에 맞게 독립적으로 늘릴 수 있다.
가용성 개선 : 시스템의 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 상호작용을 이어갈 수 있다.
성능 개선 : 메시지 큐를 사용하면 비동기 통신이 쉽게 가능하다. 생산자는 응답을 기다리지 않고도 메시지를 보낼 수 있고 소비자는 읽을 메시지가 있을 때만 해당 메시지를 소비한다.
메시지 큐 vs 이벤트 스트리밍 플랫폼
엄밀히 말해 아파치 카프카나 펄사는 이벤트 스트리밍 플랫폼으로 메시지 큐와 다르나 점차 그 경계가 희미해지고 있다.
이번 장에서는 데이터 장기 보관, 메시지 반복 소비 등의 부가 기능을 갖춘 메시지 큐를 설계해볼 것이다.
이런 부가 기능들은 통상적으로는 이벤트 스트리밍 플랫폼에서만 이용 가능하다.

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

개요
가정에 앞서 생산자는 메시지를 큐에 보내고 소비자는 큐에서 메시지를 꺼낼 수 있으면 된다.
외에도 성능, 메시지 전달 방식, 데이터 보관 기간 등 고려할 사항은 다양하다. 질문을 통해 요구사항을 구체화하고 범위를 좁히도록 하자.
요구사항 가정
메시지의 형태와 평균 크기, 텍스트 메시지 혹은 멀티미디어도 지원해야 하는지
메시지는 반복적으로 소비될 수 있어야 하는지
하나의 메시지를 여러 소비자가 수신 가능해야한다던가, 지원하지 않는다던가
메시지는 큐에 전달된 순서대로 소비되어야 하는지
데이터의 지속성이 보장되어야 하는지 보장된다면 기간은 어느정도인지
지원해야 하는 생산자와 소비사 주는 대략 어느정도인지
어떤 메시지 전달 방식을 지원해야 하는지, 최대 한 번, 최소 한 번, 정확히 한 번 중
목표로 삼아야 할 대역폭, 단대단 지연 시간

기능 요구사항

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

비기능 요구사항 및 제약 사항

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

전통적 메시지 큐와 무엇이 다른가?

RabbitMQ와 같은 전통적인 메시지 큐는 이벤트 스트리밍 플랫폼처럼 메시지 보관 문제를 중요하게 다루지 안흔다.
즉, 소비자에게 전달되기에 충분한 기간 동안만 메모리에 저장한다.
처리 용량을 넘어서는 경우, 디스크에 보관하기는 하나 이 수준이 이벤트 스트리밍 플랫폼보단 낮다.
전통적인 메시지 큐는 전달 순서도 보장하지 않는데, 생산된 순서와 소비된 순서는 다를 수 있다.

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

메시지 큐의 기본 기능부터 살펴보자.
생산자는 메시지를 메시지 큐에 발행한다.
소비자는 큐를 구독하여 구독한 메시지를 소비한다.
메시지 큐는 생산자와 소비자 사이의 결합을 느슨히하는 서비스로, 생산자와 소비자의 독립적인 운영 및 규모 확장을 가능하게 하는 역할을 수행한다.
생산자와 소비자는 모두 클라이언트/서버 모델 관점에서 클라이언트고 서버 역할을 하는 것은 메시지 큐다.

메시지 모델

가장 널리 쓰이는 메시지 모델은 일대일과 발행-구독 모델이다.
일대일 모델
이 모델은 전통적인 메시지 큐에서 흔히 발견되는 모델이다.
일대일 모델에서 큐에 전송된 메시지는 오직 한 소비자만 가져갈 수 있다.
어떤 소비자가 메시지를 가져가는 이벤트가 발생하는 경우, 해당 메시지는 큐에서 삭제된다. 이 모델은 데이터 보관을 지원하지 않는다.
발행-구독 모델
토픽은 메시지를 주제별로 정리하는 데 사용되는데, 각 토픽은 메시지 큐 서비스 전반에 고유한 이름을 가진다.
메시지를 보내고 받을 때는 토픽에 주고 받게 된다.
이 모델에서 토픽에 전달된 메시지는 해당 토픽을 구독하는 모든 소비자에게 전달된다.

토픽, 파티션, 브로커

토픽에 보관되는 데이터의 양이 너무 커져 서버 한 대로 감당이 어렵다면 어떻게 해야할까?
파티션, 즉 샤딩 기법을 사용해 해결해볼 수 있다. 파티션은 메시지 큐 클러스터 내의 서버에 고르게 분산 배치한다.

소비자 그룹

소비자 그룹 내 소비자는 토픽에서 메시지를 소비하기 위해 서로 협력한다.
하나의 소비자 그룹은 여러 토픽을 구독할 수 있고 오프셋을 별도로 관리한다.
이를 통해 메시지를 병렬로 소비할 수 있게되나, 순서를 보장할 수 없다. 이 경우 어떤 파티션의 메시지는 한 그룹 안에서는 오직 한 소비자만 읽을 수 있다는 제약사항을 걸면 방지할 수 있다.

개략적 설계안

클라이언트
생산자, 소비자
핵심 서비스 및 저장소
브로커
저장소
데이터 저장소, 상태 저장소, 메타데이터 저장소
조정 서비스
서비스 탐색 : 어떤 브로커가 사용 가능한지 알려준다.
리더 선출 : 브로커 가운데 하나는 컨트롤러 역할을 담당해야 하며, 한 클러스터에는 반드시 활성 상태 컨트롤러가 하나 있어야 한다.

3단계: 상세 설계

데이터의 장기 보관 요구사항을 만족하면서 높은 대역폭을 제공하기 위해, 다음 세가지를 설계에 적용한다.
디스크 기반 자료 구조를 활용한다.
메시지 구조의 명세를 추상화시켜 별도의 수정없이 처리가 가능하도록 한다.
일괄 처리를 우선하는 시스템을 설계한다. 소규모 입출력이 많을수록 높은 대역폭을 지원하기가 어렵다.

데이터 저장소

메시지 큐의 트래픽 패턴은 다음과 같은 특징을 가진다.
읽기와 쓰기가 빈번하게 발생한다.
갱신/삭제 연산은 발생하지 않는다.
순차적인 읽기/쓰기가 대부분이다.
선택지
1.
데이터베이스
관계형, NoSQL를 데이터 저장소로 고려해볼 수 있다.
하지만 읽기 연산과 쓰기 연산이 동시에 대규모로 빈번하게 발생하므로 시스템 병목을 유발할 수 있다.
2.
쓰기 우선 로그
WAL은 새로운 항목이 추가되기만 하는 일반 파일이다.
WAL에 대한 접근 패턴은 읽기/쓰기 전부 순차적이며 접근 패턴이 순차적이므로 디스크의 랜덤 접근을 줄여 좋은 성능을 제공할 수 있다.

메시지 자료 구조

높은 대역폭 달성의 열쇠다. 메시지 자료 구조는 생산자, 메시지 큐, 소비자 간의 계약이자 명세로 값비싼 복사를 방지하도록 추상적으로 설계해야 한다.
구성
메시지 키
파티션을 결정할 때 사용된다. 키가 주어지지 않은 메시지는 파티션이 임의로 지정된다.
메시지 값
페이로드는 일반 텍스트일수도 압축된 이진 블록일 수도 있다.
기타 필드
이외에도 토픽, 파티션, 오프셋(파티션 내의 메시지 위치), 타임스탬프, 크기, CRC 등을 가진다.

메시지 큐 일괄 처리 작업 흐름

운영체제로 하여금 여러 메시지를 한 번의 네트워크 요청으로 전송할 수 있또록 하여 값비싼 네트워크 왕복 비용을 절감한다.
브로커가 여러 메시지를 한 번에 로그에 기록하여 더 큰 규모의 순차 쓰기 연산과 더 큰 규모의 연속된 공간을 점유하게 하여 더 높은 디스크 접근 대역폭을 달성할 수 있게 한다.

소비자 측 작업 흐름

푸시 모델
장점
낮은 지연 : 브로커는 메시지를 받는 즉시 소비자에게 보낼 수 있다.
단점
소비자가 메시지를 처리하는 속도가 생산자가 메시지를 만드는 속도보다 느릴 경우, 소비자에게 큰 부하가 걸릴 가능성이 있다.
생산자가 데이터 전송 속도를 좌우하므로, 소비자는 항상 그에 맞는 처리가 가능한 컴퓨팅 자원을 준비해 두어야 한다.
풀 모델
장점
메시지를 소비하는 속도는 소비자가 알아서 결정한다. 따라서 어떤 소비자는 메시지를 실시간으로 가져가고 어떤 소비자는 일괄로 가져가는 등의 구성이 가능하다.
메시지를 소비하는 속도가 생산 속도보다 느려지면 소비자를 늘려 해결할 수도 있고 생산 속도를 따라잡을 때까지 기다려도 된다.
일괄 처리에 적합하다.
단점
브로커에 메시지가 없어도 소비자는 계속 소비를 시도한다. 따라서 소비자 측 컴퓨팅 자원이 낭비된다. 이를 극복하기 위해 롱 폴링을 지원한다.

소비자 재조정

소비자 재조정이란 어떤 소비자가 어떤 파티션을 책임지는지 다시 정하는 프로세스다.
이 절차에서는 코디네이터가 중요한 역할을 하는데, 코디네이터란 소비자 재조정을 위해 소비자들과 통신하는 브로커 노드다.
코디네이터는 소비자로부터 오는 박동 메시지를 살피고 각 소비자의 파티션 내 오프셋 정보를 관리한다.

상태 저장소

소비자에 대한 파티션의 배치 관계, 각 소비자 그룹이 각 파티션에서 마지막으로 가져간 메시지의 오프셋 등이 저장된다.
소비자 상태 정보 데이터가 사용되는 패턴은 다음과 같다.
읽기와 쓰기가 빈번하게 발생하지만 양은 많지 않다.
데이터 갱신은 빈번하게 일어나지만 삭제되는 일은 거의 없다.
읽기와 쓰기 연산은 무작위적 패턴을 보인다.
데이터의 일관성이 중요하다.
패턴을 고려하여 도입하기에 적절한 후보군으로는 주키퍼와 같은 키-값 저장소를 고려해볼 수 있다.

메타데이터 저장소

토픽 설정이나 속성 정보를 보관한다. 파티션 수, 메시지 보관 기간, 사본 배치 정보 등이 해당한다.

주키퍼

주키퍼는 계층적 키-값 저장소 기능을 제공하는 서비스로 보통 동기화, 분산 설정 서비스에 이용된다.

복제

데이터 유실을 방지하기 위해 사본 데이터를 이곳 저곳에 뿌려두는 것은 흔히 있는 일이다.
이는 사본 분산 계획에 의해 처리되며 자세한 내용은 데이터 중심 애플리케이션 설계의 5장을 참고하자.

규모 확장성

브로커
생산자, 소비자의 경우, 각각 새로운 생산자를 추가하거나 재조정 메커니즘을 손보는 정도로 쉽게 달성할 수 있지만 브로커는 얘기가 다르다.
브로커 노드에 장애가 발생하는 경우 어떻게 처리될까?
해당 브로커 노드에 존재하는 사본 데이터는 다른 곳에 복제되고 원본 데이터는 사본 데이터를 가지고 있던 곳이 원본으로 전환될 것이다.
규모 확장성의 경우, 브로커 컨트롤러로 하여금 한시적으로 시스템에 설정된 사본 수보다 더 많은 사본을 허용하도록 하면 브로커를 추가하던 도중 발생할 수 있는 데이터 손실을 방지할 수 있다.
파티션
토픽의 규모를 늘리거나 대역폭을 조정하거나, 가용성과 대역폭 사이의 균형을 맞추는 등의 운영상의 이유로 파티션의 수를 조정해야할 수 있다.
파티션 추가는 비교적 자유로우나 파티션 삭제는 절차가 보다 까다롭다.
퇴역된 파티션은 더이상 새로운 메시지를 받지 않고 잠시 유지된다. 해당 파티션의 데이터를 읽고 있는 소비자가 있을 수 있기 때문이다.
해당 유지 기간이 지나고 나면 데이터를 삭제한 후, 저장 공간을 반환한다. 따라서 파티션을 제거하는 결정을 해도 자원이 반환되는 데 시간이 좀 소요된다.
소비자 역시 퇴역된 파티션에서 소비를 진행하다 제거되는 시점이 오면 재조정 작업을 개시해야 한다.

메시지 전달 방식

최대 한 번
메시지를 말 그대로 최대 한 번만 전달하는 방식이다. 메시지가 전달 과정에서 소실되더라도 다시 전달되는 일은 없다.
과정
생산자는 토픽에 비동기적으로 메시지를 보내고 수신 응답을 기다리지 않는다. 전송이 실패하더라도 재전송하지 않는다.
소비자는 메시지를 읽고 처리하기 전, 오프셋부터 갱신하는데, 오프셋이 갱신된 후 소비자가 장애로 죽으면 메시지는 다시 소비되지 않는다.
이 전달 방식은 지표 모니터링 등 소량의 데이터 손실 정도는 감수할 수 있는 애플리케이션에 적합하다.
최소 한 번
최소 한 번은 같은 메시지가 한 번 이상 전달될 수 있으나 메시지 소실은 발생하지 않음을 보장해야할 때 사용한다.
과정
생산자는 메시지를 동기, 비동기적으로 보낼 수 있으며 ACK=1, ACK=all의 구성을 이용한다. 즉 메시지가 브로커에 전달되었음을 반드시 확인한다.
실패하는 경우, 재시도할 것이다.
소비자는 데이터를 성공적으로 처리한 후에만 오프셋을 갱신한다. 메시지 처리가 실패한 경우에는 메시지를 다시 가져오므로 데이터가 손실되는 일은 없다.
따라서 메시지는 브로커나 소비자에게 한 번 이상 전달될 수 있다.
이 방식은 메시지가 소실되는 일은 없지만 중복적으로 처리되는 경우가 발생할 수 있다. 이상적이진 않지만 데이터 중복이 큰 문제가 아닌 애플리케이션이나 소비자가 중복을 직접 처리할 수 있는 애플리케이션에 적합하다.
정확히 한 번
지불, 매매, 회계 등 금융 관련에서는 이 전송 방식이 적합하다. 사용자 입장에서는 정말 편리하지만 구현을 위해 필요한 복잡도가 크긴 하다.

고급 기능

메시지 필터링
필터링에 사용되는 데이터는 불필요한 오버헤드를 방지하기 위해 메타데이터 영역을 참조할 수 있어야 한다.
메시지마다 태그를 두면 소비자는 어떤 태그를 가진 메시지를 구독할 지 결정할 수 있게 된다.
메시지 지연 전송 및 예약 전송
지연, 예약이 필요한 메시지의 경우, 토픽에 바로 저장하지 않고 브로커 내부의 임시 저장소에 넣어 두었다 시간이 되면 토픽으로 옮긴다.
예시
하나 이상의 특별 메시지 토픽을 임시 저장소로 활용한다.
Rocket-MQ나 계층적 타이밍 휠을 사용하여 구현한다.

4단계: 마무리

좀 더 생각해볼만한 부분은 다음과 같다.
프로토콜
메시지 생산, 소비, 박동 메시지 교환 등의 모든 활동에 있어서 프로토콜을 최적화시켜볼 수 있다.
대용량 데이터를 효과적으로 전송할 방법을 찾아본다.
데이터의 무결성을 검증할 방법을 찾아본다.
이를 위해 AMQP, 카프카 등을 고려해볼 수 있다.
메시지 소비 재시도
실패하는 경우, 재시도 전용 토픽에 보내 나중에 다시 소비를 시도할 수 있다.