본문 바로가기

Study

대규모 시스템 설계 기초 2 - 4장. 분산 메시지 큐

728x90

이번 장은 분산 메시지 큐 설계에 대해서 다루고 있습니다. 메시지 큐를 사용하면 아래와 같은 이득을 얻을 수 있습니다.

  • 결합도 완화 (decoupling): 컴포넌트 간 강한 결합이 사라져 독립적으로 갱신 가능합니다.
  • 규모 확장성 개선: Producer와 Consumer 구조를 통해 시스템 규모를 트래픽 부하에 맞게 독립적으로 늘릴 수 있습니다.
  • 가용성 개선: 시스템의 특정 컴포넌트에 장애가 발생해도 다른 컴포넌트는 큐와 계속 상호작용을 이어갈 수 있습니다.
  • 성능 개선: 비동기 통신이 가능하여 데이터가 프로세싱될 때까지 기다릴 필요가 없습니다.

그럼 이 책에서 소개하는 분산 메시지 큐 설계를 위한 기능 요구사항과 비기능 요구사항을 확인해 보겠습니다.

기능 요구사항

  • Producer는 메시지 큐에 메시지를 보낼 수 있어야 하고, Consumer는 메시지 큐를 통해 메시지를 수신할 수 있어야 합니다.
  • 메시지는 반복적으로 수신 가능하고, 단 한 번만 수신해도 괜찮습니다.
  • 오래된 이력 데이터는 삭제 가능합니다.
  • 메시지 크기는 KB 수준입니다.
  • 메시지는 생산된 순서대로 Consumer에게 전달해야 합니다.
  • 메시지 전달 방식은 at-least-once(최소 한 번), at-most-once(최대 한 번), exactly-once(정확히 한 번) 가운데 설정 가능합니다.

비기능 요구사항

  • 높은 대역폭과 낮은 전송 지연 가운데 하나를 설정으로 선택 가능하게 하는 기능
  • 규모 확장성
  • 지속성 및 내구성

전통적인 메시지 큐는 메시지 보관 문제를 중요하게 다루지 않습니다. 일정 기간 동안에만 메모리에 보관하는 수준이죠. 그리고, 메시지 전달 순서도 보장하지 않아서 생산된 순서와 소비되는 순서가 달라질 수도 있습니다. 하지만 요구사항에는 메시지 큐는 데이터를 길게 저장도 가능해야 하고, 순서도 보장해야 합니다. 

이 요구사항을 만족하는 분산 메시지 큐가 아파치 Kafka 입니다. 이번 장은 Kafka에 대한 설명이 대부분이어서 Kafka에 대한 정리라고 봐도 무방할 것 같습니다.


Producer와 Consumer

  • Producer는 메시지를 메시지 큐에 발행하는 역할을, Consumer는 해당 메시지 큐를 구독(subscribe)하고 메시지를 소비하는 역할을 합니다.
  • Producer와 Consumer는 Client의 역할을, 메시지 큐는 Server 역할을 한다고 생각할 수 있습니다. 클라이언트와 서버는 네트워크를 통해 통신합니다.
  • 대부분의 메시지 큐는 Consumer가 메시지를 소비할 때 Consumer에게 메시지를 Push 하는 모델보다는 Consumer가 메시지를 Pool 하는 모델을 제공합니다. 

메시지 모델

일대일 (point-to-point)

  • 전통적인 메시지 큐에서 흔한 모델
  • 전송된 메시지는 오직 한 Consumer만 소비 가능합니다. 즉, 어떤 Consumer가 메시지를 가져갔다는 사실을 알리면 (acknowledge), 해당 메시지는 큐에서 삭제됩니다. 즉, 데이터 보관을 지원하지 않습니다.

발행-구독 (publish-subscribe)

  • 토픽(topic) 개념을 도입하여 메시지를 주제별로 정리합니다. 각 topic은 메시지 큐 서비스 전반에 고유한 이름을 가집니다.
  • 메시지를 발행하고 소비할 때는 topic에 보내고 topic에서 받게 됩니다.

 

topic과 partition

 

메시지는 앞에서 설명했듯이, topic에 보관됩니다. 하지만, topic에 보관되는 데이터의 양이 커져서 서버 한대로 감당하기 힘든 상황이 발생할 수도 있습니다. 이를 해결하는 한 가지 방법이 partition(파티션)을 활용하는 것입니다. 

 

아래와 같이 topic을 여러 partition으로 분할하여 메시지를 모든 partition에 균등하게 나눠 보냅니다. 그리고 이 파티션을 관리하는 서버가 Broker입니다. topic의 용량을 확장하고자 할 때는 이 partition 개수를 늘리면 됩니다.

 

각 topic partition은 FIFO 큐처럼 동작해서, 같은 partition 안에서는 메시지 순서가 유지됩니다. 그리고, 각 partition 내에서 메시지 위치offset(오프셋) 이라고 합니다.

 

Topic을 구독하는 Consumer가 여럿이면, 각 consumer는 해당 topic을 구성하는 partition 일부를 담당하여 소비합니다. 이 consumer들은 해당 topic의 consumer group (소비자 그룹)이라고 합니다.

 

Consumer group

하나의 consumer group은 여러 topic을 구독할 수 있고, offset을 별도로 관리합니다. 같은 group 내의 consumer는 메시지를 병렬로 소비 가능합니다. 그리고 특정 partition의 메시지는 한 consumer group 안에서는 오직 한 consumer만 읽을 수 있도록 함으로써 한 partition의 메시지를 순서대로 소비할 수 있도록 보장하게 만들 수 있습니다. (이는 일대일(point-to-point) 모델에 수렴하게 됩니다.) 즉,  Kafka의 Consumer 그룹 내의 각 Consumer는 서로 다른 파티션을 소비하도록 설계되어 있습니다. 이를 통해 병렬 처리가 가능해지며, Kafka의 데이터 처리 성능이 극대화됩니다.

 

주의할 점은, Consumer 그룹 내의 Consumer 수가 Partition 수보다 많을 경우, 일부 Consumer는 할당받을 파티션이 없어 대기 상태로 남게 되어 메모리 낭비가 됩니다. 반대로, Consumer 수가 Partition 수보다 적으면 일부 Consumer가 여러 Partition을 할당받아 처리하게 됩니다.


Broker

Parition을 관리하고 유지하는 공간으로, Kafka Cluster 내의 서버를 Broker라고 부릅니다. Kafka는 여러 Broker로 이루어진 Cluster 환경에서 동작하며, 각 Broker는 데이터를 저장하고 분산 처리 및 복제를 관리합니다. 이를 통해높은 가용성과 데이터 안정성을 보장합니다. 각각의 기능에 대해 자세히 설명드리겠습니다.


출처: https://blog.devops.dev/apache-kafka-b7ea3b521232

1. 분산 처리 (Distributed Processing)

Kafka는 분산형 구조를 사용하여 높은 처리량을 제공합니다. Kafka Cluster는 여러 Broker로 구성되며, 각 Broker는 특정 Topic의 Partition을 나누어 저장합니다. 이 분산 처리 방식 덕분에 다음과 같은 장점이 있습니다.

  • 병렬 처리 가능: Topic의 파티션이 여러 Broker에 분산되어 있으므로, Producer와 Consumer가 병렬로 데이터를 전송하고 처리할 수 있습니다. 이로 인해 높은 처리량을 유지할 수 있습니다.
  • 확장성: 새로운 Broker를 추가하면 기존의 파티션을 재분배하여 전체 Cluster의 처리 용량을 확장할 수 있습니다. Kafka는 파티션을 기반으로 데이터의 분산과 재분배를 효율적으로 처리합니다.
  • 로드 밸런싱: 각 Broker에 할당된 파티션의 데이터 처리는 균등하게 분배되어, 특정 Broker에 과부하가 걸리지 않도록 합니다.

2. 복제 (Replication)

Kafka는 파티션 복제를 통해 데이터의 안정성고가용성을 보장합니다. 복제는 특정 파티션의 데이터를 여러 Broker에 복사하여 장애가 발생해도 데이터를 잃지 않도록 하는 방식입니다.

  • 리더와 팔로워: 각 파티션은 복제를 위해 리더팔로워로 나뉩니다. 리더 파티션은 데이터를 쓰고 읽는 주 역할을 담당하며, 팔로워 파티션은 리더의 데이터를 복제하여 저장합니다.
  • 장애 복구: 만약 리더가 있는 Broker가 장애를 일으키면, Kafka는 자동으로 팔로워 중 하나를 새로운 리더로 승격시킵니다. 이를 통해 서비스 중단 없이 데이터를 처리할 수 있습니다.
  • 복제 팩터(Replication Factor) : 각 Topic의 파티션마다 복제 팩터(Replication Factor)를 설정할 수 있으며, 이는 파티션이 몇 개의 Broker에 복제될지 결정합니다. 예를 들어, 복제 팩터가 3이라면 각 파티션이 3개의 Broker에 복제됩니다. 이 값이 높을수록 데이터 안정성은 증가하지만, 그만큼 저장 공간과 네트워크 트래픽이 증가합니다.

Kafka에서의 데이터 일관성 관리

Kafka는 ISR (In-Sync Replica)라는 메커니즘을 통해 데이터 일관성을 관리합니다. ISR은 현재 리더의 데이터와 동기화된 팔로워들을 나타내며, Kafka는 ISR에 포함된 팔로워에게만 데이터를 전송합니다. 이 방식으로 Kafka는 리더와 팔로워 간 데이터 일관성을 보장합니다.

 

이러한 구조를 통해 Kafka는 고가용성을 유지하면서도 데이터의 신뢰성을 확보할 수 있습니다. 분산 처리와 복제를 결합하여 대규모 데이터 환경에서도 높은 안정성과 확장성을 제공하는 것이 Kafka Broker의 중요한 역할입니다.

ISR에 대한 자세한 설명은 아래 접은 글에 첨부하였습니다.

더보기

ISR(In-Sync Replica)는 Apache Kafka에서 데이터의 일관성과 안정성을 보장하기 위해 사용하는 중요한 메커니즘입니다. ISR은 특정 파티션의 리더와 동기화된 팔로워 복제본 목록을 유지하는 역할을 합니다.

ISR의 구체적인 역할과 중요성은 다음과 같습니다.

1. 데이터 동기화 보장
- Kafka의 각 파티션에는 리더와 여러 팔로워가 있으며, 리더는 데이터를 읽고 쓰는 주체입니다. 리더 파티션에서 생성된 데이터는 팔로워 파티션으로 복제되어야 하는데, 이때 ISR 목록에 있는 팔로워들은 리더의 데이터와 완전히 동기화된 상태를 유지합니다.
- 만약 팔로워가 리더의 데이터를 따라잡지 못하고 특정 시간 이상 지연되면, 그 팔로워는 ISR에서 제외됩니다. 이를 통해 ISR은 현재 최신 데이터를 가진 안전한 팔로워들만을 포함하게 됩니다.

2. 고가용성 보장
- ISR 목록은 Kafka가 장애 발생 시 빠르게 리더를 교체할 수 있도록 돕습니다. 만약 현재 리더가 있는 Broker에 장애가 발생하면, Kafka는 ISR 목록에 포함된 팔로워 중 하나를 새로운 리더로 지정합니다.
- ISR을 통해 동기화된 팔로워만을 대상으로 리더 선출을 진행하기 때문에, 데이터 유실을 최소화하면서도 빠르게 리더를 교체할 수 있습니다.

3. 데이터 일관성 관리
- Kafka는 특정 파티션의 메시지가 ISR에 포함된 모든 복제본에 기록될 때까지 해당 메시지를 커밋하지 않는 설정(acks=all)을 제공합니다. 이를 통해 Producer는 모든 복제본이 메시지를 저장한 이후에만 성공 신호를 받게 되어 데이터 일관성을 보장할 수 있습니다.
- 이러한 일관성 메커니즘 덕분에 ISR에 속하지 않은, 즉 최신 상태가 아닌 팔로워들은 새로운 메시지 전송에서 제외되어 데이터 일관성이 손상되지 않도록 관리됩니다.

 

4. 복구 및 복제 효율성 최적화
- ISR은 Kafka가 불필요하게 리소스를 소비하지 않도록 합니다. ISR에 포함되지 않은 팔로워는 정상적으로 동기화될 때까지 Kafka의 복제 대상에서 제외되며, 정상 상태로 돌아오면 ISR에 다시 포함됩니다.
  
ISR은 리더와 동기화된 복제본 목록으로서, 데이터의 일관성, 안정성, 고가용성을 유지하는 핵심적인 역할을 합니다. ISR을 통해 Kafka는 장애 복구를 빠르게 수행하고, 데이터 손실 없이 서비스의 연속성을 보장할 수 있습니다.

 
즉, 어떤 한 노드의 장애로 메시지가 소실되는 것을 막기 위해 메시지는 여러 partition에 두고, 각 partition은 다시 여러 사본으로 복제되어 여러 broker에 분산 저장됩니다. 또한, 메시지는 리더로만 보내고 다른 단순 사본은 리더에서 메시지를 가져가 동기화하게 됩니다.

 .

Kafka의 ack 설정 옵션 (ISR)

Kafka에서 ack(acknowledgment)는 Broker로의 데이터의 전송 성공 여부를 Producer에게 확인하는 메커니즘입니다. ack 설정을 통해 메시지가 안전하게 저장되었는지 확인할 수 있으며, 데이터 안정성과 전송 속도 사이의 균형을 조절할 수 있습니다 Kafka의 ack 설정은 acks라는 파라미터로 제어하며, 주요 옵션은 다음과 같습니다.

  1. acks=0 (무응답 모드)
    • 설명: Producer가 메시지를 Kafka로 전송하고 난 후에 확인을 받지 않습니다. 즉, 메시지가 Broker에 도착했는지 여부를 확인하지 않으며, 전송 성공 여부에 관계없이 전송이 완료된 것으로 간주합니다.
    • 장점: 매우 빠른 전송 속도를 얻을 수 있습니다. Broker의 응답을 기다리지 않기 때문에 성능이 최대화됩니다.
    • 단점: 메시지 손실 위험이 큽니다. 메시지가 Broker에 도달하지 않거나 도중에 손실되더라도 이를 감지할 방법이 없습니다.
  2. acks=1 (리더 응답 모드)
    • 설명: Producer는 리더 Broker로부터 메시지가 성공적으로 수신되었다는 확인을 받으면 전송이 성공한 것으로 간주합니다. 하지만 ISR에 있는 팔로워들에게 복제되었는지 여부는 확인하지 않습니다.
    • 장점: 성능과 안정성 사이에서 균형을 유지합니다. 메시지가 리더에만 저장되었으면 전송이 성공한 것으로 처리하므로 빠르게 응답할 수 있습니다.
    • 단점: 리더에 장애가 발생하기 전에 팔로워에게 복제가 완료되지 않은 경우, 데이터 손실이 발생할 수 있습니다.
  3. acks=all (ISR 동기화 모드, 모든 복제본 응답 모드)
    • 설명: Producer는 리더뿐만 아니라 ISR에 포함된 모든 팔로워가 메시지를 복제한 후에야 성공적인 응답을 받습니다. 이는 메시지가 리더뿐만 아니라 동기화된 모든 복제본에 안전하게 저장되었음을 보장합니다.
    • 장점: 가장 높은 데이터 안정성을 제공합니다. 모든 복제본에 메시지가 저장되었음을 확인하므로, 리더가 장애를 겪더라도 데이터 손실이 최소화됩니다.
    • 단점: 성능이 상대적으로 느려질 수 있습니다. 모든 복제본이 동기화될 때까지 기다리므로 전송 시간이 증가합니다. 하지만 이는 안정성이 중요한 환경에서 필수적인 설정입니다.

메시지 저장소

메시지 큐의 트래픽 패턴은 아래와 같습니다.

  • 빈번한 순차적인 읽기와 쓰기
  • 갱신/삭제 연산을 발생하지 않음 (전통적 메시지 큐는 예외적으로, 메시지를 지속적으로 보관하지 않습니다.)

일반적인 DB라면 데이터 저장 요구사항을 맞출 수는 있지만, 이상적인 방법은 아닙니다. 읽기 연산과 쓰기 연산이 동시에 대규모로 빈번하게 발생하는 상황을 잘 처리하는 DB를 설계하기 어렵기 때문입니다. 

따라서, 이 책은 쓰기 우선 로그(Write-Ahead Log, WAL)를 소개하고 있습니다. 그리고 Apache Kafka는 실제로 이러한 방식으로 메시지를 저장합니다.

 

WAL은 새로운 항목이 추가되기만 하는 (append-only) 쓰기 파일입니다. 지속성을 보장해야 하는 메시지는 디스크에 WAL로 보관할 것을 추천하고 있습니다. 접근 패턴이 순차적일 때 디스크는 아주 좋은 성능을 보이고 큰 용량을 저렴한 가격에 제공하기 때문입니다.

새로운 메시지는 partition 꼬리 부분에 추가되며, offset은 점진적으로 증가하게 됩니다. 그리고 각 파티션은 일정한 크기의 로그 세그먼트로 나누어져 디스크에 저장됩니다. 이는 파티션의 데이터를 관리하기 쉽게 하며, 오래된 로그 세그먼트는 자동으로 삭제하거나 압축하여 디스크 사용 효율을 높일 수 있습니다.


Consumer Rebalancing

소비자 재조정(consumer rebalancing)은 어떤 consumer가 어떤 partition을 책임지는지 다시 정하는 프로세스입니다. 이는, 새로운 consumer가 합류하거나, 기존 consumer가 그룹을 떠나거나, 어떤 consumer에 장애가 발생하거나, partition들이 조정되는 경우 시작될 수 있습니다.

이 프로세스는 Coordinator(코디네이터)에 의해서 이루어 집니다. 코디네이터는 consumer rebalancing을 위해 consumer와 통신하는 Broker 노드입니다. Consumer로부터 오는 박동(heartbeat) 메시지를 살피고, 각 consumer의 partition 내 offset 정보를 관리합니다.


Zookeeper (주키퍼)

Zookeeper는 계층적 key-value 저장소 기능을 제공하는, Kafka Cluster의 메타데이터Broker를 관리하는 서비스입니다. 

 

메타데이터에는 topic의 partition 수, 메시지 보관 기간, 사본 배치 정보 등의 topic 설정이나 속성 정보를 보관합니다. 이는 자주 변경되지 않으며 양도 적습니다. 하지만 높은 일관성을 요구합니다. 

Zookeeper는 상태 데이터도 관리합니다. 여기에는 Consumer에 대한 partition의 배치 관계, 각 consumer group이 각 partition에서 마지막으로 가져간 메시지의 offset 등에 대한 정보가 있습니다.

Zookeeper는 Broker Cluster의 리더 선출 과정을 돕는 역할도 합니다.

 

즉, Kafka의 Broker, Topic 구성, Consumer Group, Offset 관리 등 중요한 메타데이터와 상태를 유지하며, Kafka Cluster의 가용성과 안정성을 보장합니다. 


메시지 전달 방식

분산 메시지 큐는 다양한 방식으로 메시지를 전달합니다.

at-most-once (최대 한 번)

메시지가 전달 되는 과정에서 소실되더라도 다시 전달되는 일이 없습니다. 즉, ACK=0 설정으로, producer는 topic에 비동기적으로 메시지를 보내지만, 메시지 전달이 실패해도 다시 시도하지 않습니다. consumer는 메시지를 읽은 후 바로 offset부터 갱신하고 메시지 처리 과정을 시작합니다. 즉, offset이 갱신된 후 consumer가 장애로 죽어도 메시지는 다시 소비될 수 없습니다.

 

이러한 전달 방식은 metric을 모니터링하는 등의 소량의 데이터 손실이 용이될 때 사용하는 것이 적절합니다.

 

at-least-once (최소 한 번)

같은 메시지가 한 번 이상 전달될 수 있어서 메시지 소실은 발생하지 않습니다. 즉, ACK=1 또는 ACK=all 설정으로, producer는 메시지가 broker에게 전달되었음을 반드시 확인하고, 메시지 전달이 실패하면 성공할 때까지 재시도할 것입니다. Consumer는 데이터를 읽고 성공적으로 처리한 후에 offset을 갱신합니다. 따라서, consumer가 장애로 죽어도 메시지가 소실되지 않지만, 메시지를 처리한 consumer가 offset을 갱신하지 못하고 죽었다가 다시 시작할 경우 메시지는 중복 처리될 것입니다.

 

데이터 중복이 큰 문제가 아닌 애플리케이션이나, consumer가 중복을 직접 제거할 수 있는 애플리케이션에 적합합니다.

 

exactly-once (정확히 한 번)

가장 까다로운 전송 방식으로 시스템의 성능 및 구현 복잡도 측면에서 큰 대가를 지불해야 합니다. 지불, 매매, 회계 등 금융 관련 응용에 적합합니다.


위의 기본적인 내용 이외에도 책에는 고급 기능으로 tag를 통해 메시지를 필터링하는 기능, 메시지를 지연 혹은 예약 전송하는 기능 등을 설명하고 있습니다. 또한 broker에 장애가 발생했을 때 어떻게 partition 리더가 재배치되는지, consumer가 장애가 발생했을 때 어떻게 consumer rebalancing이 발생하는지 구체적으로 설명하고 있습니다. 

 

이번 스터디를 통해 Kafka에 대해 추상적으로 알고 있었던 개념들을 구체적으로 배울 수 있어서 좋았습니다. 

728x90
반응형