최근 ‘가상 면접 사례로 배우는 대규모 시스템 설계 기초 2’의 '4장 분산 메시지 큐'를 읽으며
대규모 시스템 설계의 복잡성과 분산 메시지 큐의 개념들을 새롭게 이해하게 되었다. 분산 메시지 큐에 관한 이론을 배운 내용들을 정리하여 공유하고자 한다.
* 이 도서는 면접 상황을 가정하여 요구 사항을 기반으로 문제 해결 방안과 과정을 설명하고 있지만, 이번 포스팅에서는 그 과정 속에서 다뤄지는 개념과 내용에 더 집중하여 정리하였습니다.
현대 소프트웨어 아키텍처를 따르는 시스템은
- 잘 정의된 인터페이스를 경계로 나뉜 작고 독립적인 블록으로 구성한다.
- 이때 메시지 큐는 블록 사이의 통신과 조율을 담당한다.
메시지 큐의 장점
- 결합도 완화 : 컴포넌트 사이의 강한 결합이 사라지므로 각각을 독립적으로 갱신 가능하다.
- 규모 확장성 개선 : 생산자와 소비자의 규모를 트래픽 부하에 맞게 독립적으로 확장할 수 있다.
- 가용성 개선 : 시스템의 특정 컴포넌트에 장애가 발생하더라도 다른 컴포넌트는 큐와 계속 상호작용 할 수 있다.
- 성능 개선 : 메시지 큐를 사용하면 비동기 통신이 쉽게 가능하다. 생산자는 응답을 기다리지 않고 메시지를 보낼 수 있고, 소비자는 읽을 메시지가 있을 때만 해당 메시지를 소비하면 된다. 서로를 기다리지 않는다.
메시지 큐의 간략한 흐름
- 생산자는 메시지를 메시지 큐에 발행한다.
- 소비자는 큐를 구독하고 구독한 메시지를 소비한다,
- 메시지 큐는 생산자와 소비자 사이의 결합을 느슨하게 한다.
메시지 모델
일대일 모델
- 큐에 전송된 메시지는 오직 한 소비자만 가져갈 수 있다.
- 소비자가 많아도 각 메시지는 오직 한 소비자만 가져갈 수 있다.
- 어떤 소비자가 메시지를 가져갔다는 사실을 큐에 알리면 해당 메시지는 큐에서 삭제한다.
- 해당 모델은 데이터 보관을 지원하지 않는다.
발행-구독 모델
- 토픽은 메시지를 주제별로 정리하는 데 사용된다.
- 메시지를 보내고 받을 때는 토픽에 보내고 받게 된다.
- 토픽에 전달된 메시지는 해당 토픽을 구독하고 있는 모든 소비자에게 전달된다.
토픽에 보관되는 데이터 양이 많아지면?
- 토픽을 여러 파티션으로 분할한 다음에 메시지를 모든 파티션에 균등하게 나눠 보낸다.
- 파티션은 토픽에 보낼 메시지의 작은 부분집합으로 생각하면 좋다.
- 파티션은 메시지 큐 클러스터 내의 서버에 고르게 분산 배치한다.
토픽, 파티션, 브로커, 소비자 그룹
- 파티션을 유지하는 서버를 브로커 라고 한다.
- 토픽 파티션은 FIFO 큐처럼 동작하여 메시지 순서가 유지되며, 파티션 내에서 메시지의 위치를 offset이라고 한다.
- 생산자가 보낸 메시지는 해당 토픽의 파티션 가운데 하나로 보내진다.
- 토픽을 구독하는 소비자는 하나 이상의 파티션에서 데이터를 가져올 수 있다.
- 토픽을 구독하는 소비자가 여럿인 경우, 각 구독자는 해당 토픽을 구성하는 파티션의 일부를 담당한다. 이 소비자들을 해당 토픽의 소비자 그룹이라고 한다.
3단계 : 상세 설계
4장에서는 분산 메시지큐를 설계하기 위해 데이터의 장기 보관 요구사항을 만족하면서 높은 대역폭을 제공하기 위한 3가지 결정을 한다.
- 회전 디스크의 높은 순차 탐색 성능 + 디스크 캐시 전략
- 메시지가 생산자로부터 소비자에게 전달된 순간까지 아무 수정 없이도 전송이 가능한 메시지 자료 구조 설계
- 일괄 처리
일괄 처리
- 운영체제로 하여금 여러 메시지를 한 번의 네트워크 요청으로 전송할 수 있도록 하여 네트워크 비용을 제거할 수 있다.
- 브로커가 여러 메시지를 한 번에 로그에 기록하면 더 큰 규모의 순차 쓰기 연산이 발생하고 운영체제가 관리하는 디스크 캐시에서 더 큰 규모의 연속된 공간을 점유하게 된다. 그 결과 더 높은 디스크 접근 대역폭을 달성할 수 있다.
생산자 측 작업 흐름
생산자는 토픽-A의 파티션-1로 메시지를 보내고자 한다.
라우팅 계층은 적절한 브로커에 메시지를 보내는 역할을 한다.
- 생산자는 메시지를 라우팅 계층으로 보낸다.
- 라우팅 계층은 메타데이터 저장소에서 사본 분산 계획을 읽어 자기 캐시에 보관한다. 메시지가 도착하면 라우팅 계층은 파티션-1 리더 사본으로 보낸다.
- 리더 사본은 우선 메시지를 받고 해당 리더를 따르는 다른 사본은 해당 리더로부터 데이터를 받아 동기화한다.
- 충분한 수의 사본이 동기화되면 리더는 데이터를 디스크에 기록한다. 데이터가 소비자로부터 소비가능한 상태가 되는 것은 바로 이 시점이다.
라우팅 계층을 도입하면 거쳐야 할 네트워크 노드가 하나 더 늘어나게 되므로 오버헤드가 발생하여 네트워크 전송 지연이 늘어난다.
라우팅 계층을 생산자 내부로 편입시키고 버퍼를 도입한다. 생산자 클라이언트 라이브러리의 일부로 생산자에 설치한다.
- 네트워크를 거칠 필요가 줄어들기 때문에 전송 지연도 줄어든다.
- 생산자는 메시지를 어느 파티션에 보낼지 결정하는 자신만의 로직을 가질 수 있다.
- 전송할 메시지를 버퍼 메모리에 저장했다가 목적지로 일괄 전송하여 대역폭을 높일 수 있다.
얼마나 많은 메시지를 일괄 처리하면 좋을까?
- 대역폭과 응답 지연 사이의 타협점을 찾아야 한다.
- 전송할 메시지의 양을 늘리면 대역폭은 늘어나지만 응답 속도는 느려진다. 일괄 처리가 가능할 양의 메시지가 쌓이길 기다려야 하기 때문이다.
- 양을 줄이면 메시지는 더 빨리 보낼 수 있어 지연은 줄어들지만 대역폭은 손해를 본다.
소비자 측 작업 흐름
소비자는 특정 파티션의 오프셋을 주고 해당 위치에서부터 이벤트를 묶어 가져온다.
이때, 브로커가 데이터를 소비자에게 보낼 것인지(푸시), 소비자가 브로커에서 가져갈 것인지(풀) 생각해 볼 수 있다.
푸시 모델
- 장점
- 낮은 지연 : 브로커는 메시지를 받는 즉시 소비자에게 보낼 수 있다.
- 단점
- 소비자가 메시지를 처리하는 속도가 생산자가 메시지를 만드는 속도보다 느리면 소비자에게 큰 부하가 걸릴 가능성이 높다.
- 생산자가 데이터 전송 속도를 좌우하므로, 소비자는 항상 그에 맞는 처리가 가능한 컴퓨팅 자원을 준비해 두어야 한다.
풀 모델
- 장점
- 메시지를 소비하는 속도는 소비자가 알아서 결정한다. (어떤 소비자는 실시간으로 어떤 소비자는 일괄로)
- 메시지를 소비하는 속도가 생산하는 속도보다 느려지면 소비자를 늘려 해결할 수도 있고 생산 속도를 따라잡을 때까지 기다려도 된다.
- 일괄 처리에 적합하다.
- 단점
- 브로커에 메시지가 없어도 소비자는 계속 데이터를 끌어가려 시도할 것이다.
- 소비자 측 컴퓨팅 자원 낭비를 하게 된다.
- 롤 폴링 모드를 지원한다. 가져갈 메시지가 없더라도 일정 시간은 기다리도록 하는 것
대부분은 메시지 모델은 풀 모델을 지원한다.
소비자 재조정
- 어떤 소비자가 어떤 파티션을 책임지는지 다시 정하는 프로세스
- 새로운 소비자가 합류, 기존 소비자가 그룹을 떠나거나, 어떤 소비자에 장애 발생, 파티션들이 조정되는 경우 시작될 수 있다.
코디네이터
- 소비자 재조정을 위해 소비자들과 통신하는 브로커 노드
- 소비자로부터 오는 박동 메시지를 살피고, 각 소비자의 파티션 내 오프셋 정보를 관리한다.
- 자신에 연결한 소비자 목록을 유지하고, 이 목록에 변화가 생기면 코디네이터는 해당 그룹의 새 리더를 선출한다.
- 새 리더는 새 파티션 배치 계획을 만들고 코디네이터에게 전달한다. 해당 계획을 그룹 내 다른 모든 소비자 그룹에게 알린다.
분산 시스템에서는 소비자는 네트워크 이슈를 비롯해 다양한 장애를 겪을 수 있다. 코디네이터 관점에서는 소비자에게 발생한 장애는 박동 신호가 사라지는 현상을 통해 감지할 수 있다. 감지하면 재조정 프로세스를 시작하여 파티션을 재배치한다.
복제
분산 시스템에서 하드웨어 장애는 흔한 일이며, 디스크에 손상이나 영구적 장애가 발생하면 데이터는 사라진다. 복제는 이러한 문제를 해결하고 높은 가용성을 보장해 준다.
- 각 파티션은 3개의 사본을 갖고, 이 사본들은 서로 다른 브로커 노드에 분산되어 있다.
- 짙은 색의 파티션이 리더이고, 나머지는 단순 사본이다.
- 생성자는 파티션에 메시지를 보낼 때 리더에게만 보낸다. 다른 사본은 리더에서 새 메시지를 지속적으로 가져와 동기화한다.
- 메시지를 완전히 동기화 한 사본의 개수가 지정한 임계값을 넘으면 리더는 생산자에게 메시지를 잘 받았다는 응답을 보낸다.
임계값이 넘었다. 동기화가 되었다는 기준은 replica.lag.max.message로 설정할 수 있다.
4로 설정되어 있다면, 단순 사본에 보관된 메시지 개수와 리더 사이의 차이가 3이라면 해당 사본은 동기화된 사본(ISR)이다.
ISR는 성능과 영속성 사이의 트레이드 오프점이다.
- 생산자가 보낸 어떤 메시지도 소실하지 않는 가장 안전한 방법은 생산자에게 메시지를 받았다는 응답을 보내기 전에 모든 사본을 동기화하는 것이다.
- 하지만 어느 사본 하나라도 동기화를 신속하게 처리하지 못하게 되면 파티션 전부가 느려지거나 아예 못 쓰게 되는 일이 벌어진다.
ACK=all
생산자는 모든 ISR이 메시지를 수신한 뒤에 ACK 응답을 받는다. 영속성 측면에서는 좋지만 느린 ISR의 응답을 기다려야 하는 이슈가 있을 수 있다.
ACK=1
생산자는 리더가 메시지를 저장하고 나면 바로 ACK 응답을 받는다. 동기화를 기다리지 않아 응답 지연은 개선되지만, ACK 보낸 직후 리더에 장애가 발생하면 메시지는 다른 사본에 반영되지 못하였으므로 복구할 길이 없어진다.
ACK=0
생산자는 보낸 메시지에 대한 수신 확인 메시지를 기다리지 않고 계속 메시지를 전송하며 어떤 재시도도 하지 않는다. 낮은 응답 지연을 달성할 수 있다. 지표 수집이나 데이터 로깅 등 처리해야 하는 메시지 양이 많고 때로 데이터 손실이 발생해도 상관없는 경우에 좋다.
주요 시스템 컴포넌트의 규모 확장성
생산자
새로운 생산자를 추가하거나 삭제함으로써 쉽게 확장/축소 가능하다.
소비자
소비자 그룹은 서로 독립적이므로 새 소비자 그룹은 쉽게 추가하고 삭제할 수 있다. 같은 소비자 그룹 내의 소비자 추가/삭제가 되는 경우 재조정 메커니즘이 맡아 처리한다.
브로커
브로커의 결합 내성
- 메시지가 성공적으로 합의되었다고 판단하려면 얼마나 많은 사본에 메시지를 저장해야 하는가, 수치가 높으면 높을수록 안전하지만 응답 지연이 늦어진다. 응답 지연과 안전성 사이에 균형을 찾을 필요가 있다.
- 사본은 같은 브로커에 두면 안 된다.
- 사본은 여러 데이터 센터에 분산하는 것이 안전하다. 하지만 데이터 동기화 때문에 응답 지연과 비용이 늘어난다.
브로커 노드가 추가되거나 삭제될 때 사본을 재배치하는 것이다. 혹은 브로커 컨트롤러는 한시적으로 시스템에 설정된 사본 수보다 많은 사본을 허용하도록 하는 것이다. 새로 추가된 브로커 노드가 기존 브로커 상태를 따라잡고 나면 더 이상 필요 없는 노드는 제거한다.
파티션
토픽의 규모를 늘리거나, 대역폭을 조정하거나, 가용성과 대역폭 사이의 균형을 맞추는 등의 운영상 이유로 파티션의 수를 조정해야 하는 일이 있을 수 있다. 생산자는 브로커와 통신할 때 그 사실을 통지받으며, 소비자는 재조정을 시행한다.
파티션 추가
- 지속적으로 보관된 메시지는 여전히 기존 파티션에 존재하며 해당 데이터는 이동하지 않는다.
- 새로운 파티션이 추가되면 그 이후 오는 메시지는 3개 파티션 전부에 지속적으로 보관된다.
- 파티션을 늘리면 간단히 토픽의 규모를 늘릴 수 있다.
파티션 제거
- 파티션 추가는 간단한 반면 파티션 제거는 좀 더 까다롭다.
- 파티션-3을 퇴역하면, 새로운 메시지는 나머지 파티션에 저장한다.
- 해당 파티션의 데이터를 읽고 있는 소비자가 있을 수 있기 때문에, 퇴역된 파티션은 바로 제거되지 않고 일정 시간 유지한다.
- 따라서 파티션을 줄여도 저장 용량은 신속하게 늘어나지 않는다.
- 파티션 퇴역 후 실제로 제거가 이루어지는 시점까지 생산자는 메시지를 남은 두 파티션으로만 보내지만 소비자는 세 파티션 모두에서 메시지를 읽는다.
- 실제로 파티션이 제거되는 시점이 오면 생산자 그룹은 재조정 작업을 게시한다.
Apache Kafka는 partition number을 줄이는 것을 지원하지 않는다. Topic에 대한 data 처리량을 늘리기 위해 partition을 쉽게 늘릴 수 있지만, partition number을 줄이는 것 즉 partition 삭제에 대한 정책은 지원하지 않는다.
메시지 전달 방식
최대 한 번
- 메시지가 전달 과정에서 소실되더라도 다시 전달되는 일은 없다.
- 생산자는 토픽에 비동기적으로 메시지를 보내고 수신 응답을 기다리지 않는다. (ACK=0)
- 메시지 전달이 실패해도 다시 시도하지 않는다.
- 소비자는 메시지를 읽고 처리하기 전에 오프셋부터 갱신한다.
- 오프셋이 갱신된 직후에 소비자가 장애로 죽으면 메시지는 다시 소비될 수 없다.
- 지표 모니터링 등, 소량의 데이터 손실은 감수할 수 있는 애플리케이션에 적합하다.
최소 한 번
- 같은 메시지가 한 번 이상 전달될 수는 있으나 메시지 소실은 발생하지 않는 전달 방식이다.
- 생산자는 메시지를 동기적/비동기적으로 보낼 수 있다. (ACK=1 또는 ACK=all)
- 메시지가 브로커에 전달되었음을 반드시 확인한다. 실패하거나 타임아웃이 발생하면 재시도를 한다.
- 소비자는 데이터를 성공적으로 처리한 뒤에만 오프셋을 갱신한다.
- 메시지를 처리한 소비자가 미처 오프셋을 갱신하지 못하고 죽었다가 다시 시작되면 메시지는 중복 처리될 것이다.
- 따라서 브로커나 소비자에게 한 번 이상 전달될 수 있다.
- 데이터 중복이 큰 문제가 아닌 애플리케이션이나 소비자가 중복을 직접 제거할 수 있는 애플리케이션에서는 충분히 괜찮은 전송 방식이다.
정확히 한 번
- 지불, 매매, 회계 등 금융 관련 응용에는 이 전송 방식이 적합하다.
- 중복을 허용하지 않는다.
메시지 필터링
- 소비자가 일단 모든 메시지를 받아 필요 없는 메시지는 버리는 방법은 유연성은 높지만 불필요한 트래픽이 발생한다.
- 브로커에서 메시지를 필터링하여 소비자는 원하는 메시지만 받을 수 있도록 하는 것이다.
- 필터링을 하기 위해 복호화, 역직렬화는 기능은 브로커의 성능을 저하하게 한다.
- 브로커에 구현할 필터링 로직은 메시지의 내용을 추출하면 안 된다.
- 메시지의 메타데이터 영역에 두어 효율적으로 읽어갈 수 있도록 해야 한다.
- 태그 필드가 있으면 브로커는 해당 필드를 활용해 메시지를 필터링할 수 있다.
- 메시지마다 태그를 두면 소비자는 어떤 태그를 가진 메시지를 구독할지 지정할 수 있다.
메시지의 지연 전송 및 예약 전송
- 소비자에게 보낼 메시지를 일정 시간만큼 지연시켜야 하는 일이 있다.
- 이런 메시지는 토픽에 바로 저장하지 않고 브로커 내부의 임시 저장소에 두었다가 시간이 되면 토픽으로 옮긴다.
끄읕.!
'정리' 카테고리의 다른 글
[Redis] redis.conf 기준 Redis 백업 방법 정리 (RDB vs AOF) (0) | 2024.10.28 |
---|---|
[Redis] Redis 캐싱 전략 패턴 정리 (0) | 2024.10.20 |
[친절한 SQL 튜닝] NL 조인, 소트 머지 조인, 해시 조인 (2) | 2024.06.16 |
[친절한 SQL 튜닝] 인덱스 튜닝 (2) : 인덱스 스캔 효율화, 인덱스 설계 (0) | 2024.06.13 |
[친절한 SQL 튜닝] 인덱스 튜닝 (1) 테이블 액세스 최소화, 부분범위 처리 활용 (1) | 2024.05.05 |