블라인드 스터디 : 분산 메시지 큐 시스템 설계
서버개발

블라인드 스터디 : 분산 메시지 큐 시스템 설계

스터디에 들어가기 전

이번에 시스템 설계 면접 사례를 겪게 되고, 테크 컨퍼런스를 찾아보다보니 현 회사에서 겪어보지 못하는, 혹은 직접 개발에 관여하지 않는 대규모 시스템 설계에 대해 더 공부하면 좋겠다 생각이 들어 세 번째 블라인드 구성원들과 함께 하는 스터디로 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2를 시작하였습니다.

 

8주간 총 13장 중 구성원들이 가장 관심 있어하는 8장에 대해서 진행하려고 하고, 제 기준으로는 가볍지 않은 내용이기 때문에 주마다 한 장씩 공부하면서 블로그에 정리하는 시간을 가져보려고 합니다. 요즘 많이 안 적기도 했고..

투표 결과

 

 

[4장] 분산 메시지 큐

분산 메시지 큐를 사용하는 목적

1. 결합도 완화 : 컴포넌트 사이의 강한 결합을 제거하여 독립적으로 갱신

2. 규모 확장성 개선 : pub/sub 구조로 시스템 규모를 트래픽 규모에 맞게 scale-out 가능

3. 가용성 개선 : 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 게속 상호작용(분산화)

4. 성능 개선 : 응답을 기다리지 않고 송신, 메시지가 있을 때만 수신하여 성능 향상

 

 

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

두 개 모두 분산 시스템에서 메시지를 효율적으로 전달하기 위해 사용되며 몇 가지 차이점이 있다

 

메시지 큐

- 비동기적인 통신을 위해 사용되며, 시스템 간의 메시지를 안정적으로 전송하고 처리 지연을 최소화 하는데 중점

- 송신 시스템은 메시지를 보내고 수신 시스템은 메시지를 소비한다

- point to point, pub/sub으로 메시지를 통신하며 메시지를 큐에 넣고 큐에서 꺼내 동작

 

이벤트 스트리밍 플랫폼

- 이벤트의 연속적인 흐름을 다루는데 중점을 두어, 실시간 데이터 스트리밍과 이벤트 처리를 위해 설계

- 이벤트의 발생과 소비에 초점을 두어 여러 시스템 간의 실시간 데이터 흐름을 처리

- 이벤트의 연속적인 스트림을 다뤄 다양한 구독자들이 이벤트를 처리

 

하지만 요즘 플랫폼들은 서로 간의 기능을 지원해주면서 차이가 희미해지고 있다

 

 

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

메시지 큐의 기본 기능으로는 생산자는 메시지를 큐에 보내고, 소비자는 큐에서 메시지를 꺼낸다

하지만 성능, 메시지 전달 방식, 데이터 보관 기간 등 고려할 사항이 많아 실제 면접 시 질문을 통해 요구사항을 분명히 밝혀야 한다

 

고려할 질문으로는 

- 메시지의 형태와 평균 크기(몇 KB, 멀티 미디어?)

- 반복적으로 소비될 수 있는 메시지인지? (기본적으로 분산 메시지 큐는 한 소비자가 받아간 메시지를 지운다)

- 전달된 순서대로 소비되어야 하는지? (기본적으로 순서를 보증하지 않는다)

- 데이터의 지속성은 보장되어야 하는지? (기본적으로 길게 보관을 보증하지 않는다)

- 생산자와 소비자의 수는?

- 어떤 메시지 전달 방식을 지원해야 하는지(최대 한번, 최소 한 번, 정확히 한 번)

- 대역폭(throughput)과 단대단(end to end) 지연 시간은? (로그 수집 고려한 높은 전송 지연, 기본 메시지 큐와 같은 낮은 전송 지연)

 

요구사항 분석

기능 요구사항 분석

책에 있는 예시대로 분석을 진행한다

실제로는 면접 대답을 듣고 능동적으로 분석할 수 있어야 한다

- 생산자는 메시지 큐에 메시지를 보낼 수 있어야 한다

- 소비자는 메시지 큐를 통해 메시지를 수신할 수 있어야 한다

- 메시지는 반복적으로 수신할 수도 있어야 하고, 단 한 번만 수신하도록 설정될 수도 있어야 한다

- 오래된 이력 데이터는 삭제될 수 있다 (2주는 보관)

- 메시지 크기는 킬로바이트 수준이다

- 메시지가 생산된 순서를 보장해야 한다

- 전달 방식은 최소 한 번, 최대 한 번, 정확히 한 번 중 선택할 수 있어야 한다

 

비 기능 요구사항 분석

- 높은 대역폭과 낮은 전송 지연 가운데 선택이 가능해야 한다

- 규모 확장성 : 시스템 특성 상 분산 시스템이므로 메시지가 급증해도 처리 가능해야 한다

- 지속성 및 내구성 : 데이터는 디스크에 지속적으로 보관되고 여러 노드에 복제 되어야 한다

 

 

전통적 메시지 큐와 다른점

전통적 메시지 큐는 이벤트 스트리밍 플랫폼과 달리 메시지 보관 문제를 중요하게 다루지 않는다

메시지가 소비자에게 전달되기 충분한 기간 동안만 메모리에 보관하며, 처리 용량을 넘어선 경우 디스크에 보관하지만, 이벤트 스트리밍보다는 낮은 수준이다.

또한, 메시지 전달 순서도 보존되어야 한다

 

 

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

생산자는 메시지를 메시지 큐에 발행하고 소비자는 큐를 구독하고 구독한 메시지를 소비한다

메시지 큐는 생산자와 소비자 사이의 결합을 느슨하게 하여 생산자와 소비자가 독립적인 운영 및 규모 확장을 가능하게 하는 역할을 담당하며, 생산자와 소비자를 모두 클라이언트/서버 모델 관점에서 보면 클라이언트고, 서버 역할을 하는 것이 메시지 큐이다

 

point to point와 pub/sub으로 많이 사용되며 일대일 모델에서는 전송된 메시지를 하나의 소비자만 가져갈 수 있고, 어떤 소비자가 메시지를 가져갔다는 걸 큐에 알리면 해당 메시지는 큐에서 삭제된다. (데이터 보관을 지원하지 않을 시)

 

본 설계에서는 메시지를 2주 동안 보관해야 하므로 Persistence Layer를 통해 메시지가 반복적으로 소비될 수 있도록 한다

 

Pub/Sub 모델은 Topic을 통해 메시지를 주제별로 정리하는데 사용한다

각 토픽은 메시지 큐 서비스 전반에 고유한 이름을 가지며, 메시지를 보내고 받을 때 토픽을 보내고 받게 된다

이를 통해 구독하는 모든 소비자에게 전달하여 토픽을 통해 구현하거나, 메시지 큐와 consumer group을 통해 구현할 수 있다.

 

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

샤딩 기법을 통해 토픽을 여러 파티션으로 분리하고, 메시지를 모든 파티션에 나눌 방법을 설계한다

파티션을 유지하는 서버를 브로커라 부르며, 브로커를 통해 파티션의 개수를 늘리면 높은 규모 확장성을 달성할 수 있다

브로커는 토픽 별이 아닌 파티션 별로 나누어 가질 수 있고, 이를 통해 각 파티션을 병렬적으로 소비시킬 수 있다

또한, 파티션의 레플리카를 두어 서버 장애에 대응하고, 파티션 내에서도 바로 정보를 pop 시키지 않고 offset을 통해 제공해 소비자가 장애가 발생한 경우를 대비한다

 

각 토픽 파티션은 큐 처럼 동작하며, 안에서 메시지 순서가 유지된다

생산자가 보낸 메시지는 해당 토픽의 파티션 중 하나로 보내지며, 키를 붙여 같은 키를 가진 모든 메시지는 같은 파티션으로 보내지도록 설정하고, 없는 메시지는 무작위로 전송된다

토픽을 구독하는 소비자가 하나 이상의 파티션에서 데이터를 가져오고, 구독하는 소비자가 여럿인 경우 해당 토픽을 구성하는 파티션의 일부를 담당한다

 

Kafka 메시지 순서 보장 확인해보기

모든 소스 코드는 https://github.com/lkimilhol/kotlin-kafka-toy 에서 확인 가능합니다. 오늘의 고민 내용은 카프카를 통하여 메시지를 순서대로 받을 수 있을까 입니다. 사실 이는 가능한데요. 카프카의 파

kimmayer.tistory.com

 

본 설계안은 일대일 모델과 발행-구독 모델을 전부 지원해야 하므로 소비자 그룹 내 소비자들은 토픽에서 메시지를 소비하기 위해 서로 협력한다

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

같은 그룹 내의 소비자는 메시지를 서로 다른 파티션의 메시지를 병렬로 소비 할 수 있다

 

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

본 설계에서는 읽는 순서가 보장되어야 하므로 다른 전략이 필요하다

 

나이브한 방법으로는 어떤 파티션의 메시지는 소비자 그룹 내에서는 한 소비자만 읽도록 제한 하는 것이다

그렇지만 이 경우에는 scale out으로 소비자의 수가 늘어나게 되면 토픽의 파티션 수보다 커지게 되어 어떤 소비자는 해당 토픽에서 데이터를 읽지 못하게 되고, 트래픽이 몰리는 경우 scale out을 통해 성능을 향상시킬 수 없게 된다

모든 소비자가 같은 그룹이라면 일대일 모델과 같아지게 된다

파티션을 미리 늘려 동적 스케일링을 피하고, 처리 용량을 늘릴 때 소비자를 늘리면 된다

병렬성의 정도는 소비자의 수가 아닌, 파티션의 개수가 결정하기 때문이다

 

Understanding Kafka Topic Partitions

Everything in Kafka is modeled around partitions. They rule Kafka’s storage, scalability, replication, and message movement.

medium.com

+ 이 경우에도 파티션 내에서 순서는 보장하지만 토픽 내에서 메시지 순서를 보장하지는 않는다

토픽에 대한 전역한 순서 보장이 필요한 경우에는 다시 파티션을 1개로 변경해야 한다.

확장성을 고려해야 한다면 어떻게 해야할까... 

 

전송이 완료된 메시지들은 파티션 내 데이터 저장소에 보관되며, 소비자 상태는 소비자 저장소에 저장되고, 토픽 관련된 데이터는 메타 데이터 저장소에 저장된다

 

또한, 브로커 또한 관리 되어야 하기 때문에 브로커를 조정할 서비스와 와 브로커를 관리할 컨트롤러 브로커를 선정해야 하며 해당 브로커가 파티션 배치를 담당한다

ZooKeeper나 etcd가 보통 컨트롤러 선출을 담당하는 컴포넌트로 이용된다

 

 

3단계 : 상세 설계

데이터의 장기 보관 요구사항을 만족하며, 높은 대역폭을 제공하기 위해 세 가지가 결정한다

1. 회전 디스크의 높은 순차 탐색 성능과 디스크 캐시 전략을 잘 이용하는 디스크 기반 자료구조 활용

2. 메시지가 생산자 - 소비자로 전달되는 순간까지 아무 수정 없이 전송 가능한 메시지 자료 구조 설계 및 활용

데이터 양이 커지는 경우 메시지 복사에 드는 비용 최소화를 위함

3. 일괄 처리를 우선하는 시스템 설계하여 소규모 I/O가 반복되더라도 높은 대역폭을 지원하도록 설계

생산자는 메시지를 일괄 전송, 메시지 큐는 그 메세지들을 더 큰 단위로 묶어 보관

 

데이터 저장소

메시지를 지속적으로 저장하는 방법으로 가장 좋은 방법을 선택하기 위해 메시지 큐의 트래픽 패턴을 확인한다

- 읽기와 쓰기가 빈번하게 일어난다

- 갱신/삭제 연산은 발생하지 않는다(전통적인 메시지 큐가 아닌 지금 만드는 메시지 큐)

- 순차적인 읽기/쓰기가 대부분이다

 

선택지 1 : 데이터베이스

- RDBMS : 토픽별로 테이블을 만들어, 토픽에 보내는 메시지를 해당 테이블에 새로운 레코드로 추가한다

- NoSQL : 토픽별로 컬렉션을 만들어 토픽에 보내는 메시지를 추가한다

저장 요구사항은 맞출 수는 있으나 읽기 연산과 쓰기 연산이 빈번하게 일어나기 때문에 최선의 선택지는 될 수 없고, 오히려 시스템 병목 현상이 발생할 수 있다

 

선택지 2 : 쓰기 우선 로그(WAL)

WAL은 새로운 항목이 추가되기만 하는 append only 파일을 만들어 복구하는 전략으로 MySQL의 복구로그, 주키퍼와 같으며, Redis와 같은 인메모리 데이터베이스에서도 AOF라는 백업 전략으로 사용하는 방법과 유사하다

WAL의 접근 패턴은 읽기/쓰기 모두 순차적이며, 접근 패턴이 순차적일 때 높은 성능을 보인다

 

게다가 회전식 디스크 기반 저장장치(HDD)는 큰 용량에 저렴한 가격에 제공된다

파티션의 오프셋도 대응하기 위해서 로그 파일 줄 번호를 오프셋으로 활용하고, 파일이 커지는 경우 세그먼트를 나눈다

파티션 내 세그먼트에서 가장 최신의 세그먼트만 남겨두고 다른 세그먼트는 비활성 상태로 유지해 새로운 메시지를 수용한다

 

오래된 세그먼트는 보관 기한이 만료되거나 용량 한계에 도달하면 삭제한다

데이터 장기 보관에 대한 요구사항 때문에 본 설계안은 HDD를 활용해 데이터를 보관하며, 데이터 접근 순서가 순차적이므로 높은 효율을 보인다

 

 

메시지 자료구조

메시지 구조는 높은 대역폭을 위한 열쇠이다

생산자, 메시지 큐, 소비자 사이의 계약으로 메시지가 이동하는 동안 불필요한 복사가 일어나지 않도록 하여 높은 대역폭을 달성할 수 있다

이 사이에 원활한 복사가 이루어지지 않으면 값비싼 복사가 이루어질 수 있고, 시스템 전반의 성능이 낮아질 수 있다

 

1. 메시지 키

키는 파티션을 구분하기 위해 해시나 특수한 알고리즘의 형태로 작성될 수 있고, 문자열이나 숫자로 지정된다

비즈니스 관련 정보가 담기는 것이 보통이며, 클라이언트에 노출이 되어서는 안된다

키를 파티션에 대응 시키는 알고리즘을 정의해 놓으면 파티션의 수가 달라져도 모든 파티션에 메시지가 균등히 분산될 수 있다

 

*메시지의 키는 메시지마다 고유할 필요가 없다. 반드시 키가 필요한 것도 아니며, 키를 사용해 값을 찾을 필요도 없다

 

2. 메시지 값

메시지의 내용, 즉 payload이다. 일반 텍스트일 수도 있고 압축된 이진 데이터일 수도 있다

 

그 외

3. 토픽 : 메시지가 속한 토픽의 이름

4. 파티션 : 메시지가 속한 파티션의 ID

5. 오프셋 : 파티션 내 메시지의 위치. 메시지는 토픽, 파티션, 오프셋의 정보로 조회

6. 타임스탬프 : 저장된 시각

7. 크기 : 메시지의 크기

8. CRC : 순환 중복 검사, 주어진 데이터의 무결성 보장

+ @

 

 

일괄 처리

일괄 처리는 시스템 성능에 큰 영향을 끼쳐 생산자, 소비자, 메시지 큐는 메시지를 가급적 일괄 처리한다

일괄 처리가 중요한 이유는

- OS에 여러 메시지를 한 번의 네트워크 요청으로 전송할 수 있어 네트워크 왕복 비용을 제거할 수 있다

- 브로커가 여러 메시지를 한 번에 로그에 기록하여 더 큰 규모의 순차 쓰기 연산이 발생하고, 운영체제가 관리하는 디스크 캐시에서 더 큰 규모의 연속 된 공간을 점유하여 더 높은 접근 대역폭을 달성한다

 

높은 대역폭과 낮은 응답 지연은 동시에 달성하기 어렵기 때문에 낮은 응답 지연이 필요한 시스템에서는 일반적인 메시지 큐를 활용하면서, 처리량을 높여야 하면 토픽당 파티션 수를 늘려 낮아진 순차 쓰기 연산 대역폭을 늘린다

 

 

생산자 측 작업 흐름

생산자는 어떤 파티션에 메시지를 보내야 할 때, 어느 브로커에 연결해야 할까?

한 가지 해결책은 라우팅 계층을 도입하는 것이다

라우팅 계층은 적절한 브로커에 메시지를 보내는 역할을 담당하며, 브로커를 여러 개로 복제하여 운용 시 메시지를 받을 적절한 브로커는 바로 리더 브로커이다. 

 

기본 설계

생산자 <-> 라우팅 계층 <-> 리더 브로커 <-> 사본 브로커

1. 우선 생산자는 메시지를 라우팅 계층으로 보낸다

2. 라우팅 계층은 메타 데이터 저장소에서 replica distribution plan을 읽어 자기 캐시에 보관하고, 메시지가 도착하면 파티션 1의 리더 사본을 보낸다.

3. 리더 사본이 우선 메시지를 받고, 해당 리더를 따르는 다른 사본들은 해당 리더로부터 데이터를 받는다

4. 충분한 수의 사본이 동기화되면 리더는 데이터를 디스크에 기록하고, 데이터가 소비 가능 상태가 되는 것이 이 시점이다. 기록이 끝나면 생산자에게 회신을 보낸다

 

리더와 사본이 필요한 이유는 장애를 버틸 수 있는 시스템을 만들기 위해서다.

위의 방법은 라우팅 계층을 도입 시, 거치는 네트워크 노드가 늘어나 오버헤드가 증가하고, 일괄 처리가 가능한 설계가 되어있지 않다.

 

생산자[버퍼, 라우팅 계층] <-> 리더 브로커 <-> 사본 브로커

그래서 라우팅 계층을 생산자 내부로 편입 시키고, 버퍼를 도입 후 생산자 클라이언트 라이브러리의 일부로 생산자에 설치하는 것이다.

 

이렇게 하면 네트워크를 거칠 필요가 없어지고, 생산자는 메시지를 어느 파티션에 보낼지 결정하는 로직을 가질 수 있으며,

전송할 메시지를 버퍼 메모리에 보관했다가 목적지로 일괄 전송하여 대역폭을 높일 수 있다

 

 

소비자 측 작업 흐름

소비자는 특정 파티션의 오프셋을 주고 해당 위치에서부터 이벤트를 묶어 가져온다

 

푸시 모델(브로커가 데이터를 소비자에게 보내는 것)

장점

- 낮은 지연 : 브로커는 메시지를 받는 즉시 소비자에게 보낼 수 있다

단점

- 소비자가 메시지를 처리하는 속도가 생산자가 메시지를 만드는 속도보다 느릴 경우, 소비자에 부하가 생긴다

- 생산자가 데이터 전송 속도를 좌우하므로, 소비자가 항상 그에 맞는 컴퓨팅 자원을 준비해야 한다

 

풀 모델(소비자가 데이터를 가져가는 것)

장점

- 메시지를 소비하는 속도를 소비자가 결정하여, 어떤 소비자는 메시지를 실시간으로 가져가고, 어떤 소비자는 일괄로 가져갈지 선택할 수 있다

- 메시지를 소비하는 속도가 생산 속도보다 느려져도 소비자를 늘려 해결할 수 있고, 생산 속도를 따라 잡을 때 까지 기다릴 수 있다

- 일괄 처리에 적합하다. 푸시 모델은 브로커가 소비자의 상태를 알지 못하기 때문에 적합하지 않다

단점

- 브로커에 메시지가 없어도 소비자는 계속 데이터를 가져가려고 시도한다(소비자 측 자원 낭비)

많은 메시지 큐가 busy waiting 하지 않기 위해서 long polling 모드를 지원한다(당장 가져가지 않아도 일정 대기)

 

1. 그룹에 합류해 토픽을 구독하길 원하는 새로운 소비자가 있으면 소비자는 그룹 이름을 해싱해 접속할 브로커 노드(코디네이터)를 찾는다. 같은 그룹의 소비자는 같은 브로커에 접속한다(브로커 클러스터링)

2. 코디네이터는 해당 소비자를 그룹에 참여시키고 특정 파티션을 소비자에게 할당한다(라운드 로빈 등으로)

3. 소비자는 마지막 소비한 오프셋 이후 메시지를 가져온다

4. 메시지를 처리하고 새로운 오프셋을 브로커에 보낸다. 데이터 처리와 오프셋 갱신 순서는 메시지 전송 시맨틱에 영향을 미친다

 

 

소비자 재조정

소비자가 어떤 파티션을 책임지는지 다시 정하는 프로세스로, 새로운 소비자가 합류하거나 기존 소비자를 그룹에서 떠나서(장애 등) 조정해야 하는 경우다

이 경우에 코디네이터가 큰 역할을 하는데, 코디네이터는 소비자들과 통신하는 브로커 노드로, 소비자로부터 오는 heartbeat와 파티션 내 오프셋 정보를 관리하며 소비자 그룹을 해싱하여 정보를 찾을 수 있다

 

코디네이터는 자신에 연결한 소비자 목록을 유지하고, 변화가 생기면 해당 그룹의 새 리더를 선출한다

새 리더는 새 파티션 배치 계획을 짜고, 코디네이터에게 전달해 해당 계획을 그룹 내 다른 모든 소비자에게 전달한다

 

분산 시스템이기 때문에 소비자는 언제든 문제가 생길 수 있고, 소비자에게 발생한 장애로 인해 heartbeat가 사라지는 현상을 감지하여 재조정 프로세스를 시작해 파티션을 재배치한다

그룹에 재조정이 일어나면 모든 소비자에게 재 그룹 참여 요청을 보내고 마지막에 그룹 동기화를 각각 진행하게 된다

 

 

상태 저장소

메시지 큐 브로커의 상태 저장소는 다음과 같은 정보가 저장된다

- 소비자에 대한 파티션의 배치 관계

- 각 소비자 그룹이 각 파티션에 마지막으로 가져간 메시지의 오프셋

 

저장된 상태는 다음과 같이 이용된다

- 읽기와 쓰기가 빈번하지만 양이 많지 않다

- 데이터갱신은 빈번하게 일어나지만 삭제되는 일은 거의 없다(재조정 일 때)

- 읽기와 쓰기 연산은 무작위적 패턴을 보인다

- 데이터의 일관성이 중요하다

 

고로 상태 저장소의 저장소 기술로는 높은 읽기/쓰기 속도와 데이터 일관성을 갖춘 주키퍼 같은 키-값 저장소를 사용하는 것이 바람직하다

 

 

메타 데이터 저장

토픽 설정이나 속성 정보를 보관하는 저장소로 파티션 수, 메시지 보관 기간, 사본 배치 정보 등이 해당되며 자주 변경되지 않고 양도 적지만 아주 높은 일관성을 필요로 하기 때문에 이런 데이터의 보관도 주키퍼가 적절하다..

 

 

주키퍼

계층적 키-값 저장소로, 분산 시스템에 유용한 서비스이다.

분산 설정 서비스와 동기화 서비스, 이름 레지스트리 등으로 이용되며 주키퍼로 메타데이터, 상태 저장소, 조정 서비스(브로커 클러스터의 리더 선출 과정)를 구성 시 브로커는 데이터 저장소만 구축하면 된다

 

 

복제

분산 시스템에서 하드웨어 장애는 흔한 일이므로 대응을 해야 하는데, 디스크 손상이나 영구적 장애가 발생하면 데이터는 사라진다

이런 문제를 해결하기 위해 높은 가용성을 보장하는 방법이 복제(replication)다

각 파티션은 2개의 사본을 갖고, 이 사본들은 여러 브로커에 분산이 되게 구성한다

초록색만 파티션의 리더이고, 나머지는 단순 복사본이다

생산자는 파티션에 메시지를 보낼 때 리더에게만 보내며, 다른 사본은 리더에서 새 메시지를 지속적으로 가져와 동기화한다

메시지를 완전히 동기화 한 사본의 개수가 임계값을 넘으면 리더는 생산자에게 메시지를 잘 받았다는 응답을 보내 넘길 수 있는 상태가 된다

사본을 분산 하는 계획을 사본 분산 계획이라고 하며 리더 브로커 노드가 이 계획을 메타 데이터 저장소에 보관하고 다른 모든 브로커는 해당 계획대로 움직인다

 

 

사본 동기화

한 노드의 장애로 메시지가 소실되는 것을 막기 위해 메시지는 여러 파티션에 두며, 각 파티션은 다시 여러 사본으로 복제 한다

메시지는 리더에게만 보내고 다른 단순 사본은 리더에게 메시지를 가져가 동기화하는데 어떻게 동기화를 할까?

동기화된 사본(In Synced Replicas, ISR)은 리더와 동기화 되어 있고, 토픽 설정에 따라 동기화의 의미가 달라진다

단순 사본에 보관된 메시지 개수와 리더 사이의 차이가 replica.lag.max.messages 값보다 작다면 동기화된 상태이다

ISR은 성능과 영속성 사이의 타협점으로, 생산자가 보낸 어떤 메시지도 소실하지 않는 가장 안전한 방법은 생산자에게 메시지를 잘 받았다는 응답을 보내기 전, 모든 사본을 동기화하는 것이다

 

갑작스럽게 메시지가 몰려서 replica.lag.max.messages보다 리더와 ISR의 차이가 벌어지는 경우 동기화가 순차적으로 잘 되고 있지만, 되고 있지 않다고 판단될 수 있다

이런 경우에는 replica.lag.time.max.ms 지연 시간으로 확인하여야 한다

 

하지만 어느 사본 하나라도 동기화에 실패하면 파티션 전부가 느려지거나 못 쓰는 일이 발생하기 때문에 생산자는 k개의 ISR이 메시지를 받았을 때, 응답을 받도록 설정할 수 있다

- ACK = all : 모든 ISR이 메시지를 수신한 뒤 ACK 응답

- ACK = 1 : 생산자는 리더가 메시지를 저장한 뒤 바로 응답, 직후 리더가 장애 시 메시지 소실(사라져도 상관없지만 빠른 응답을 요구할 때)

- ACK = 0 : 보낸 메시지에 대한 수신 확인 메시지를 기다리지 않고 게속 전송해 어떤 재시도도 하지 않는다(아주 빠른 시도, 지표 수집, 데이터 로깅)

이처럼 ACK 설정을 변경 가능하도록 한다면 성능을 높일 때 영속성이 희생될 수 있다

 

소비자 측면에서는 소비자로 하여금 리더에서 메시지를 읽도록 하는 것이다

그러나 리더 사본에 요청이 너무 많이 몰리면 어떻게 될까? ISR에서 왜 메시지를 가져가지 않을까?

- 설계 및 운영이 단순

- 특정 파티션의 메시지는 같은 소비자 그룹 안에서 오직 한 소비자만 읽어 리더 사본에 대한 연결은 많지 않다

- 아주 인기 있는 토픽이 아니라면 리더 사본에 대한 연결의 수는 많지 않다

- 아주 인기 있는 토픽의 경우 파티션 및 소비자 수를 늘려 규모를 확장하면 된다

 

하지만 그렇지 않은 예도 있다

소비자의 위치가 리더 사본이 존재하는 데이터 센터라 읽기 성능이 나빠지고, 지역적으로 가까운 ISR 사본에서 메시지를 읽는 선택지도 고려해볼 수 있다

 

 

규모 확장성

생산자

소비자에 비해 개념적으로 간단하며 그룹 단위의 조정에 가담하지 않아도 되어 새로운 생산자를 쉽게 추가/삭제한다

 

소비자

소비자 그룹은 서로 독립적이므로 새 소비자 그룹은 쉽게 추가/삭제 가능하고, 같은 그룹 내 소비자가 추가/삭제 되거나 장애로 제거되는 경우 재조정 메커니즘이 처리한다. 이를 통해 규모확장성과 결함 내성을 보장한다

 

브로커

브로커에 장애가 발생해 해당 브로커 내에 있던 리더 파티션과 사본 파티션들이 사용을 할 수 없게 되면 브로커 컨트롤러가 감지하여 남은 브로커 노드들을 위해 파티션 분산 계획을 작성한다

새롭게 추가된 사본은 리더의 메시지를 따라잡는 동작 개시한다

 

다음과 같은 사항을 고려하며 미리 준비한다

- 메시지가 성공적으로 커밋되려면 몇 개의 사본에 메시지가 반영되어야 하는지

- 파티션의 모든 사본을 같은 브로커에 두면 장애 시 영구 소실된다 + 자원 낭비

- 파티션의 모든 사본에 문제가 생기면 해당 파티션의 데이터는 영원히 사라진다

- 그래서 사본 수와 위치, 데이터 안전성, 자원 유지에 드는 비용, 응답 지연을 고려해야한다

- 확장 시 브로커 컨트롤러가 한시적으로 시스템 설정 사본 수보다 많은 사본을 허용하여 새로 추가된 브로커가 기존 브로커 상태를 따라잡고 나면 더 이상 필요 없는 노드를 제거하는 식으로 진행된다(중간 데이터 손실 회피)

 

*리더 파티션이 사본 파티션에 동기화를 했지만 완벽히 같은 상태는 아니고, 생산자한테 커밋되었음을 알린 뒤에 리더 파티션이 포함된 브로커에 장애가 발생하게 된다면 리더 파티션과 사본 파티션 사이의 메시지 차이만큼 소실이 발생되지 않나?

 

파티션 

토픽의 규모를 늘리거나, 대역폭 조정, 가용성과 대역폭 사이 균형을 맞추는 등 파티션의 수를 조정해야 한다면 발생하여 생산자는 브로커와 통신할 때 그 사실을 통지 받으며, 소비자는 재조정을 시행한다

따라서 파티션의 수 조정(사본이 아닌 서로 다른 파티션)은 생산자와 소비자의 안전성에는 영향을 끼치지 않고, 데이터 저장 계층이 달라진다

 

파티션을 추가하게되면 기존에 존재하던 파티션은 원래 데이터를 가지고 있고, 새로 추가된 파티션은 기존 파티션들과 함께 새롭게 오는 메시지를 받게 된다

하지만 파티션을 제거하게되면 새로운 메시지는 다른 파티션에만 보관하며 제거된 파티션은 바로 제거되지 않고 일정 시간 유지하다가 해당 파티션의 소비자가 재차 읽는 것을 기다린다

그래서 파티션을 지워도 바로 용량이 늘어나지 않는다

 

메시지 전달 방식

최대 한 번

메시지가 전달 과정에서 소실되더라도 다시 전달되지 않는다

생산자는 토픽에 비동기적으로 메시지를 보내고 수신 응답을 기다리지 않는다 

소비자는 메시지를 읽고 처리하기 전에 오프셋부터 갱신하며, 오프셋이 갱신된 직후 소비자가 장애로 죽으면 메시지를 다시 소비할 수 없다

지표 모니터링, 소량의 데이터 손실을 감수할 수 있는 어플리케이션에 적합하다

 

최소 한 번

메시지가 소실되지 않고 한 번 이상 전달될 수 있다

생산자는 메시지를 동기/비동기적으로 보낼 수 있으며, 메시지가 브로커에게 전달되었음을 반드시 확인한다

메시지 전달이 실패하거나 타임아웃이 발생한 경우에는 계속 재시도 한다

소비자는 데이터를 성공적으로 처리한 후에 오프셋을 갱신하며, 실패한 경우 재시도해 메시지가 소실되지 않는다

메시지를 처리한 소비자가 미쳐 오프셋을 갱신하지 못하고 죽으면 중복 처리될 수 있다

고로 한 번 이상 전달될 수 있다

 

데이터 중복이 큰 의미가 없는 어플리케이션이나 소비자가 중복을 직접 제거할 수 있는 어플리케이션의 경우에 괜찮은 전송 방식이다

메시지마다 고유한 키가 있는 경우 해당 키가 이미 처리된 것을 확인할 수 있는 경우 버리면 될 것이다

 

정확히 한 번

사용자 입장에서는 편하지만 구현하기 가장 까다로운 시스템의 성능 및 구현 복잡도를 가지고 있다

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

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

 

 

고급 기능

메시지 필터링

토픽은 같은 유형의 메시지를 담아 처리하기 위해 도입된 논리적 개념으로, 어떤 소비자는 특정한 하위 유형의 메시지에만 관심이 있다

주문 시스템은 토픽에 주문과 관련된 모든 활동을 전송하지만, 지불 시스템은 결재나 환불 관련 메시지에만 관심이 있다

이런 요구사항을 처리하는 방법으로 토픽을 분리할 수도 있지만 시스템이 필요로 할 때마다 토픽을 분리하기도 어렵고, 같은 메시지를 여러 토픽에 저장하는 것도 자원 낭비이기 때문에 좋지 않다 + 생산자/소비자간의 결합도 증가

 

메시지를 필터링하기 가장 쉬운 방법은 소비자가 모든 메시지를 받은 다음 필요 없는 메시지를 버리는 방법이 있지만, 불필요한 트래픽이 발생해 시스템 성능이 저하되는 문제가 있다

반대로 브로커가 필터링해서 소비자에게 메시지를 줄 수 있지만, 필터링 연산에 복호화/역직렬화 등이 포함된다면 브로커 성능이 저하된다. 또한 비공개 데이터가 있을 수 있어 페이로드를 추출해서는 안된다

 

필터링에 사용될 데이터를 메시지의 메타 데이터에 두어 브로커에서 필터링할 수 있게 한다면 좋다

태그를 이용해 특정 태그들을 구독하는 형태로 소비자를 만들고, 브로커 스크립트를 작성해 필터링한다

 

 

메시지 지연 전송 및 예약 전송

소비자에게 보낼 메시지를 지연시켜야 하는 일이 있을 수 있다(30분 뒤 A 주문 취소 같은 메시지를 보내고 싶을 때)

이런 메시지들은 바로 토픽에 넣지 않고 브로커 내부의 임시 저장소에 넣어둔 뒤 시간이 되면 토픽으로 옮긴다

 

하나 이상의 특별 메시지 토픽을 활용해 임시 저장소로 활용할 수 있고, 계층적 타이밍 휠, 메시지 지연 전송 전용 메시지 큐를 활용해 구현할 수 있다

예약 전송 또한 지정된 시간에 소비자에게 메시지를 보내 지연 시스템과 유사하다