# 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)으로 나뉜다.

* 최대 한 번: 메시지가 전달 과정에서 소실되더라도 다시 전달하지 않음
* 최소 한 번: 메시지가 브로커에게 전달되었음을 반드시 확인하는 방식으로, 메시지 손실되지 않음
* 정확히 한 번: 성능 및 구현 복잡도가 높은 방식으로, 중요한 데이터 전달에 사용

**참고자료**

* [가상 면접 사례로 배우는 대규모 시스템 설계 기초 2](https://kobic.net/book/bookInfo/view.do?isbn=9788966264254)
