•
시스템 설계 면접에서 단골로 출제되는 문제인 분산 메시지 큐 설계는, 현대적 소프트웨어 아키텍처에서의 작고 독립적인 블록들 사이의 통신과 조율을 담당한다.
◦
결합도 완화 : 메시지 큐를 사용하면 컴포넌트 사이의 강한 결합이 사라지므로 각각을 독립적으로 갱신할 수 있다.
◦
규모 확장성 개선 : 메시지 큐에 데이터를 생산하는 생산자와 큐에서 메시지를 소비하는 소비자 시스템 규모를 트래픽 부하에 맞게 독립적으로 늘릴 수 있다.
◦
가용성 개선 : 시스템의 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 상호작용을 이어갈 수 있다.
◦
성능 개선 : 메시지 큐를 사용하면 비동기 통신이 쉽게 가능하다. 생산자는 응답을 기다리지 않고도 메시지를 보낼 수 있고 소비자는 읽을 메시지가 있을 때만 해당 메시지를 소비한다.
•
메시지 큐 vs 이벤트 스트리밍 플랫폼
◦
엄밀히 말해 아파치 카프카나 펄사는 이벤트 스트리밍 플랫폼으로 메시지 큐와 다르나 점차 그 경계가 희미해지고 있다.
•
이번 장에서는 데이터 장기 보관, 메시지 반복 소비 등의 부가 기능을 갖춘 메시지 큐를 설계해볼 것이다.
◦
이런 부가 기능들은 통상적으로는 이벤트 스트리밍 플랫폼에서만 이용 가능하다.
1단계: 문제 이해 및 설계 범위 확정
•
개요
◦
가정에 앞서 생산자는 메시지를 큐에 보내고 소비자는 큐에서 메시지를 꺼낼 수 있으면 된다.
◦
외에도 성능, 메시지 전달 방식, 데이터 보관 기간 등 고려할 사항은 다양하다. 질문을 통해 요구사항을 구체화하고 범위를 좁히도록 하자.
•
요구사항 가정
◦
메시지의 형태와 평균 크기, 텍스트 메시지 혹은 멀티미디어도 지원해야 하는지
◦
메시지는 반복적으로 소비될 수 있어야 하는지
▪
하나의 메시지를 여러 소비자가 수신 가능해야한다던가, 지원하지 않는다던가
◦
메시지는 큐에 전달된 순서대로 소비되어야 하는지
◦
데이터의 지속성이 보장되어야 하는지 보장된다면 기간은 어느정도인지
◦
지원해야 하는 생산자와 소비사 주는 대략 어느정도인지
◦
어떤 메시지 전달 방식을 지원해야 하는지, 최대 한 번, 최소 한 번, 정확히 한 번 중
◦
목표로 삼아야 할 대역폭, 단대단 지연 시간
기능 요구사항
•
생산자는 메시지 큐에 메시지를 보낼 수 있어야 한다
•
소비자는 메시지 큐를 통해 메시지를 수신할 수 있어야 한다
•
메시지는 반복적으로 수신할 수도 있어야 하고 단 한 번만 수신하도록 설정될 수도 있어야 한다.
•
오래된 이력 데이터는 삭제될 수 있다.
•
메시지 크기는 킬로바이트 수준이다.
•
메시지가 생산된 순서대로 소비자에게 전달될 수 있어야 한다.
•
메시지 전달 방식은 최소 한 번, 최대 한 번, 정확히 한 번 가운데 설정할 수 있어야 한다.
비기능 요구사항 및 제약 사항
•
높은 대역폭과 낮은 전송 지연 가운데 하나를 설정으로 선택 가능하게 하는 기능이 필요하다.
•
규모 확장성, 이 시스템은 특성상 분산 시스템일 수 밖에 없다. 메시지 양이 급증해도 처리 가능해야 한다.
•
지속성 및 내구성, 데이터는 디스크에 지속적으로 보관되어야 하며 여러 노드에 복제되어야 한다.
전통적 메시지 큐와 무엇이 다른가?
•
RabbitMQ와 같은 전통적인 메시지 큐는 이벤트 스트리밍 플랫폼처럼 메시지 보관 문제를 중요하게 다루지 안흔다.
◦
즉, 소비자에게 전달되기에 충분한 기간 동안만 메모리에 저장한다.
◦
처리 용량을 넘어서는 경우, 디스크에 보관하기는 하나 이 수준이 이벤트 스트리밍 플랫폼보단 낮다.
•
전통적인 메시지 큐는 전달 순서도 보장하지 않는데, 생산된 순서와 소비된 순서는 다를 수 있다.
2단계 : 개략적 설계안 제시 및 동의 구하기
•
메시지 큐의 기본 기능부터 살펴보자.
◦
생산자는 메시지를 메시지 큐에 발행한다.
◦
소비자는 큐를 구독하여 구독한 메시지를 소비한다.
◦
메시지 큐는 생산자와 소비자 사이의 결합을 느슨히하는 서비스로, 생산자와 소비자의 독립적인 운영 및 규모 확장을 가능하게 하는 역할을 수행한다.
◦
생산자와 소비자는 모두 클라이언트/서버 모델 관점에서 클라이언트고 서버 역할을 하는 것은 메시지 큐다.
메시지 모델
•
가장 널리 쓰이는 메시지 모델은 일대일과 발행-구독 모델이다.
•
일대일 모델
◦
이 모델은 전통적인 메시지 큐에서 흔히 발견되는 모델이다.
◦
일대일 모델에서 큐에 전송된 메시지는 오직 한 소비자만 가져갈 수 있다.
◦
어떤 소비자가 메시지를 가져가는 이벤트가 발생하는 경우, 해당 메시지는 큐에서 삭제된다. 이 모델은 데이터 보관을 지원하지 않는다.
•
발행-구독 모델
◦
토픽은 메시지를 주제별로 정리하는 데 사용되는데, 각 토픽은 메시지 큐 서비스 전반에 고유한 이름을 가진다.
▪
메시지를 보내고 받을 때는 토픽에 주고 받게 된다.
◦
이 모델에서 토픽에 전달된 메시지는 해당 토픽을 구독하는 모든 소비자에게 전달된다.
토픽, 파티션, 브로커
•
토픽에 보관되는 데이터의 양이 너무 커져 서버 한 대로 감당이 어렵다면 어떻게 해야할까?
•
파티션, 즉 샤딩 기법을 사용해 해결해볼 수 있다. 파티션은 메시지 큐 클러스터 내의 서버에 고르게 분산 배치한다.
소비자 그룹
•
소비자 그룹 내 소비자는 토픽에서 메시지를 소비하기 위해 서로 협력한다.
•
하나의 소비자 그룹은 여러 토픽을 구독할 수 있고 오프셋을 별도로 관리한다.
•
이를 통해 메시지를 병렬로 소비할 수 있게되나, 순서를 보장할 수 없다. 이 경우 어떤 파티션의 메시지는 한 그룹 안에서는 오직 한 소비자만 읽을 수 있다는 제약사항을 걸면 방지할 수 있다.
개략적 설계안
•
클라이언트
◦
생산자, 소비자
•
핵심 서비스 및 저장소
◦
브로커
◦
저장소
▪
데이터 저장소, 상태 저장소, 메타데이터 저장소
◦
조정 서비스
▪
서비스 탐색 : 어떤 브로커가 사용 가능한지 알려준다.
▪
리더 선출 : 브로커 가운데 하나는 컨트롤러 역할을 담당해야 하며, 한 클러스터에는 반드시 활성 상태 컨트롤러가 하나 있어야 한다.
3단계: 상세 설계
•
데이터의 장기 보관 요구사항을 만족하면서 높은 대역폭을 제공하기 위해, 다음 세가지를 설계에 적용한다.
◦
디스크 기반 자료 구조를 활용한다.
◦
메시지 구조의 명세를 추상화시켜 별도의 수정없이 처리가 가능하도록 한다.
◦
일괄 처리를 우선하는 시스템을 설계한다. 소규모 입출력이 많을수록 높은 대역폭을 지원하기가 어렵다.
데이터 저장소
•
메시지 큐의 트래픽 패턴은 다음과 같은 특징을 가진다.
◦
읽기와 쓰기가 빈번하게 발생한다.
◦
갱신/삭제 연산은 발생하지 않는다.
◦
순차적인 읽기/쓰기가 대부분이다.
•
선택지
1.
데이터베이스
•
관계형, NoSQL를 데이터 저장소로 고려해볼 수 있다.
•
하지만 읽기 연산과 쓰기 연산이 동시에 대규모로 빈번하게 발생하므로 시스템 병목을 유발할 수 있다.
2.
쓰기 우선 로그
•
WAL은 새로운 항목이 추가되기만 하는 일반 파일이다.
•
WAL에 대한 접근 패턴은 읽기/쓰기 전부 순차적이며 접근 패턴이 순차적이므로 디스크의 랜덤 접근을 줄여 좋은 성능을 제공할 수 있다.
메시지 자료 구조
•
높은 대역폭 달성의 열쇠다. 메시지 자료 구조는 생산자, 메시지 큐, 소비자 간의 계약이자 명세로 값비싼 복사를 방지하도록 추상적으로 설계해야 한다.
•
구성
◦
메시지 키
▪
파티션을 결정할 때 사용된다. 키가 주어지지 않은 메시지는 파티션이 임의로 지정된다.
◦
메시지 값
▪
페이로드는 일반 텍스트일수도 압축된 이진 블록일 수도 있다.
◦
기타 필드
▪
이외에도 토픽, 파티션, 오프셋(파티션 내의 메시지 위치), 타임스탬프, 크기, CRC 등을 가진다.
메시지 큐 일괄 처리 작업 흐름
•
운영체제로 하여금 여러 메시지를 한 번의 네트워크 요청으로 전송할 수 있또록 하여 값비싼 네트워크 왕복 비용을 절감한다.
•
브로커가 여러 메시지를 한 번에 로그에 기록하여 더 큰 규모의 순차 쓰기 연산과 더 큰 규모의 연속된 공간을 점유하게 하여 더 높은 디스크 접근 대역폭을 달성할 수 있게 한다.
소비자 측 작업 흐름
•
푸시 모델
◦
장점
▪
낮은 지연 : 브로커는 메시지를 받는 즉시 소비자에게 보낼 수 있다.
◦
단점
▪
소비자가 메시지를 처리하는 속도가 생산자가 메시지를 만드는 속도보다 느릴 경우, 소비자에게 큰 부하가 걸릴 가능성이 있다.
▪
생산자가 데이터 전송 속도를 좌우하므로, 소비자는 항상 그에 맞는 처리가 가능한 컴퓨팅 자원을 준비해 두어야 한다.
•
풀 모델
◦
장점
▪
메시지를 소비하는 속도는 소비자가 알아서 결정한다. 따라서 어떤 소비자는 메시지를 실시간으로 가져가고 어떤 소비자는 일괄로 가져가는 등의 구성이 가능하다.
▪
메시지를 소비하는 속도가 생산 속도보다 느려지면 소비자를 늘려 해결할 수도 있고 생산 속도를 따라잡을 때까지 기다려도 된다.
▪
일괄 처리에 적합하다.
◦
단점
▪
브로커에 메시지가 없어도 소비자는 계속 소비를 시도한다. 따라서 소비자 측 컴퓨팅 자원이 낭비된다. 이를 극복하기 위해 롱 폴링을 지원한다.
소비자 재조정
•
소비자 재조정이란 어떤 소비자가 어떤 파티션을 책임지는지 다시 정하는 프로세스다.
•
이 절차에서는 코디네이터가 중요한 역할을 하는데, 코디네이터란 소비자 재조정을 위해 소비자들과 통신하는 브로커 노드다.
•
코디네이터는 소비자로부터 오는 박동 메시지를 살피고 각 소비자의 파티션 내 오프셋 정보를 관리한다.
상태 저장소
•
소비자에 대한 파티션의 배치 관계, 각 소비자 그룹이 각 파티션에서 마지막으로 가져간 메시지의 오프셋 등이 저장된다.
•
소비자 상태 정보 데이터가 사용되는 패턴은 다음과 같다.
◦
읽기와 쓰기가 빈번하게 발생하지만 양은 많지 않다.
◦
데이터 갱신은 빈번하게 일어나지만 삭제되는 일은 거의 없다.
◦
읽기와 쓰기 연산은 무작위적 패턴을 보인다.
◦
데이터의 일관성이 중요하다.
•
패턴을 고려하여 도입하기에 적절한 후보군으로는 주키퍼와 같은 키-값 저장소를 고려해볼 수 있다.
메타데이터 저장소
•
토픽 설정이나 속성 정보를 보관한다. 파티션 수, 메시지 보관 기간, 사본 배치 정보 등이 해당한다.
주키퍼
•
주키퍼는 계층적 키-값 저장소 기능을 제공하는 서비스로 보통 동기화, 분산 설정 서비스에 이용된다.
복제
•
데이터 유실을 방지하기 위해 사본 데이터를 이곳 저곳에 뿌려두는 것은 흔히 있는 일이다.
•
이는 사본 분산 계획에 의해 처리되며 자세한 내용은 데이터 중심 애플리케이션 설계의 5장을 참고하자.
규모 확장성
•
브로커
◦
생산자, 소비자의 경우, 각각 새로운 생산자를 추가하거나 재조정 메커니즘을 손보는 정도로 쉽게 달성할 수 있지만 브로커는 얘기가 다르다.
◦
브로커 노드에 장애가 발생하는 경우 어떻게 처리될까?
▪
해당 브로커 노드에 존재하는 사본 데이터는 다른 곳에 복제되고 원본 데이터는 사본 데이터를 가지고 있던 곳이 원본으로 전환될 것이다.
◦
규모 확장성의 경우, 브로커 컨트롤러로 하여금 한시적으로 시스템에 설정된 사본 수보다 더 많은 사본을 허용하도록 하면 브로커를 추가하던 도중 발생할 수 있는 데이터 손실을 방지할 수 있다.
•
파티션
◦
토픽의 규모를 늘리거나 대역폭을 조정하거나, 가용성과 대역폭 사이의 균형을 맞추는 등의 운영상의 이유로 파티션의 수를 조정해야할 수 있다.
◦
파티션 추가는 비교적 자유로우나 파티션 삭제는 절차가 보다 까다롭다.
▪
퇴역된 파티션은 더이상 새로운 메시지를 받지 않고 잠시 유지된다. 해당 파티션의 데이터를 읽고 있는 소비자가 있을 수 있기 때문이다.
▪
해당 유지 기간이 지나고 나면 데이터를 삭제한 후, 저장 공간을 반환한다. 따라서 파티션을 제거하는 결정을 해도 자원이 반환되는 데 시간이 좀 소요된다.
▪
소비자 역시 퇴역된 파티션에서 소비를 진행하다 제거되는 시점이 오면 재조정 작업을 개시해야 한다.
메시지 전달 방식
•
최대 한 번
◦
메시지를 말 그대로 최대 한 번만 전달하는 방식이다. 메시지가 전달 과정에서 소실되더라도 다시 전달되는 일은 없다.
◦
과정
▪
생산자는 토픽에 비동기적으로 메시지를 보내고 수신 응답을 기다리지 않는다. 전송이 실패하더라도 재전송하지 않는다.
▪
소비자는 메시지를 읽고 처리하기 전, 오프셋부터 갱신하는데, 오프셋이 갱신된 후 소비자가 장애로 죽으면 메시지는 다시 소비되지 않는다.
◦
이 전달 방식은 지표 모니터링 등 소량의 데이터 손실 정도는 감수할 수 있는 애플리케이션에 적합하다.
•
최소 한 번
◦
최소 한 번은 같은 메시지가 한 번 이상 전달될 수 있으나 메시지 소실은 발생하지 않음을 보장해야할 때 사용한다.
◦
과정
▪
생산자는 메시지를 동기, 비동기적으로 보낼 수 있으며 ACK=1, ACK=all의 구성을 이용한다. 즉 메시지가 브로커에 전달되었음을 반드시 확인한다.
•
실패하는 경우, 재시도할 것이다.
▪
소비자는 데이터를 성공적으로 처리한 후에만 오프셋을 갱신한다. 메시지 처리가 실패한 경우에는 메시지를 다시 가져오므로 데이터가 손실되는 일은 없다.
▪
따라서 메시지는 브로커나 소비자에게 한 번 이상 전달될 수 있다.
◦
이 방식은 메시지가 소실되는 일은 없지만 중복적으로 처리되는 경우가 발생할 수 있다. 이상적이진 않지만 데이터 중복이 큰 문제가 아닌 애플리케이션이나 소비자가 중복을 직접 처리할 수 있는 애플리케이션에 적합하다.
•
정확히 한 번
◦
지불, 매매, 회계 등 금융 관련에서는 이 전송 방식이 적합하다. 사용자 입장에서는 정말 편리하지만 구현을 위해 필요한 복잡도가 크긴 하다.
고급 기능
•
메시지 필터링
◦
필터링에 사용되는 데이터는 불필요한 오버헤드를 방지하기 위해 메타데이터 영역을 참조할 수 있어야 한다.
◦
메시지마다 태그를 두면 소비자는 어떤 태그를 가진 메시지를 구독할 지 결정할 수 있게 된다.
•
메시지 지연 전송 및 예약 전송
◦
지연, 예약이 필요한 메시지의 경우, 토픽에 바로 저장하지 않고 브로커 내부의 임시 저장소에 넣어 두었다 시간이 되면 토픽으로 옮긴다.
◦
예시
▪
하나 이상의 특별 메시지 토픽을 임시 저장소로 활용한다.
▪
Rocket-MQ나 계층적 타이밍 휠을 사용하여 구현한다.
4단계: 마무리
•
좀 더 생각해볼만한 부분은 다음과 같다.
◦
프로토콜
▪
메시지 생산, 소비, 박동 메시지 교환 등의 모든 활동에 있어서 프로토콜을 최적화시켜볼 수 있다.
▪
대용량 데이터를 효과적으로 전송할 방법을 찾아본다.
▪
데이터의 무결성을 검증할 방법을 찾아본다.
▪
이를 위해 AMQP, 카프카 등을 고려해볼 수 있다.
◦
메시지 소비 재시도
▪
실패하는 경우, 재시도 전용 토픽에 보내 나중에 다시 소비를 시도할 수 있다.