본문 바로가기

Server/Kafka

[Kafka] Producer 메시지 전송 방식

반응형

Kafka Producer는 메시지를 어디로 보낼까?

Kafka에서 Producer가 메시지를 보낼 때, 메시지가 아무 브로커에 전송되는 것이 아닙니다. Producer는 레코드가 들어갈 Topic-Partition의 Leader Replica를 보유한 브로커에게 Producer 요청을 보냅니다. 이 브로커를 Leader Broker라고 부릅니다.

조금 더 정확히는 Broker 자체가 Leader 라는 뜻은 아니며, Kafka에서 Leader와 Follower는 Broker의 고정 역할이 아니라, 특정 파티션 replica의 역할 입니다.

 

Kafka 토픽은 여러 파티션으로 나뉘고, 각 파티션은 복제를 위해 여러 개의 replica를 가질 수 있습니다. 예를 들어, order-topic에 파티션이 3개 있고, replication factor가 3이라고 한다면 다음과 같은 구조가 됩니다.

order-topic partition-1
  leader replica   on broker-1
  follower replica on broker-2
  follower replica on broker-3

order-topic partition-2
  follower replica on broker-1
  leader replica   on broker-2
  follower replica on broker-3

order-topic partition-3
  follower replica on broker-1
  follower replica on broker-2
  leader replica   on broker-3

 

위 구조에서 broker-1은 partition-1에서는 leader replica를 가지고 있지만, partition-2, partition-3 에서는 follower replica를 가지고 있습니다. 즉, broker-1 자체가 항상 Leader 인 것이 아닙니다.

 

정확한 표현은 "partition-1의 leader replica 는 broker-1에 있다" 이며, 실무에서는 "partition-1의 leader broker는 broker-1"이라고 표현합니다. 따라서, Leader Broker 라는 표현은 "특정 파티션의 leader replica를 가진 Broker"를 의미합니다.

Producer는 Leader Broker에게 Produce 요청을 보낸다

Producer의 네트워크 요청 대상은 Broker 인데, Producer가 아무 Broker에게 메시지를 보내는 것은 아닙니다. Producer는 meta-data를 기준으로 Record가 들어갈 파티션을 찾고, 그 파티션의 Leader Replica가 위치한 Broker에게 Produce 요청을 보냅니다.

Producer는 Follower Broker에게 메시지를 보내지 않는다

Producer는 Follower replica에게 직접 메시지를 보내지 않습니다. Producer는 앞서 이야기한 것 처럼 Leader Replica를 가진 Broker에게만 Produce 요청을 보냅니다.

 

이후 Follower Replica를 가진 Broker들이 Leader Replica가 있는 Broker로 부터 데이터를가져와 복제합니다. 즉, 복제는 Producer가 Follower에게 직접 보내는 방식이 아닌, Follower replica가 Leader replica로 부터 fetch 하는 방식으로 이루어집니다.

 

Producer는 "Leader Replica"를 보유한 Broker에 Produce 요청

  1. Leader Replica가 로그에 기록
  2. Follower Replica들이 Leader Replica로부터 fetch
  3. Follower Replica들이 자신의 로그에 복제

bootstrap.servers

Producer는 처음부터 각 파티션의 Leader Broker를 알고 있지 않습니다. Producer는 시작 시 bootstrap.servers에 지정된 Broker 중 연결 가능한 Broker에 접속합니다.

bootstrap.servers=broker-1:9092,broker-2:9092,broker-3:9092

위 설정은 "메시지를 항상 이 Broker에게 보내라"는 의미가 아니며, bootstrap.servers는 Producer가 Kafka 클러스터에 진입하기 위한 시작점입니다. Producer는 bootsrap broker에 접속해 Kafka Cluster의 meta-data를 받아옵니다.

Kafka Cluster의 meta-data
- 클러스터의 브로커 목록
- 토픽 목록
- 토픽의 파티션 목록
- 각 파티션의 leader replica가 위치한 브로커
- replica 정보
- ISR 정보

 

Producer는 이 메타데이터를 기반으로 실제 Produce 요청을 보낼 브로커를 결정합니다. 예를 들어 Producer가 broker-1을 통해 메타데이터를 받았더라도, order-topic partition-1의 leader replica가 broker-2에 있다면 실제 메시지는 broker-2로 전송됩니다. 따라서 bootstrap broker와 leader broker는 역할이 다릅니다.

구분 역할
Bootstrap Broker Producer가 처음 접속해 클러스터 meta-data를 받아오는 Broker
Leader Broker 특정 토픽-파티션의 Leader Replica를 보유한 Broker
Follower Broker 특정 토픽-파티션의 Follower Replica를 보유한 Broker

Producer 와 Consumer 간 메시지 전송

  • Producer 와 Consumer 간에는 Serialized(직렬화된) 메시지만 주고 받습니다.
  • Producer는 객체를 `Byte[]`로 직렬화 후 전송하고, Consumer는 `Byte[]`를 역직렬화 후 객체를 사용합니다.

Key 값을 가지지 않는 메시지 전송

Producer를 통해 메시지 전송 시, Partitioner를 통해 토픽의 어떤 파티션으로 전송되어야할지 미리 결정됩니다. Topic이 여러 Partition을 가질 경우, Kafka는 Topic 전체 기준의 메시지 순서는 보장하지 않습니다. 단, 각 Partition 내부에서는 offset 순서대로 읽히는 것이 보장됩니다.

  • Key 값을 가지는 메시지
    • 메시지 전송 시, 특정 파티션으로 고정되어 전송됩니다.
      • Partition 내에서는 순서가 보장됩니다.
  • Key 값을 갖지 않는 메시지
    • 메시지 전송 시, 파티션 분배 전략이 선택되어 메시지가 전송됩니다.
      • ⚠️ Round Robin (v2.4 이전의 기본 파티션 전략)
        • 최대한 메시지를 파티션에 균등하게 분배하는 전략, 메시지 배치를 순차적으로 다른 파티션으로 전송한다.
      • ✅ Sticky Partitioning (v2.4 부터 기본 파티션 전략)
        • Round Robin 성능을 개선하고자 특정 파티션으로 전송되는 하나의 배치에 메시지를 먼저 채워서 보내는 방식
        • 배치를 채우지 못하고 전송하거나, 배치를 채우는데 시간이 너무 오래 걸리는 문제가 개선된다.
          • batch.size, linger.ms 두 옵션으로 컨트롤 가능하다.

Kafka에서 전역 전송 순서를 보장하려면?

Kafka는 기본적으로 파티션 단위로 순서를 보장합니다. 즉, 하나의 파티션 안에서는 메시지가 offset 순서대로 저장되고 소비되지만, 여러 파티션에 흩어진 메시지 사이에는 전역 순서가 존재하지 않습니다. 따라서, Kafka에서 전역 순서를 보장하려면 결국 모든 메시지가 하나의 순서 위에 놓이도록 설계해야 합니다.

방법1) Topic을 단일 Partition으로 운영한다.

가장 단순한 방법으로는 Topic의 Partition 수를 1개로 두는 것 입니다. 이 경우 해당 Topic은 하나의 Partition 로그만 가지므로, Topic에 기록되는 메시지는 하나의 offset 순서를 갖습니다. Kafka가 제공하는 순서 보장 모델과 가장 잘 맞는 방식입니다.

장점

  • 구조가 단순합니다.
  • 메시가 하나의 Partition에만 쌓이므로, 전역 순서를 이해하기 쉽습니다. 별도의 재정렬 로직이나 시퀀스 관리 불필요합니다.

단점

  • 처리량과 확장성에 한계가 있습니다.
  • Kafka의 병렬 처리 단위는 Partition 이기 때문에, Partition이 1개라면, 같은 consumer group 안에서는 사실상 하나의 consumer만 의미 있게 메시지를 처리할 수 있습니다. consumer 인스턴스를 더 늘려도 같은 Partition을 동시에 나눠서 소비할 수는 없습니다.
즉, 전역 순서를 얻는 대신 Kafka의 핵심 장점인 Partition 기반 병렬성을 포기하는 방식입니다.

방법2) 여러 Partition이 있어도 모든 메시지를 같은 Partition으로 보낸다.

Topic의 Partition은 여러 개지만, 모든 메시지를 하나의 Partition으로만 보내는 방식도 있습니다.

  1. 모든 메시지에 같은 key를 사용
  2. 명시적으로 특정 partition을 지정
  3. Custom Partitioner를 사용해 특정 Partition으로 고정 라우팅

항상 같은 key(예: GLOBAL)를 사용하면, 같은 key를 가진 메시지는 같은 Partition으로 라우팅됩니다. 결과적으로 실제로 사용되는 Partition은 하나가 되므로 단일 Partition과 유사하게 순서를 유지할 수 있습니다.

장점

  • Topic의 Partition 수가 여러 개여도, 실제 메시지 흐름은 하나의 Partition에 모입니다.
  • 이미 여러 Partition으로 만들어진 Topic에서 특정 메시지 흐름만 순서를 보장하고 싶을 때 사용할 수 있습니다.

단점

  • 결과적으로 단일 Partition과 같은 한계를 갖습니다.
  • 모든 메시지가 하나의 Partition으로 몰리기 때문에 처리량은 Partition의 처리량에 제한되고, consumer 병렬성도 제한됩니다.
    • 즉, 나머지 Partition은 아무것도 하지 않고 놀게됩니다.
  • 이 방법에서 주의할 점은 파티션 수 변경입니다. 기본 key 기반 Partitioning은 key hash와 Partition 수를 기준으로 대상 Partition을 결정합니다. 따라서, 운영 중 Partition 수를 늘리면 같은 key라도 이전과 다른 Partition으로 라우팅될 수 있습니다. 라우팅 대상은 새로 추가된 Partition일 수도 있고, 기존에 있던 다른 Partition일 수도 있습니다. 결과적으로 동일 key의 메시지가 여러 파티션에 나뉘어 쌓이면, key 단위 순서 보장도 깨질 수 있습니다.
    • partition = hash(key) % partitionCount
방법2를 사용하려면 다음 중 하나를 반드시 지켜야 합니다.
1. Partition 수를 변경하지 않는다.
2. 특정 Partition 번호를 명시해서 보낸다.
3. Custom Partition로 항상 같은 Partition에 보내도록 고정한다.

방법3) Partition은 유지하고 전역 순서는 애플리케이션에서 보장한다

앞서 이야기한 것 처럼 Kafka는 기본적으로 Partition 내부의 순서만 보장합니다. 따라서, 여러 Partition에 메시지가 나뉘어 들어가면, Partition 사이의 전역 순서는 Kafka가 보장해주지 않습니다.

 

그럼에도 Partition을 N개 유지하면서 전역 순서가 필요하다면, 순서 보장을 애플리케이션에서 직접 처리해야합니다. 대표적인 방식은 모든 이벤트에 globalSequence를 붙여서 메시지를 발행하는 것입니다. 이러한 sequence는 Redis `INCR`, DB seqeuence, 별도 발급 서비스 등을 통해 만들 수 있습니다.

{
  "globalSequence": 1001,
  "eventType": "ORDER_CREATED",
  "payload": {
    "orderId": "ORD-1"
  }
}

 

메시지 발행과 처리 흐름을 정리하자면 다음과 같습니다.

  1. Producer가 전역 sequence를 발급받는다.
  2. 메시지에 globalSequence를 포함해 Kafka에 메시지를 뱅한다.
  3. Consumer는 여러 Partition에서 메시지를 읽는다.
  4. Consumer는 globalSequence 기준으로 메시지를 재정렬한 뒤 처리한다.

여기서 중요한 점은 globalSequence를 붙였다고 해서 전역 순서가 자동으로 보장되는 것은 아니라는 점입니다. 예를 들어 1001번 메시지보다 1002번 메시지가 먼저 Kafka에 도착할 수 있습니다.

Producer A → sequence=1001 발급
Producer B → sequence=1002 발급

Producer B → Kafka 발행 성공
Producer A → Kafka 발행 지연

 

이 경우 Consumer는 1002를 먼저 읽을 수 있습니다. 하지만 전역 순서를 지키려면 1002를 바로 처리하면 안 됩니다. 1001이 도착할 때까지 버퍼에 보관하고 기다려야 합니다.

현재 처리해야 할 sequence: 1001

1002 수신 → 버퍼에 보관
1003 수신 → 버퍼에 보관
1001 수신 → 1001, 1002, 1003 순서로 처리

 

이 방식은 파티션을 여러 개 유지할 수 있다는 장점이 있습니다. 즉, Kafka의 병렬성과 확장성을 어느 정도 가져갈 수 있습니다. 하지만 이 방법은 구현 복잡도가 높으며, 특히 다음과 같은 정책이 필요합니다.

  • 누락된 sequence를 얼마나 기다릴 것인가?
  • sequence가 비어 있으면 건너뛸 것인가?
  • 늦게 도착한 메시지는 어떻게 처리할 것인가?
  • offset commit은 언제 할 것인가?
  • 중복 처리는 어떻게 막을 것인가?

결국 globalSequence는 전역 순서를 보장해주는 값이 아니라, 전역 순서를 판단하기 위한 기준 값 입니다. 실제 순서 보장은 Consumer의 재정렬, 버퍼링, 누락 처리 정책까지 포함해야 완성됩니다. 따라서, 이 방식은 정말 모든 메시지의 전역 순서가 필요한 경우에만 선택하는 것이 좋습니다.

 

대부분의 실무 상황에서는 전체 전역 순서보다 orderId, userId, productId 같은 비즈니스 key 단위의 순서 보장만으로 충분한 경우가 많습니다. 이 경우에는 해당 key를 Kafka message key로 사용해 같은 key의 메시지를 같은 파티션으로 보내는 방식이 더 단순하고 Kafka의 설계와도 잘 맞습니다.

반응형