Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 290 additions & 0 deletions Gimini-3/Week02/Chapter04.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
# 4장 분산 메시지 큐

### 메시지 큐 이점

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

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

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

생산자는 메시지를 큐에 보내고, 소비자는 큐에서 메시지를 꺼낼 수 있으면 된다.

## 기능 요구사항

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

## 비기능 요구사항

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

### 전통적 메시지 큐와 다른 점

전통적인 큐는 메시지가 소비자에 전달되기 충분한 기간 동암난 메모리에 보관한다.

- 처리 용량을 넘어선 메시지는 디스크에 보관하긴 하는데 이벤트 스트리밍 플랫폼이 감당하는 용량보다는 아주 낮은 수준이다.

전통적인 메시지 큐는 메시지 전달 순서도 보존하지 않는다.

- 생산된 순서와 소비되는 순서는 다를 수 있다.

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

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

## 메시지 모델

### 일대일 모델

일대일 모델에서 큐에 전송된 메시지는 오직 한 소비자만 가져갈 수 있다.

소비자가 아무리 많아도 각 메시지는 오직 한 소비자만 가져갈 수 있다.

- 어떤 소비자가 메시지를 가져갔다는 사실을 큐에 알리면 해당 메시지는 큐에서 삭제된다.
- 이 모델은 데이터 보관을 지원하지 않는다.

본 설계안은 두 주 동안은 보관할 수 있도록 하는 지속성 계층를 포함

- 해당 계층을 통해 메시지가 반복적으로 소비될 수 있도록 한다.

### 발행-구독 모델

토픽은 메시지를 주제별로 정리하는 데 사용된다.

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

이 모델에서 토픽에 전달된 메시지는 해당 토픽을 구독하는 모든 소비자에 전달된다.

## 토픽, 파티션, 브로커

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

이 문제를 해결하는 방법은 파티션, 즉 샤딩 기법을 활용하는 것이다.

토픽을 여러 파티션으로 분할한 다음에 메시지를 모든 파티션에 균등하게 나눠 보낸다.

- 파티션은 토픽에 보낼 메시지의 작은 부분집합이라고 생각하면 좋다.
- 파티션은 메시지 큐 클러스터 내의 서버에 고르게 분산 배치한다.

파티션을 유지하는 서버는 보통 브로커라 부른다.

- 파티션을 브로커에 분산하는 것이 높은 규모 확장성을 달성하는 비결이다.
- 토픽의 용량을 확장하고 싶으면 파티션 개수를 늘리면 되기 때문이다.

각 토픽 파티션은 FIFO 큐처럼 동작한다.

같은 파티션 안에서는 메시지 순서가 유지된다.

파티션 내에서의 위치는 오프셋이라고 한다.

생산자가 보낸 메시지는 해당 토픽의 파티션 가운데 하나로 보내진다.

메시지에는 사용자 ID 같은 키를 붙일 수 있는데, 같은 키를 가진 모든 메시지는 같은 파티션으로 보내진다.

키가 없는 메시지는 무작위로 선택된 파티션으로 전송된다.

토픽을 구독하는 소비자는 하나 이상의 파티션에서 데이터를 가져오게 된다.

토픽을 구독하는 소비자가 여럿인 경우, 각 구독자는 해당 토픽을 구성하는 파티션의 일부를 담당하게 된다.

## 소비자 그룹

소비자 그룹 내 소비자는 토픽에서 메시지를 소비하기 위해 서로 협력해야 한다.

하나의 소비자 그룹은 여러 토픽을 구독할 수 있고 오프셋을 별도로 관리한다.

- 예를 들어 큐 용례에 따라 과금용 그룹, 회계용 그룹 등으로 나눌 수 있을 것이다.

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

- 데이터를 병렬로 읽으면 대역폭 측면에서는 좋지만 같은 파티션 안에 있는 메시지를 순서대로 소비할 수는 없다.

### 개략적 설계안

클라이언트

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

핵심 서비스 및 저장소

- 브로커: 파티션들을 유지한다. 하나의 파티션은 특정 토픽에 대한 메시지의 부분 집합을 유지한다.
- 저장소
- 데이터 저장소: 메시지는 파티션 내 데이터 저장소에 보관한다.
- 상태 저장소: 소비자 상태는 이 저장소에 유지된다.
- 메타데이터 저장소: 토픽 설정, 토픽 속성 등은 이 저장소에 유지된다.
- 조정 서비스
- 서비스 탐색: 어떤 브로커가 살아있는지 알려준다.
- 리더 선출: 브로커 가운데 하나는 컨트롤러 역할을 담당해야 하며,한 클러스터에는 반드시 활성 상태 컨트롤러가 하나 있어야 한다. 이 컨트롤러가 파티션 배치를 책임진다.
- 아파치 주키퍼나 etcd가 보통 컨트롤러 선출을 담당하는 컴포넌트로 널리 이용된다.

# 3단계: 상세 설계

- 회전 디스크의 높은 순차 탐색 성능과 현대적 운영체제가 제공하는 적극적 디스크 캐시 전략을 잘 이용하는 디스크 기반 자료 구조를 활용
- 메시지가 생산자로부터 소비자에게 전달되는 순간까지 아무 수정 없이도 전송이 가능하도록 하는 메시지 자료 구조를 설계하고 활용할 것
- 전송 데이터의 양이 막대한 경우에 메시지 복사에 드는 비용을 최소화하기 위함이다.
- 일괄 처리를 우선하는 시스템을 설계할 것
- 소규모의 I/O가 많으면 높은 대역폭을 지원하기 어렵다.
- 생산자는 메시지를 일괄 전송하고, 메시지 큐는 그 메시지들을 더 큰 단위로 묶어 보관한다.
- 소비자도 가능하면 메시지를 일괄 수신하도록 한다.

## 데이터 저장소

- 읽기와 쓰기가 빈번하게 일어난다.
- 갱신/삭제 연산은 발생하지 않는다.
- 순차적인 읽기/쓰기가 대부분이다.

### 선택지1: 데이터베이스

- 관계형 데이터베이스: 토픽별로 테이블을 만든다. 토픽에 보내는 메시지는 해당 테이블에 새로운 레코드로 추가한다.
- NoSQL 데이터베이스: 토픽별로 컬렉션을 만든다. 토픽에 보내는 메시지는 하나의 문서가 된다.

### 선택지2: 쓰기 우선 로그(Write-Ahead Log, WAL)

두 번째 선택지는 쓰기 우선 로그, 즉 WAL이다.

WAL은 새로운 항목이 추가되기만 하는 일반 파일이다.

WAL은 다양한 시스템에서 사용되는 기술인데, MySQL의 복구 로그가 WAL로 구현되어 있고 아파치 추키퍼도 해당 기술을 활용한다.

지속성을 보장해야 하는 메시지는 디스크에 WAL로 보관할 것을 추천한다.

WAL에 대한 접근 패턴은 읽기/쓰기 전부 순차적이다.

접근 패턴이 순차적일 때 디스크는 아주 좋은 성능을 보인다.

새로운 메시지는 파티션 꼬리 부분에 추가되며, 오프셋은 그 결과를 점진적으로 증가한다.

가장 쉬운 방법은 로그 파일 줄 번호를 오프셋으로 사용하는 것이다.

하지만 파일의 크기도 무한정 커질수는 없으니, 세그먼트 단위로 나누는 것이 바람직하다.

세그먼트를 사용하는 경우 새 메시지는 활성 상태의 세그먼트 파일에만 추가된다.

해당 세그먼트의 크기가 일정 한계에 도달하면 새 활성 세그먼트 파일이 만들어져 새로운 메시지를 수용하고, 종전까지 활성 상태였던 세그먼트 파일은 다른 나머지 세그먼트 파일과 마찬가지로 비활성 상태로 바뀐다.

비활성 세그먼트는 읽기 요청만 처리한다.

낡은 비활성 세그먼트 파일은 보관 기한이 만료되거나 용량 한계에 도달하면 삭제해 버릴 수 있다.

### 디스크 성능 관련 유의사항

회전식 디스크가 느리다는 것은 널리 퍼진 편견이다.

회전식 디스크가 정말로 느려지는 것은 데이터 접근 패턴이 무작위일 때다.

순차적 데이터 접근 패턴을 적극 활용하는 디스크 기반 자료 구조를 사용하면, RAID로 구성된 현대적 디스크 드라이브에서 수백 MB/sec 수준의 읽기/쓰기 성능을 달성하는 것은 어렵지 않다.

## 메시지 자료 구조

메시지 구조는 높은 대역폭 달성의 열쇠다.

메시지 자료 구조는 생산자, 메시지 큐, 그리고 소비자 사이의 계약이다.

메시지가 큐를 거쳐 소비자에게 전달되는 과정에서 불필요한 복사가 일어나지 않도록 함으로써 높은 대역폭을 달성할 수 있다.

## 규모 확장성

### 생산자

생산자의 규모 확장성은 새로운 생산자를 추가하거나 삭제함으로써 쉽게 달성할 수 있다.

### 소비자

소비자 그룹은 서로 독립적이므로 새 소비자 그룹은 쉽게 추가하고 삭제할 수 있다.

같은 소비자 그룹 내의 소비자가 새로 추가/삭제되거나 장애로 제거되어야 하는 경우는 재조정 메커니즘이 맡아 처리한다.

### 브로커

브로커는 노드의 장애가 발생해 브로커가 죽었을 때 주변 브로커들이 감지를 하여 남은 브로커 노드를 위해 새로운 파티션 분산 계획을 만든다.

- 파티션 분산 계획에 따라 토픽의 파티션을 브로커에 추가하거나 삭제한다.

새로 추가된 사본은 단순 사본으로서, 리더에 보관된 메시지를 따라잡는 동작을 개시한다.

### 브로커 결함 내성을 높이기 위해서 추가로 고려해야 할 사항

- 메시지가 성공적으로 합의 되었다고 판단하려면 얼마나 많은 사본에 메시지가 반영되어야 하는가?
- 사본은 같은 노드에 두면 안 된다
- 파티션의 모든 사본이 같은 브로커 노드에 있으면 해당 노드에 장애가 발생할 경우 해당 파티션은 완전 소실될 것이다.
- 파티션의 모든 사본에 문제가 생기면 해당 파티션의 데이터는 영원히 사라진다.
- 사본 수와 사본 위치를 정할 때는 데이터 안전성, 자원 유지에 드는 비용, 그리고 응답 지연 등을 고려하여야 한다.
- 사본은 여러 데이터 센터에 분산하는 것이 안전하다..
- 하지만 그렇게 하면 데이터 동기화 때문에 응답 지연과 비용은 늘어난다.

### 브로커 규모 확장성 더 나은 방법

브로커 컨트롤러로 하여금 한시적으로 시스템에 설정된 사본 수보다 많은 사본을 허용하도록 하는 것이다.

새로 추가된 브로커 노드가 기존 브로커 상태를 따라잡고 나면 더 이상 필요 없는 노드는 제거하면 된다.

### 파티션 추가

- 지속적으로 보관된 메시지는 여전히 기존 파티션에 존재하며 해당 데이터는 이동하지 않는다.
- 새로운 파티션이 추가되면 그 이후 오는 메시지는 3개 파티션 전부에 지속적으로 보관되어야 한다.

따라서 파티션을 늘리면 간단히 토픽의 규모를 늘릴 수 있다.

### 파티션 삭제

- 파티션-3을 퇴역시킨다는 결정이 내려지면 새로운 메시지는 다른 파티션에만 보관된다.
- 퇴역된 파티션은 바로 제거하지 않고 일정 시간 동안 유지한다.
- 해당파티션의 데이터를 읽고 있는 소비자가 있을 수 있기 때문이다.
- 해당 유지 기간이 지나고 나면 데이터를 삭제하고 저장 공간을 반환한다.
- 따라서 파티션을 줄여도 저장 용량은 신속하게 늘어나지 않는다.
- 파티션 퇴역 후 실제로 제거가 이루어지는 시점까지 생산자는 메시지를 남은 두 파티션으로만 보내지만 소비자는 세 파티션 모두에서 메시지를 읽는다. 실제로 파티션이 제거되는 시점이 오면 생산자 그룹은 재조정 작업을 개시해야 한다.

## 메시지 전달 방식

### 최대 한 번

메시지를 최대 한 번 전달하는 방식

- 생산자는 토픽에 비동기적으로 메시지를 보내고 수신 응답을 기다리지 않는다.(ACK=0)
- 메시지 전달이 실패해도 다시 시도하지 않는다
- 소비자는 메시지를 읽고 처리하기 전에 오프셋부터 갱신한다.
- 오프셋이 갱신된 직후에 소비자가 장애로 죽으면 메시지는 다시 소비될 수 없다.

이 전달 방식은 지표 모니터링 등, 소량의 데이터 손실은 감수할 수 있는 애플리케이션에 적합하다.

### 최소 한 번

같은 메시지가 한 번 이상 전달될 수는 있으나 메시지 소실은 발생하지 않는 전달 방식이다.

- 생산자는 메시지를 동기적/비동기적으로 보낼 수 있으며, ACK=1 또는 ACK=all의 구성을 이용한다.
- 즉, 메시지가 브로커에게 전달되었음을 반드시 확인한다.
- 메시지 전달이 실패하거나 타임아웃이 발생한 경우에는 계속 재시도할 것이다.
- 소비자는 데이터를 성공적으로 처리한 뒤에만 오프셋을 갱신한다.
- 메시지 처리가 실패한 경우에는 메시지를 다시 가져오므로 데이터가 손실되는 일은 없다.
- 한편 메시지를 처리한 소비자가 미처 오프셋을 갱신하지 못하고 죽었다가 다시 시작하면 메시지는 중복 처리될 것이다.
- 따라서 메시지는 브로커나 소비자에게 한 번 이상 전달될 수 있다.

이 방식을 채택하면 메시지가 소실되는 일은 없지만 같은 메시지가 여러 번 전송될 수 있다.

- 메시지마다 고유한 키가 있는 경우, 해당 키가 이미 데이터베이스에 있는 메시지는 처리하지 않고 버리면 될 것이다.

### 정확히 한 번

사용자 입장에서는 편리하지만, 시스템의 성능 및 구현 복잡도 측면에서는 큰 대가를 지불해야 한다.

지불, 매매, 회계 등 금융 관련 응용에는 이 전송 방식이 적합하다.

중복을 허용하지 않으며, 구현에 이용할 서비스나 제3자 제품이 같은 입력에 항상 같은 결과를 내 놓도록 구현되어 있지 않은 애플리케이션에 특히 중요하다.
Loading