분산 메시지 큐
장점
Decoupling
•
메시지큐는 컴포넌트 사이의 tight coupling을 제거해서 각각의 컴포넌트들이 독립적으로 업데이트될 수 있게 한다
Scalability
•
컴포넌트들이 독립적으로 작동하기 때문에 트래픽에 따라 Producer와 Consumer를 확장하기 쉽다. 예를 들어 피크 시간대에 트래픽이 증가하면 단순히 Consumer 개수를 늘리는 형식으로 시스템을 확장할 수 있다.
Availability
•
만약 하나의 컴포넌트에 문제가 생기더라도 독립성이 보장되기 때문에 다른 컴포넌트들은 큐와 여전히 잘 상호작용할 수 있다.
Performance
•
메시지 큐는 비동기로 작동하기 때문에 성능이 좋다. Producer는 큐의 응답을 기다리지 않고 메시지를 보낼 수 있고, Consumer는 메시지를 처리할 수 있을 때 언제든지 메시지를 받아올 수 있다.
이벤트 스트리밍 플랫폼
이벤트 스트리밍 플랫폼은 러프하게 메시지 큐 + 부가 기능이라고 볼 수 있다. 대표적인 부가 기능은 다음과 같다
Data Retention
•
기존 메시지 큐는 메시지가 Consumer에 의해 소비되고 나면 사라진다.
•
이벤트 스트리밍 플랫폼의 경우 데이터를 디스크에 저장해서 추후 기록을 확인할 수 있다.
Multiple Consumption
•
기존 메시지 큐의 경우 하나의 메시지는 하나의 Consumer에 의해 소비된다.
•
이벤트 스트리밍 플랫폼의 경우 필요에 의해 하나의 메시지가 여러 번 소모될 수도 있다.
Consuming Order
•
기존 메시지 큐에서는 비동기 처리로 인해 메시지가 큐에 들어온 순서와 소모되는 순서가 같다는 보장이 없다
•
이벤트 스트리밍 플랫폼의 경우 필요에 의해 메시지의 소모 순서를 강제할 수 있다.
Step 1
Understand the Problem and Establish Design Scope
면접에서는 면접관과의 질문을 통해 시스템 디자인의 요구사항을 명확히 해야한다. 다음은 면접관과의 질문을 통해 도달한 합의점이다.
Functional Requirements
1.
메시지 큐는 한 번, 혹은 그 이상 소모될 수 있다.
2.
메시지는 2주동안 보관한다.
3.
메시지의 크기는 KB 단위이다.
4.
메시지의 Publish 순서와 Consume 순서가 같도록하는 기능을 추가한다.
5.
유저가 Data Delivery Semantics를 정할 수 있게 한다.
Non-functional Requirements
1.
유저가 높은 throughput과 low latency 중 선택할 수 있게 설계한다.
2.
확장성이 좋아야한다. 급작스럽게 메시지의 볼륨이 커져도 감당할 수 있어야한다.
3.
메시지 데이터를 보존할 수 있어야한다. 디스크에 기록을 보관하되, 복제본을 여러 노드에 보관해서 가용성을 높인다.
Step 2
Propose High-level Design by Get Buy-in
메시지 모델
Point-to-point Model
Point-to-point 모델은 기본적인 메시지 큐가 채택한 모델이다. 하나의 메시지는 하나의 Consumer에 의해 소모된다.
해당 모델에서 데이터가 보존되지 않는다. 이번 설계에서는 데이터가 2주 동안 보존되어야 하는 조건이 있기 때문에 해당 모델은 채택하지 않는다. (우리는 Data Persistence Layer가 필요하다)
Publish-subscribe Model
Pub Sub 모델에서는 Topic이라는 개념이 등장한다.
•
Topic: 메시지를 분류하기 위한 카테고리
Producer는 특정 토픽으로 메시지를 보내고(Publish) 해당 토픽을 구독(Subscribe)하는 모든 Consumer는 메시지를 받게 된다.
종합 모델
우리가 설계해야 하는 메시지 큐의 경우는 두 가지 모델의 기능을 모두 지원해야한다. 이를 위해 몇 가지 개념을 추가로 도입해야한다. 먼저 Consumer Group을 도입하면 다음과 같은 설계가 가능하다.
•
먼저, Consumer들은 Consumer Group을 이룬다. 각각의 Consumer Group은 특정 Topic을 구독할 수 있다(Pub-Sub Model). 따라서 위의 그림에서 Consumer Group 1, 2, 3은 모두 msgA를 받게 된다.
•
추가로, 하나의 Consumer Group은 여러 개의 Topic을 구독할 수 있다.
•
Consumer Group은 독자적인 Consuming offset을 보관한다.
•
Consumer Group에 메시지가 전달되면 그룹에 속하는 Consumer 중 단 하나의 Consumer만이 메시지를 받게 된다. 따라서 여기서는 Point-to-point 모델이 적용된다.
Partition
하나의 Topic이 담당하는 데이터의 볼륨이 너무 크다면 문제가 발생할 것이다. 따라서 하나의 토픽은 여러 개의 Partition으로 나눠서 관리한다. Partition은 Topic을 Sharding한 것이다.
•
Producer가 Publish한 메시지는 messageID에 의해 어느 파티션으로 들어갈 지 결정된다.
•
하나의 Partition은 그룹 내에서 하나의 Consumer에 의해서만 소비되어야 한다. 그렇지 않으면 순서를 보장할 수 없다.
특정 메시지가 어떤 Consumer로 보내질까?
이후 Topic을 Partition 단위로 구분하게 된다. 각각의 Consumer는 Topic을 구성하는 partition의 subset을 책임지게 된다. 담당이 정해지는 것은 이후 Partition Dispatch Plan에 대해 다루며 조금 얘기할 시간이 있다.
Q. 그렇다면 위의 그림에서 C3은 놀아야만 할까?
Q. 하나의 Producer가 Publish하는 메시지 A, B, C의 순서가 보장되어야 하는 요구사항이 있다. 어떻게 하지?
Q. 성능 향상을 위해 Partition 수와 Consumer 수를 어떻게 설정하는 게 효율적일까?
Broker
Broker는 파티션을 hold하는 물리적인 서버를 의미한다. 하나의 Topic을 구성하는 Partition들을 하나의 서버에 둔다면 가용성이 낮을 것이다. 따라서 파티션들은 복제되어 여기 저기 다른 서버에 나뉘어 저장되는데 이 때 여러 토픽의 여러 파티션을 저장하는 물리적인 서버 각각을 Broker라고 하는 것이다.
High Level Architecture
•
Coordination Service
◦
zookeeper나 etcd 등의 coordination service는 어떤 브로커가 살아있는 지에 대한 정보를 트래킹하고 클러스터 내에서 리더를 선출한다
◦
리더 브로커는 파티션 분배 플랜을 짜고 알리는 역할을 한다
•
Metadata Storage
◦
토픽에 대한 정보를 관리한다
•
State Storage
◦
브로커는 Consumer 상태에 대한 정보를 관리한다
Step 3
Design Deep Dive
데이터를 보존하면서 높은 Throughput을 달성하기 위해서 세 가지 요소를 채택할 수 있다.
1.
On-Disk Data Storage
2.
수정 불가한 메시지 데이터 구조
3.
Batching
Data Storage
어떤 데이터 저장소를 선택할 지 결정하기 위해서는 메시지 큐의 트래픽 패턴을 분석할 필요가 있다.
1.
Write heavy, read heavy
2.
Update나 Delete가 없음
3.
연속적인 read-write access (Predominantly sequential)
Database
먼저, 일반적인 데이터베이스의 사용을 가정해보자. 관계형 데이터베이스의 경우 Topic 테이블을 생성하고 각각의 row에 메시지를 저장할 수 있다. NoSQL의 경우 topic Collection을 만들고 메시지를 document 형태로 저장할 수 있다.
하지만 데이터베이스는 근본적으로 1번 조건 하에서 많은 트래픽을 감당하기 힘들다는 단점이 있다. 많은 경우 데이터베이스가 시스템의 Bottleneck이 될 것이다. 이 때 선택할 수 있는 방법이 WAL이다.
Write-ahead log (WAL)
WAL은 새로운 데이터가 기존의 데이터 뒤에 append만 되도록 강제하는 형식의 파일이다. Rotational Disk의 경우 용량이 크고, 연속적인 데이터 접근에 대한 처리 속도가 빠르므로 WAL을 선택하는 것이 좋다. 다음은 메시지 큐에 WAL이 적용되는 방식이다.
•
메시지는 partition의 끝에만 붙으며 증가하는 offset 값을 갖는다. 가장 단순한 값의 offset은 파일의 line number이다.
•
하나의 partition이 무한대로 커질 수 없으므로 일정 크기 이상으로 커지만 segment 단위로 나눈다.
•
하나의 segment만 active한 상태로 read/write이 가능하고, 오래 된 inactive segment는 read만 가능하다. Inactive segment의 경우 일정 시간이 지나면 삭제한다 (우리의 요구사항은 2주였다).
디스크 저장소의 성능
메시지 큐 데이터 구조
수정 불가능한 메시지 큐의 데이터구조 역시 high throughput의 핵심 요인으로 작용한다. 데이터를 변경하는 것은 매우 비싼 복사 과정을 동반한다. 일반적인 메시지 큐는 다음과 같은 값을 갖는다.
Message key
•
메시지는 hash(key) % numPartitions 의 값에 따라 어떤 파티션으로 할당될 지 결정된다.
•
키는 보통 노출되어서는 안 되는 비즈니스적인 정보를 담는다.
Message Value
•
plain text, compressed binary block 등의 payload
Others
•
Topic: 소속된 토픽의 이름
•
Partition: 소속된 partition의 ID
•
Offset: 파티션 내의 메시지의 위치
→ 위의 세 필드로 하나의 메시지를 특정할 수 있다.
•
Timestamp: 메시지가 저장된 시간
•
Size: 메시지의 크기
•
CRC: Cyclic Redundancy Check, 데이터의 무결성을 위한 값
tag
추가로, 메시지에 태그를 달아서 메시지 필터링에 사용될 수 있다.
Batching
배치는 성능 향상에 굉장히 중요한 요소로 Producer, Consumer, 그리고 Message Queue 모두에서 적용될 수 있다. 여기서는 Message Queue에서의 batching을 주로 살펴본다.
•
Batching의 가장 큰 장점은 값비싼 Network Round Trip을 줄여준다는 것이다.
•
Throughput과 Latency 사이의 Trade Off가 존재한다
Producer
기존의 Producer는 다음과 같은 과정을 통해 메시지 큐에 메시지를 보낸다.
1.
Router에 메시지를 보낸다. Router는 Metadata Storage에서 Replica Distribution Plan을 참조해 어떤 브로커로 메시지를 전송할 지 알 수 있다. 플랜은 라우팅 레이어에 캐싱된다.
2.
브로커의 리더 레플리카에 메시지가 저장된다.
3.
다른 레플리카는 리더 레플리카로부터 데이터를 받아 저장한다.
위의 설계는 다음과 같이 개선될 수 있다.
1.
앞서 설명한 Batch를 위한 Buffer를 둔다 → high throughput
2.
라우터를 프로듀에 둬서 네트워크 hop 비용을 감소한다 → low latency
Consumer
Consumer가 메시지를 받는 Push model, Pull model 두 가지 방법을 살펴보자. 각각의 장단점은 모두 서로가 서로를 모른다는 것에 기인한다.
Push model
Push model은 메시지 큐가 Consumer의 정보를 모른 채 Consumer에게 메시지를 push하는 모델이다.
•
장점: Low Latency. 메시지 큐는 메시지를 받자 마자 Consumer에게 push할 수 있다.
•
단점: Consumer의 처리 능력과 상관 없이 메시지를 보내기 때문에 Consumer는 과부화될 수 있다. 또한, 처리 능력이 다른 Consumer 들에 대한 개별적인 처리가 어렵다
Pull model
Pull model은 Consumer가 큐에서 메시지를 pull 해오는 모델이다. 우리는 Pull model을 선택한다.
•
장점: Consumer가 Consumption rate을 조절할 수 있다. Scale out도 편해지고, batch 처리도 용이해진다.
•
단점: 큐의 상태를 모르기 때문에 메시지가 없음에도 계속 pull 요청을 보내게 된다. 이는 long polling model로 어느 정도 완화할 수 있다.
Distribution Plan
이 부분은 나중에 자세히 정리할 필요가 있을 것 같다. 중요한 내용만 간단히 설명하면,
•
Coordinator와 Consumer 들은 지속적으로 heartbeat을 주고 받는다
•
필요시 Coordinator는 리더를 뽑는다 (Join, Leave, Crash of a Consumer)
•
선별된 리더 Consumer는 Partition Dispatch Plan을 짜서 Coordinator에게 알린다
•
Coordinator는 해당 플랜을 다른 Consumer들에게도 알린다
State Storage
메시지 큐 브로커에는 다음 역할을 하는 State Storage다
•
파티션 컨슈머 매핑 정보
•
각각 컨슈머에 대한 파티션 offset
State Storage의 data access pattern
•
빈번한 read, write 그렇지만 volume은 적음
•
빈번한 update, 그렇지만 delete는 적음
•
랜덤 read, write
•
데이터 일관성이 매우 중요하다
→ 이를 만족하기 위해 ZooKeeper 등의 KV store를 사용하도록 한다
Metadata Storage
•
파티션 수
•
데이터 리텐션 기간
•
레플리카 분산 정보
→ ZooKeeper
ZooKeeper
주키퍼는 분산 시스템에서 굉장히 유용하게 사용할 수 있는 계층적 키값 저장소이다. 우리의 설계에서는 ZooKeeper 하나로 Metadata Storage, State Storage, Coordination Service 모두 한 군데 모을 수 있다.
Kafka ZooKeeper
하지만 카프카는 offset storage를 ZooKeeper에서 Kafka 브로커로 옮겼다.
Replica
•
Replica Distribution Plan은 Coordinator의 도움을 받아 리더로 선정된 Broker가 짠다.
•
플랜은 Metadata storage에 저장되고, 모든 브로커가 알 수 있다.
In-Sync Replica (ISR)
•
ISR은 리더와 싱크된 레플리카를 의미한다.
•
replica.lag.max.messages = 4이면, 리더와 3개 이하로 메시지가 차이날 때 싱크 됐다고 할 수 있다.
•
몇 개의 레플리카가 in sync일 때 브로커가 프로듀서에게 ACK을 보낼 지 설정해서 performace와 durability 사이의 trade off를 결정할 수 있다
◦
ACK=all: 가장 강한 message durability
◦
ACK=1: low latency, 가끔 있는 message loss는 용납할 수 있는 경우
◦
ACK=0: metrics 수집, logging data 수집 등 볼륨이 크고, 종종 있는 data loss를 용납할 수 있는 경우
•
보통 리더가 다른 replica의 lag를 트래킹하여 ISR 리스트를 갖고 있는다. 자세한 알고리즘은 다음에 정리!
Scalability
Producer
•
Producer는 Consumer과 다르게 그룹이 없기 때문에 확장하기 쉽다. 단순히 instance를 추가, 제거하면 된다.
Consumer
•
Consumer Group은 독립적이기 때문에 단순히 추가하면 된다.
•
그룹 내의 Consumer들은 리밸런스 알고리즘에 의해 삭제, 추가될 수 있다.
Broker
브로커를 Fault-tolerant하게 하기 위해서는 몇 가지를 고려해야한다.
•
브로커를 추가, 삭제하고 레플리카를 재분산하면 된다
Partitions
•
Broker는 Producer, Consumer와 분리되어 있기 때문에 단순 축소, 확장할 수 있다.
•
Producer는 Broker와 통신 후 변경 사실을 알게 된다.
•
Consumer 역시 마찬가지며, Consumer rebalancing이 이루어진다.
Data Delivery Semantics
At Most Once
•
Producer: ACK=0
•
Consumer: 데이터 처리 전 offset 커밋
At Least Once
•
Producer: ACK=1 or all (최소 한 번 이상의 전송 보장)
•
Consumer: 데이터 처리 후 offset 커밋
Exactly Once
•
Producer
◦
ACK=all
◦
메시지에 PID를 부여하여 중복 제거 (Producer ID)
•
Consumer
◦
처리 전 DB에 처리 정보 확인
◦
처리 후 DB 저장 + commit