Design Distributed Message Queue
현대 소프트웨어 아키텍처는 작고 독립적인 서비스로 구성되며, 메시지 큐는 서비스 사이의 통신과 조율을 담당하게 되면서 다음과 같은 이점을 제공한다.
- 결합도 완화(decoupling): 컴포넌트 사이 강한 결합을 제거하고, 각 컴포넌트들을 독립적으로 갱신 가능
- 규모 확장성 개선: 메시지 큐에 데이터를 생산하는 생산자와 큐에서 메시지를 소비하는 소비자 시스템 규모를 트래픽 부하에 맞게 조정 가능
- 가용성 개선: 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 통신 가능
- 성능 개선: 생산자는 응답을 기다리지 않고 메시지를 전송 할 수 있고, 소비자는 메시지가 있을 때만 처리하게 되어 비동기 통신을 원활하게 함
- 메시지 형태: 텍스트
- 메시지 평균 크기: 수 KB
- 하나의 메시지가 하나의 소비자 / 여러 소비자에게 전달 설정 가능
- 생산된 순서대로 소비
- 데이터 지속성 2주 보장
- 메시지 전달 방식 최소 한 번(at-least-once) / 최대 한 번(at-most-once) / 정확히 한 번(exactly-once) 설정 가능
메시지 모델
Section titled “메시지 모델”가장 널리 쓰이는 메시지 모델은 일대일(point-to-point)과 발행-구독(publish-subscribe)이 존재한다.
- 일대일 모델
- 각 메시지는 오직 한 소비자에게만 전달
- 어떤 소비자가 메시지를 가져갔다는 사실을 큐에 알리면(acknowledge) 해당 메시지는 큐에서 삭제
- 큐에 저장됐던 데이터 보관을 지원하지 않음
- 발행-구독 모델
- 해당 토픽을 구독하는 모든 소비자에게 전달
- 메시지를 주고 받을 때 토픽에 보내고 받는 방식(토픽 = 메시지의 주제 개념)
토픽에 데이터가 부족한 경우
Section titled “토픽에 데이터가 부족한 경우”발행-구독 모델은 메시지가 토픽에 저장되는데, 보관되는 데이터의 양이 커지게 되면 파티션을 나누어 해결할 수 있다.
- 하나의 토픽을 여러 파티션으로 분할
- 메시지를 모든 파티션에 균등하게 나누어 전송
트래픽이나 데이터 양이 많아질 수록 파티션이 증가하게 되는데, 파티션을 유지하는 서버를 보통 브로커라고 부른다.
개략적 설계안
Section titled “개략적 설계안” 메타데이터 저장소 조정 서비스 ↕ ↕생산자 -> 브로커 (데이터 저장소, 상태 저장소) -> 소비자 (소비자 그룹)- 생산자: 메시지를 특정 토픽으로 전송
- 소비자 그룹: 토픽을 구독하고 메시지 소비
- 브로커: 파티션 유지
- 데이터 저장소: 메시지를 파티션 내 데이터 저장소에 보관
- 상태 저장소: 소비자의 상태 보관
- 메타데이터 저장소: 토픽 설정 / 토픽 속성 등 저장
- 조정 서비스
- 서비스 탐색: 어떤 브로커가 살아있는지 감지
- 리더 선출: 브로커 가운데 하나를 컨트롤러 역할로 선출(한 클러스터에는 하나 이상의 컨트롤러가 필요)
데이터 저장소
Section titled “데이터 저장소”메시지 큐의 트래픽 패턴은 다음과 같다.
- 읽기와 쓰기 빈번
- 순차적 읽기/쓰기가 대부분
- 갱신/삭제 연산 발생 X
생각해 볼 수 있는 선택지로는 관계형/비관계형 데이터베이스가 존재하지만, 읽기/쓰기가 대규모로 빈번하게 발생하기 때문에 적합하지 않다.
쓰기 우선 로그(Write-Ahead Log, WAL)
Section titled “쓰기 우선 로그(Write-Ahead Log, WAL)”WAL은 새로운 항목이 추가되기만 하는 일반 파일로, 메시지 큐에 적합한 데이터 저장소로 사용할 수 있다.
- 새로운 메시지가 파티션 꼬리 부분에 추가되는 방식
- 접근 패턴이 순차적이기 때문에 디스크 I/O 최소화
- 순차 접근이기 때문에 회전식 디스크 환경에서도 빠른 데이터 접근 가능
메시지 자료 구조
Section titled “메시지 자료 구조”메시지 구조는 생산자와 메시지 큐, 소비자 사이의 계약이라고 볼 수 있다.
| 필드 | 데이터 자료형 | 설명 |
|---|---|---|
| key | byte[] | 파티션을 정하는 키 |
| value | byte[] | 메시지의 내용(=payload) |
| topic | string | 메시지가 속한 토픽 |
| partition | integer | 메시지가 속한 파티션 |
| offset | long | 파티션 내 메시지의 위치 |
| timestamp | long | 메시지 생성 시간 |
| size | integer | 메시지 크기 |
| crc | integer | 순환 중복 검사의 약자로, 데이터 무결성 보장에 사용 |
생산자 / 소비자 / 메시지 큐는 메시지를 가급적 일괄 처리하게 되는데, 일괄 처리는 시스템 성능에 많은 영향을 미친다.
- 한 번의 네트워크 요청으로 처리하여 네트워크 왕복 비용 완화
- 여러 메시지가 한 번에 로그에 기록되면, 큰 규모의 순차 쓰기 연산이 발생하여 디스크에 연속된 공간으로 기록됨(대역폭 상승)
하지만 한 번에 많은 양을 처리할수록, 메시지 큐의 지연 시간이 증가하게 되기 때문에 적절한 균형을 찾아야 한다.
푸시 vs 풀
Section titled “푸시 vs 풀”메시지 큐는 푸시(push)와 풀(pull) 방식으로 메시지를 소비할 수 있다.
- 푸시 모델: 브로커가 소비자에게 메시지를 전달하는 방식
- 즉시 소비자에게 전달하여 지연 시간 감소
- 소비자 메시지 처리 속도가 생산자 생성 속도보다 느린 경우 높은 부하 가능성 존재
- 생산자 생성 속도에 맞춰 소비자의 컴퓨팅 자원을 준비해 두어야 함
- 풀 모델: 소비자가 메세지를 가져가는 방식
- 메시지 소비 속도를 알아서 결정하여 실시간 / 일괄 처리 선택 가능
- 소비 속도가 느리더라도 부하가 생기지 않음
- 쌓인 모든 메시지를 한 번에 가져가 일괄 처리 가능
- 브로커에 메시지가 없어도 불필요한 풀링 요청으로 자원 낭비 가능성 존재(롱 풀링으로 문제 완화)
메시지 전달 방식
Section titled “메시지 전달 방식”메시지 전달 방식은 최소 한 번(at-least-once) / 최대 한 번(at-most-once) / 정확히 한 번(exactly-once)으로 나뉜다.
- 최대 한 번: 메시지가 전달 과정에서 소실되더라도 다시 전달하지 않음
- 최소 한 번: 메시지가 브로커에게 전달되었음을 반드시 확인하는 방식으로, 메시지 손실되지 않음
- 정확히 한 번: 성능 및 구현 복잡도가 높은 방식으로, 중요한 데이터 전달에 사용