4장. 분산 메세지 큐 - 1 편
메세지 큐를 사용하면 어떤 이득을 얻을 수 있을까?
- 결합도 완화(decoupling): 컴포넌트 사이의 강한 결합이 사라지므로 각각 독립적이다.
- 규모 확장성 개선: 데이터를 생산하는 생상자(producer)와 메세지를 소비하는 소비자(consumer) 시스템 규모를 트래픽 부하에 맞게 독립적으로 늘릴 수 있다.
- 가용성 개선: 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 사호작용을 이어갈 수 있다.
- 성능 개선: 메세지 큐를 사용하면 비동기 통신이 쉽게 가능하다.
메시지 큐 vs 이벤트 스트리밍 플랫폼
데이터 장기보관, 메세지 반복 소비 등 부가 기능을 갖춘 이벤트 스트리밍 플랫폼을 사용한다.
- 아파치 카프카나 펄사는 메세지 큐가 아니라 이벤트 스트리밍 플랫폼이다.
- 메세지 큐(RokcetMQ, RabbitMQ, ZeroMQ 등)와 이벤트 스트리밍 플랫폼(카프카, 펄사) 사이의 기능이 서로 수렴하면서 점차 차이가 희미해지고 있다.
1단계: 설계 범위
기능 요구 사항
- 메세지는 반복적으로 수신할 수도 있어야 하고, 단 한 번만 수신하도록 설정될 수도 있어야 한다.
- 메세지가 생산되 순서대로 소비자에게 전달할 수 있어야한다.
- 메세지 전달 방식은 최소 한 번, 최대 한 번, 정확히 한 번 가운데 설정할 수 있어야 한다.
비기능 요구사항
- 높은 대역폭과 낮은 전송 지연 가운데 하나를 설정할 수 있어야 한다.
- 규모 확장성. 메세지 양이 급증해도 처리 가능해야 한다.
- 지속성 및 내구성:데이터는 디스크에 지속적으로 보관되어야 하며 여러 노드에 복제되어야 한다.
전통적 메시지 큐와 다른 점
- RabbitMQ와 같은 전통적인 메세지 큐는 메세지 보관 문제를 중요하게 다루지 않는다.
- 전통적인 메세지 큐는 소비자에 전달 되기 충분한 기간 동안만 메모리에 보관한다.
- 전통적인 메세지 큐는 메세지 전달 순서도 보존하지 않는다. 생성된 순서와 소비되는 순서가 다를 수 있다.
2단계: 개략적 설계안
메세지 큐의 기본 기능
- 생산자는 메세지를 메세지 큐에 발행
- 소비자는 큐를 구독(subscribe)하고 구독한 메세지를 소비
- 메세지 큐는 생산자와 소비자 사이의 결합을 느슨하게 하는 서비스로, 생산자와 소비자의 독립적인 운영 및 규모 확장을 가능하게 하는 역할
- 클라이언트와 서버 역할을 하는 것은 메세지 큐이며, 클라이언트와 서버는 네트워크를 통해 통신
1. 메세지 모델
일대일(point to point)과 발행-구독(publish-subscribe) 모델이 가장 널리 쓰인다.
1) 일대일 모델
- 전통적인 메세지 큐에서 발견되는 모델이다.
- 큐에 전송된 메세지는 오직 한 소비자만 가져갈 수 있다. 소비자가 아무리 많아도 각 메세지는 오직 한 소비자만 가져갈 수 있다.
- 어떤 소비자가 메세지를 가져갔다는 사실을 큐에 알리면 해당 메세지는 삭제된다.
(해당 모델은 데이터 보관을 지원하지 않는다.)
2) 발행-구독 모델
- 발행-구독 모델을 설명하려면 토픽(topic)이라는 새로운 개념을 도입해야 한다.
- 토픽은 메세지를 주제별로 정리하는데 사용된다.
(각 토픽은 메세지 큐 서비스 전반에 고유한 이름을 가진다.) - 모든 소비자에 메세지가 전달 된다.
- 발행 구독 모델은 토픽을 통해 토픽을 구독하고 있는 소비자 모두에게 메세지를 보낼 수 있다.
- 일댕리 모델은 소비자 그룹을 통해 지원할 수 있다.
2. 토픽, 파티션, 브로커
- 토픽(topic)에 보관되는 양이 커서 한 대 서버로 감당하기 힘든 경우 파티션(partition) 즉, 샤딩 기법을 활용한다.
- 토픽을 여러 파티션으로 분할한 다음에 메세지를 모든 파티션에 균등하게 나눠 보낸다.
- 파티션을 유지하는 서버는 보통 브로커(broker)라고 부른다.
- 파티션을 브로커에 분산하는 것이 높은 규모 확장성을 달성하는 비결이다.
- 토픽 파티션은 FIFO 큐처럼 동작하기 때문에 같은 파티션 안에서는 메세지 순서가 유지된다.
- 파티션 내에서의 메세지 위치는 오프셋(offset)이라고 한다.
- 생산자가 보내는 메세지에는 사용자 ID 같은 키를 붙일 수 있는데, 같은 키를 가진 모든 메세지는 같은 파티션으로 보내진다. 키가 없는 메세지는 무작위로 선택된 파티션으로 전송된다.
- 토픽을 구독하는 소비자가 여럿인 경우, 토픽을 구성하는 파티션의 일부를 담당하게 되는데 이 소비자들을 소비자 그룹(consumer group)이라고 부른다.
3. 소비자 그룹
- 하나의 소비자 그룹은 여러 토픽을 구독할 수 있고, 오프셋을 별도로 관리한다.
- 같은 그룹 내의 소비자는 메세지를 병렬로 소비할 수 있다.
- 데이터를 병렬로 읽으면 대역폭 측면에서는 좋지만 같은 파티션 안에 있는 메세지를 순서대로 소비할 수 없다.
- 한가지 제약사항을 추가하면 문제를 해결할 수 있는데, 어떤 파티션의 메세지는 하나의 그룹 안에서는 오직 한 소비자만 읽을 수 있도록 하는 것이다.
- 그룹 내 소비자의 수가 구독하는 토픽의 파티션 수보다 크면 어떤 소비자는 해당 토픽에서 데이터를 읽지 못하게 된다.
- 해당 제약사항을 도입하면 결국 일대일 모델에 수렴하게 된다.
4. 개략적 설계안
클라이언트
- 생산자: 메세지를 특정 포틱으로 보낸다.
- 소비자 그룹: 토픽을 구독하고 메세지를 소비한다.
핵심 서비스 및 저장소
- 브로커: 파티션들을 유지한다. 하나의 파티션은 특정 토픽에 대한 메세지의 부분 집합을 유지한다.
- 저장소
- 데이터 저장소: 메세지는 파티션 내 데이터 저장소에 보관한다.
- 상태 저장소: 소비자 상태는 이 저장소에 유지한다.
- 메타데이터 저장소: 토픽 설정, 토픽 속성 등이 저장소에 유지된다.
- 조정 서비스
- 서비스 탐색: 어떤 브로커가 살아있는지 알려준다.
- 리더 선출: 브로커 가운데 하나는 컨트롤러 역할을 담당하며, 반드시 활성 상태 컨트롤러가 하나 있어야한다. 해당 컨트롤러가 파티션 배치를 책임진다.
- 아파치 주키퍼나 etcd가 보통 컨트롤러 선출을 담당하는 컴포넌트로 이용된다.
3단계: 상세 설계
데이터의 장기 보관 요구사항을 만족하면서 높은 대역폭을 제공하기 위해 세 가지 중요한 결정을 내린다.
- 회전 디스크의 높은 순차 탐색 성능과 현대적 운영체제가 제공하는 적극적 디스크 캐시 전략을 잘 이용하는 디스크 기반 자료 구조를 활용
- 메세지가 생산자로부터 소비자에게 전달되는 순서까지 아무 수정 없이도 전송이 가능하도록 하는 메세지 자료 구조를 설계하고 활용한다. 전송 데이터의 양이 막대한 경우에 메세지 복사에 드는 비용을 최소화 하기 위함
- 일괄 처리(baching)를 우선하는 시스템을 설계한다. 소규모의 I/O가 많으면 높은 대역폭을 지원하기 어렵다.
- 생산자는 메세지를 일괄 전송하고, 메세지 큐는 메세지들을 더 큰 단위로 묶어 보관한다.
- 소비자도 가능하면 메세지를 일괄 수신하도록 한다.
1. 데이터 저장소
메세지 큐의 트래픽 패턴을 살펴보자
- 읽기와 쓰기가 빈번하게 일어난다.
- 갱신/삭제 연산은 발생하지 않는다
- 큐에서 메세지가 제때 소비되기 시작하면 저장된 메세지에 대한 삭제 연사이 발생하기는 한다.
- 순차적인 읽기/쓰기가 대부분이다.
1) 선택지 1: 데이터베이스
- 관계형 데이터 베이스: 토픽 별로 테이블을 만든다. 토픽에 보내는 메세지는 해당 테이블에 새로운 레코드로 추가한다.
- NoSQL 데이터베이스: 토빅별로 컬렉션을 만든다. 토픽에 보내는 메세지는 하나의 문서가 된다.
데이터베이스라면 저장 요구사항을 맞출 수 는 있지만 이상적인 방법일 수는 없다.
읽기 연산과 쓰기 연사이 동시에 대규모로 빈번하게 발생하는 상황을 잘 처리하는 데이터베이스는 설계하기 어렵다. 데이터베이스는 최선의 선택지가 될 수없고, 오히려 시스템 병목이 될 수 있다.
2) 선택지 2: 쓰기 우선 로그(Write-Ahead Log, WAL)
- WAL은 새로운 항목이 추가되기만 하는(append-only) 일반 파일이다.
- MySQL의 복구 로그(redo log)가 WAL로 구현되어 있고 아파치 주키퍼도 해당 기술을 활용한다.
- 지속성을 보장 해야하는 메세지는 디스크에 WAL로 보관하는 것을 추천한다.
- WAL에 대한 접근 패턴은 읽기/쓰기 전부 순차적이다.
- 접근 패턴이 순차적일 때 디스크는 아주 좋은 성능을 보이고, 회전식 디스크 기반 저장자잋는 큰 용량을 저렴한 가격에 제공한다.
- 세그먼트를 사용하는 경우, 새 메세지는 활성 상태의 세그먼트 파일에만 추가된다.
- 세그먼트의 크기가 일정 한계에 도달하면 새 활성 세그먼트 파일이 만들어져 새로운 메세지를 수용하고, 종전까지 활성 상태였던 세그먼트 파일은 다른 나머지 세그먼트 파일과 마찬가지로 비활성 상태로 바뀐다.
- 비활성 세그먼트는 읽기 요청만 처리한다.
- 낡은 비활성 세그먼트 파일은 보관 기한이 만료되거나 용량 한계에 도달하면 삭제해 버릴 수 있다.
3) 디스크 성능 관련 유의 사항
- 회전식 디스크가 느리다는 것은 데이터 접근 패턴이 무작위 일때이다.
- 순차적 데이터 접근 패턴을 적극 활용하는 디스크 기반 자료 구조를 사용하면, RAID로 구성된 현대적 디스크 드라이브에서 수백 MB/sec 수준의 읽기/쓰기 성능을 달성하는 것은 어렵지 않다.
- 현대 운영체제는 디스크 데이터를 메모리에 아주 적극적으로 캐시한다.
2. 메세지
1) 메세지 자료구조
- 메세지 자료 구조는 생산자, 메세지 큐 그리고 소비자 사이의 계약이다.
- 시스템의 컴포넌트 가운데 계약을 있는 그대로 받아들이지 못하는 것이 있으면 메세지는 변경되어야 하고, 그 과정에서 값비싼 복사가 발생하게 된다.
필드이름 | 데이터 자료형 |
key | byte[] |
value | byte[] |
topic | string |
partition | integer |
offset | long |
timestamp | long |
size | integer |
crc | integer |
2) 메세지 키
- 메세지 키는 파티션을 정할 때 사용된다.
- 파티션은 hash(key) % numPartitions의 공식에 따라 결정된다.
- 더 유연한 설계가 필요하다면 생산자는 파티션 선정 메커니즘을 직접 정의할 수도 있다.
- 키에는 비지니스 관련 정보가 담기는 것이 보통이고 내부적으로 사용되는 개념이므로 클라이언트에게 노출되어서는 안된다.
3) 메시지 값
- 메세지 값은 메세지의 내용, 즉 페이로드(payload)를 말한다.
- 메세지 값은 텍스트일 수도 있고 압축된 이진 블록일 수도 있다.
4) 메세지의 기타 필드
- 토픽: 메세지가 속한 토픽의 이름
- 파티션: 메세지가 속한 파티션의 ID
- 오프셋: 파티션 내 메세지의 위치.
- 타임스탬프: 메세지가 저장된 시각
- 크기: 메세지 크기
- CRC: 순환 중복 검사의 약자로, 주어진 데이터의 무결성을 보장하는데 이용
3. 일괄처리
일괄 처리는 생산자, 소비자, 메세지 큐에서 광범위하게 사용된다.
일괄 처리가 성능 개선에 중요한 이유
- 운영체제로 하여금 여러 메세지를 한 번의 네트워크 요청으로 전송할 수 있도록 하기 때문
- 브로커가 여러 메세지를 한 번에 로그에 기록하면 더 큰 규모의 순차 쓰기 연산이 발생하고 운영체제가 관리하는 디스크 캐시에서 더 큰 규모의 연속되 공간을 점유하게 된다. 그 결과 더 높은 디스크 접근 대역폭을 달성할 수 있게 된다.
높은 대역폭과 낮은 응답 지연은 동시에 달성하기 어려운 목표다.
시스템이 낮은 응답 지연이 중요한 전통적 메세지 쿠로 이용된다면 일괄 처리 메세지 양은 낮춘다.
처리량을 높여야 한다면 토픽당 파티션의 수를 늘린다. 그래야 낮아진 순차 쓰기 연산 대여폭을 벌충할 수 있다.
4. 생산자 측 작업 흐름
생산자가 어떤 파티션에 메세지를 보내는 방법으로 라우팅 계층을 도입하는 것이다.
라우팅 계층은 '적절한' 브로커에 메세지를 보내는 역할을 담당한다.
브로커를 여러 개로 복제하여 운용하는 경우에 메시지를 받을 '적절한' 브로커는 바로 리더 브로커다.
생산자가 토픽-A의 파티션-1로 메세지를 보내는 과정
- 생산자는 메세지를 라우팅 계층으로 보낸다.
- 라우팅 계층은 메타데이터 저장소에서 사본 분산 계획을 읽어 자기 캐시에 보관한다. 메세지가 도착하면 라우팅 계층은 파티션-1의 리더 사본에 보낸다.
- 리더 사본이 우선 메세지를 받고 해당 리더를 따르는 다른 사본은 해당 리더로부터 데이터를 받는다.
- '충분한' 수의 사본이 동기화되면 리더는 데이터를 디스크에 기록한다. 데이터가 소비 가능 상태가 되는 것이 바로 해당 시점이다. 기록이 끝나고 나면 생산자에게 회신을 보낸다.
해당 동작은 몇 가지 단점이 있다.
- 라우팅 계층을 도입하면 거쳐야 할 네트워크 노드가 하나 더 늘어나게 되므로 오버헤드가 발생하여 네트워크 전송 지연이 늘어난다.
- 일괄 처리가 가능하면 효율을 많이 높일 수 있는데 그런 부분은 고려하지 않았다.
변경된 설계안
라우팅 계층을 생산자 내부로 편입시키고 버퍼를 도입한다.
생산자 클라이언트 라이브러리의 일부로 생산자에 설치한다.
몇 가지 장점
- 네트워크를 거칠 필요가 줄어들기 때문에 전송 지연도 줄어든다.
- 생산자는 메세지를 어느 파티션에 보낼지 결정하는 자신만의 로직을 가질 수 있다.
- 전송할 메세지를 버퍼 메모리에 보관했다가 목적지로 일괄 전송하여 대역폭을 높일 수 있다.
대역폭과 응답지연 사이에 타협점을 찾아서 얼마나 많은 메세지를 일괄 처리하는 것이 좋을지 고민하자.
5. 소비자 측 작업 흐름
소비자는 특정 파티션의 오프셋을 주고 해당 위치에서부터 이벤트를 묶어 가져온다.