Post

Kafka 입문3

Kafka 입문3

파티셔너

  • 카프카의 토픽은 성능 향상을 위한 병렬 처리가 가능하도록 하기 위해 파티션으로 나뉘고, 최소 하나 또는 둘 이상의 파티션으로 구성됨
  • 프로듀서가 카프카로 전송한 메시지는 해당 토픽 내 각 파티션의 로그 세그먼트에 저장됨
  • 프로듀서는 토픽으로 메시지를 보낼 때 해당 토픽의 어느 파티션으로 메시지를 보내야 할지를 결정해야 하는데, 이때 사용하는 것이 바로 파티셔너
  • 프로듀서가 파티션을 결정하는 알고리즘은 메시지의 키를 해시 처리해 파티션을 구하는 방식을 사용함
    • 따라서 메시지의 키값이 동일하면 해당 메시지들은 모두 같은 파티션으로 전송됨
  • 예상치 못한 많은 양의 메시지가 카프키로 인입되는 경우, 카프카는 클라이언트의 처리량을 높이기 위해 토픽의 파티션을 늘릴 수 있는 기능을 제공함
    • 이때 파티션 수가 변경됨과 동시에, 메시지의 키와 매핑된 해시 테이블도 변경됨
    • 따라서 메시지의 키를 이용해 카프카로 메시지를 전송하는 경우, 관리자의 의도와는 다른 방식(다른 파티션으로 전송)으로 메시지 전송이 이뤄질 수 있으므로 되도록 파티션 수를 변경하지 않는 것을 권장함

라운드 로빈 전략

  • 키값을 지정하지 않는다면 키값은 null이 되고, 기본값인 라운드 로빈 알고리즘을 사용해 프로듀서는 목적지 토픽의 파티션들로 레코드들을 랜덤 전송함

예시 (배치/압축 효과를 얻지 못하는 상황)

토픽 1개, 파티션 3개(P0, P1, P2), 프로듀서가 record1~record5 전송 (key=null)

[STEP 1] Producer Record → Partitioner (Round Robin)

1
2
3
4
5
6
7
8
9
10
11
12
 Producer Records (key=null)
 ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐
 │ record1 ││ record2 ││ record3 ││ record4 ││ record5 │
 └────┬────┘└────┬────┘└────┬────┘└────┬────┘└────┬────┘
      │          │          │          │          │
      ▼          ▼          ▼          ▼          ▼
 ┌──────────────────────────────────────────────────────┐
 │             Partitioner (Round Robin)                │
 └──────────────────────────────────────────────────────┘
      │          │          │          │          │
      ▼          ▼          ▼          ▼          ▼
     P0         P1         P2         P0         P1

[STEP 2] 배치 전송을 위해 파티션별로 프로듀서 버퍼 메모리에 대기

1
2
3
4
5
6
7
8
 Producer Buffer Memory (batch.size / linger.ms 대기 중)

 ┌────────────────┐   ┌────────────────┐   ┌────────────────┐
 │ P0 batch       │   │ P1 batch       │   │ P2 batch       │
 │ [ record1 ]    │   │ [ record2 ]    │   │ [ record3 ]    │
 │ [ record4 ]    │   │ [ record5 ]    │   │ [  (empty) ]   │
 └────────────────┘   └────────────────┘   └────────────────┘
    2 records             2 records             1 record

레코드 5개가 파티션 3개로 흩어지면서, 하나의 배치 안에 모이는 레코드 수가 적어짐

문제점

  • 배치(batch)는 여러 레코드를 하나로 묶어 한 번의 요청으로 전송할 때 효율이 커지는데, 라운드 로빈으로 레코드가 파티션별로 잘게 분산되면 각 배치에 담기는 레코드 수가 줄어들어 배치 효과를 충분히 얻지 못함
  • 압축(compression) 역시 배치 단위로 묶인 데이터가 클수록 압축률이 좋아지는데, 배치 크기가 작으면 압축 효과도 함께 떨어짐
  • 결과적으로 큰 배치 하나 대신 작은 배치 여러 개를 자주 전송하게 되어 네트워크 요청 수가 늘고, 전체적인 처리량(throughput) 측면에서 비효율적인 상황이 발생함

스티키 파티셔닝 전략

  • 하나의 파티션에 레코드 수를 먼저 채워서 카프카로 빠르게 배치 전송하는 전략

예시 (배치 최소 레코드 수 충족 시 바로 전송)

토픽 1개, 파티션 3개(P0, P1, P2), 프로듀서가 record1~record3 전송 (key=null), 배치 최소 레코드 수 = 3

[STEP 1] Producer Record → Partitioner (Sticky, 하나의 파티션에 고정)

1
2
3
4
5
6
7
8
9
10
11
12
 Producer Records (key=null)
 ┌─────────┐┌─────────┐┌─────────┐
 │ record1 ││ record2 ││ record3 │
 └────┬────┘└────┬────┘└────┬────┘
      │          │          │
      ▼          ▼          ▼
 ┌──────────────────────────────────────────────┐
 │        Partitioner (Sticky → Partition P0)    │
 └──────────────────────────────────────────────┘
      │          │          │
      ▼          ▼          ▼
     P0         P0         P0

[STEP 2] 버퍼 메모리에서 배치 최소 레코드 수 충족 → 즉시 전송

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 Producer Buffer Memory - Partition P0 batch

 ┌────────────────────────┐
 │ [ record1 ]            │
 │ [ record2 ]            │
 │ [ record3 ]            │  ← 배치 최소 레코드 수(3개) 충족
 └────────────────────────┘
         3 records
              │
              │  linger.ms 대기 없이 즉시 전송
              ▼
        ┌────────────┐
        │   Kafka    │
        │   Broker   │
        │ (Partition │
        │     P0)    │
        └────────────┘

record1~3이 모두 같은 파티션(P0)에 고정되면서 배치가 빠르게 채워지고, 최소 레코드 수를 충족한 즉시 배치 전송이 이뤄짐

장점

  • 라운드 로빈처럼 레코드가 여러 파티션에 흩어지지 않고 한 파티션에 몰리기 때문에, 배치가 더 빨리 채워지고 배치/압축 효과를 온전히 얻을 수 있음
  • 배치 전송이 끝나면 스티키 파티셔너는 다음 배치를 위해 새로운 파티션(P1 등)을 다시 고정해 순환하므로, 특정 파티션에만 부하가 몰리는 문제도 방지됨
  • 라운드 로빈 전략에 비해 약 30% 이상 지연시간이 감소하고 프로듀서의 CPU 사용률도 줄어드는 효과를 얻을 수 있었음
  • 카프카로 전송하는 메시지의 순서가 그다지 중요하지 않은 경우에 스티키 파티셔닝 전략을 적용하는 것이 권장

프로듀서의 배치

  • 카프카 클라이언트인 프로듀서에서는 처리량을 높이기 위해 배치 전송을 권장함
    • 카프카에서는 토픽의 처리량을 높이기 위한 방법으로 토픽을 파티션으로 나눠 처리하듯이

프로듀서의 배치 구성도 이미지

배치 전송 옵션

  • buffer.memory
    • 카프카로 메시지들을 전송하기 위해 담아두는 프로듀서의 버퍼 메모리 옵션
    • 기본값은 32MB
  • batch.size
    • 배치 전송을 위해 메시지들을 묶는 단위를 설정하는 배치 크기 옵션
    • 기본값은 16KB로 설정되어 있음.
    • 이때 batch.size는 “정확히 이 크기를 채워야 전송”이 아니라 “최대 이만큼까지만 쌓겠다”는 상한선 개념임
      • 다음 레코드를 추가하면 batch.size를 초과하는 시점에 지금까지 쌓인 배치를 그대로 닫아서 전송하고, 그 레코드는 새 배치의 첫 레코드가 됨 → 대부분 batch.size보다 약간 작은 상태로 전송됨
      • linger.ms가 만료되면 batch.size를 채우지 못했어도 그 시점까지 쌓인 만큼만 전송되므로, 이 경우도 batch.size보다 작게 나감
      • 카프카는 레코드 하나를 쪼개서 여러 배치에 나눠 담지 않으므로, 레코드 하나의 크기 자체가 batch.size보다 크면 그 레코드 하나만으로 batch.size를 초과하는 배치가 만들어짐 (절대적인 크기 제한은 max.request.size, message.max.bytes가 별도로 담당)
  • linger.ms
    • 배치 전송을 위해 버퍼 메모리에서 대기하는 최대 대기시간을 설정하는 옵션
    • 단위는 밀리초(ms) 이며 기본값은 0임
    • 기본값이 0이므로 배치 전송을 위해 기다리지 않고 메시지들이 즉시 전송됨

무조건 배치 처리만 해야 하는 것은 아니며, 카프카를 사용하는 목적에 따라 처리량을 높일지, 아니면 지연 없는 전송을 해야 할지 선택을 해야 함

처리량을 높이는 경우

  • batch.size, linger.ms의 값을 크게 설정해야 함

지연 없는 전송이 목표인 경우

  • batch.size, linger.ms의 값을 작게 설정해야 함

메시지들의 높은 압축률을 선호하는 경우

  • gzip, zstd를 선택

낮은 지연시간을 선호하는 경우

  • lz4, snappy

중복없는 전송

멱등성 : 동일한 작업을 여러 번 수행하더라도 결과가 달라지지 않는 것을 의미

적어도 한 번 전송

  • 메시지 유실은 없지만, 동일한 메시지가 중복으로 전송될 수 있는 전송 방식
  • 프로듀서가 브로커로부터 ACK 응답을 받지 못하면, 실제로는 브로커에 메시지가 잘 저장됐더라도 재전송을 시도하기 때문에 중복이 발생함

예시 (ACK 유실로 인한 중복 전송)

[STEP 1] Producer가 record를 전송하고 ACK를 기다림

1
2
3
4
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record A    │ ── send(record A) ─▶ │  offset 0 : (empty)  │
 └─────────────┘                      └──────────────────────┘

[STEP 2] Broker는 record A를 정상적으로 저장하고 ACK를 응답

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record A    │                      │  offset 0 : record A │  ← 저장 완료
 │ (ACK 대기중) │ ◀── ACK(offset 0) ── │                      │
 └─────────────┘                      └──────────────────────┘

[STEP 3] 하지만 네트워크 문제로 ACK가 Producer에게 전달되지 못함

1
2
3
4
5
6
7
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record A    │        ✕ ACK 유실     │  offset 0 : record A │
 │ (ACK 못받음) │ ◀── ─ ─ ─ ─ ─ ─ ─ ── │                      │
 └─────────────┘                      └──────────────────────┘

  Producer는 ACK를 못 받았으므로 "전송 실패"로 판단

[STEP 4] Producer가 retry 정책에 따라 동일한 record A를 재전송

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record A    │ ── send(record A) ─▶ │  offset 0 : record A │
 │ (재전송)     │                      │  offset 1 : record A │  ← 동일한 메시지 중복 저장
 └─────────────┘                      └──────────────────────┘

[결과] 브로커 로그에 record A가 두 번 저장됨

1
2
3
4
5
6
7
8
 Broker (Partition P0)
 ┌────────────────────────────────────┐
 │  offset 0 : record A               │
 │  offset 1 : record A   ← 중복!     │
 └────────────────────────────────────┘

 ⚠️ Producer 입장에선 "적어도 한 번은 전달됨"을 보장받았지만,
    Broker 입장에선 동일한 메시지가 두 번 저장되는 중복이 발생함

즉, 브로커가 메시지를 받지 못한 상황이었다면 브로커는 처음으로 메시지를 저장하게 될 것이고, 브로커가 메시지를 저장하고 ACK만 전송하지 못한 상황이었다면 브로커에 중복 저장될 것

최대 한 번 전송

  • 메시지가 중복으로 전송되진 않지만, 유실될 수 있는 전송 방식
  • 브로커가 메시지를 저장하는 도중 문제가 발생해 ACK를 프로듀서에게 보내지 못하면, 프로듀서는 재전송을 하지 않고 다음 메시지로 넘어가기 때문에 해당 메시지는 그대로 유실됨

예시 (Broker의 ACK가 Producer에게 전달되지 못한 경우)

[STEP 1] Producer가 record B를 전송

1
2
3
4
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record B    │ ── send(record B) ─▶ │  offset 0 : (empty)  │
 └─────────────┘                      └──────────────────────┘

[STEP 2] Broker가 record B를 저장하는 도중 문제가 발생해 ACK를 보내지 못함

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record B    │        ✕ 저장 실패    │  offset 0 : (empty)  │
 │ (ACK 대기중) │ ◀── ─ ─ ─ ─ ─ ─ ─ ── │  (record B 저장 안 됨)│
 └─────────────┘                      └──────────────────────┘

[STEP 3] 최대 한 번 전송이므로 재전송 없이 다음 record C로 넘어감

1
2
3
4
5
6
7
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record C    │ ── send(record C) ─▶ │  offset 0 : record C │
 │ (재전송 없음)│                      │                      │
 └─────────────┘                      └──────────────────────┘

  Producer는 record B의 ACK를 못 받았지만 재전송하지 않고 그대로 넘어감

[결과] 브로커 로그에 record B가 끝내 존재하지 않음

1
2
3
4
5
6
 Broker (Partition P0)
 ┌────────────────────────────────────┐
 │  offset 0 : record C               │
 └────────────────────────────────────┘

 ⚠️ record B는 중복 없이 깔끔하지만, 재전송 없이 그대로 영구 유실됨

적어도 한 번 전송과 동일하게 ACK를 받지 못한 상황에서 출발하지만, 재전송을 하지 않기 때문에 중복은 막을 수 있는 대신 유실된 메시지를 복구할 방법이 없음

일부 메시지가 손실되더라도 높은 처리량을 필요로 하는 대량의 로그 수집이나 IOT 같은 환경에서 사용하곤 함

중복 없는 전송

  • 메시지 유실도, 중복도 없이 정확히 한 번만 전달되는 것을 목표로 하는 전송 방식
  • 프로듀서가 ACK 유실로 재전송을 하더라도 브로커가 이를 중복으로 판별할 수 있어야 하는데, 이를 가능하게 하는 것이 PID(Producer ID)시퀀스 번호(Sequence Number)
    • PID : 프로듀서가 초기화될 때 브로커로부터 발급받는 고유 식별자
    • 시퀀스 번호 : 프로듀서가 파티션별로 전송하는 각 레코드에 순차적으로 부여하는 번호 (0부터 시작)
    • 브로커는 파티션마다 (PID, 마지막으로 처리한 시퀀스 번호)메모리에 기록해두고, 이미 처리한 시퀀스 번호로 재요청이 오면 저장하지 않고 중복으로 판단함

예시 (PID와 시퀀스 번호를 메모리에 기록해 중복을 막는 과정)

[STEP 1] Producer가 초기화되며 Broker로부터 PID를 발급받음

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ (초기화 요청)│ ── InitProducerId ─▶ │                      │
 │             │ ◀──── PID=1000 ───── │                      │
 └─────────────┘                      └──────────────────────┘

[STEP 2] Producer가 record A를 (PID=1000, seq=0)과 함께 전송

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌──────────────────────┐             ┌──────────────────────┐
 │ record A              │             │  offset 0 : (empty) │
 │ PID=1000, seq=0       │ ── send ──▶ │                      │
 └──────────────────────┘             └──────────────────────┘

[STEP 3] Broker가 record A를 저장하고, PID·시퀀스 번호를 메모리에 기록

1
2
3
4
5
6
7
8
9
10
11
12
13
 Broker (Partition P0)
 ┌──────────────────────┐
 │ offset 0 : record A  │  ← 저장 완료
 └──────────────────────┘

 Broker 메모리 (Producer State per Partition)
 ┌─────────┬───────────┬────────────────┐
 │  PID    │ Partition │ Last Sequence  │
 ├─────────┼───────────┼────────────────┤
 │  1000   │    P0     │       0        │  ← 새로 기록됨
 └─────────┴───────────┴────────────────┘

 Broker → Producer : ACK(offset 0)

[STEP 4] 네트워크 문제로 ACK가 Producer에게 전달되지 못함

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌──────────────────────┐             ┌──────────────────────┐
 │ record A              │  ✕ ACK 유실 │ offset 0 : record A │
 │ (ACK 못받음)          │ ◀─ ─ ─ ─ ─  │                      │
 └──────────────────────┘             └──────────────────────┘

[STEP 5] Producer가 동일한 (PID=1000, seq=0)으로 record A를 재전송

1
2
3
4
5
 Producer                              Broker (Partition P0)
 ┌──────────────────────┐             ┌──────────────────────┐
 │ record A (재전송)     │             │ offset 0 : record A │
 │ PID=1000, seq=0       │ ── send ──▶ │                      │
 └──────────────────────┘             └──────────────────────┘

[STEP 6] Broker가 메모리에 기록된 시퀀스 번호와 비교 → 중복으로 판단

1
2
3
4
5
6
7
8
9
10
11
 Broker 메모리 (Producer State per Partition) 조회

 ┌─────────┬───────────┬────────────────┐
 │  PID    │ Partition │ Last Sequence  │
 ├─────────┼───────────┼────────────────┤
 │  1000   │    P0     │       0        │  ← 이미 처리된 seq=0과 동일
 └─────────┴───────────┴────────────────┘

 "PID=1000, seq=0은 이미 처리됨" → 중복 저장 없이 성공 ACK만 재응답

 Broker → Producer : ACK(offset 0)

[결과] 브로커 로그에는 record A가 단 한 번만 저장됨

1
2
3
4
5
6
 Broker (Partition P0)
 ┌────────────────────────────────────┐
 │  offset 0 : record A               │
 └────────────────────────────────────┘

 ✅ Producer는 재전송을 통해 결국 ACK를 받았고, Broker에는 중복 없이 한 번만 저장됨

Broker가 PID와 시퀀스 번호를 메모리에 기록해두었다가 재전송 요청을 비교하기 때문에, ACK 유실로 인한 재전송이 발생해도 중복 저장 없이 정확히 한 번 전송이 가능해짐

카프카에서는 메시지 비교 동작으로 발생하는 오버헤드를 최소화하기 위해 고민했으며 메시지에 단순한 숫자 필드만 추가하는 방법으로 구현

중복 없는 전송을 적용한 후 기존 대비 최대 약 20% 정도만 성능이 감소했다고 함 따라서 프로듀서 전송 성능에 그다지 민감하지 않은 상황에서 중복 없는 메시지 전송이 필요하다면 권장함

프로듀서 옵션설명
enable.idempotencetrue프로듀서가 중복 없는 전송을 허용할지 결정하는 옵션. 기본 값은 false이므로, 이 옵션을 설정하기 원한다면 true로 변경. true로 변경 시 다음에 나오는 옵션들도 반드시 변경해야 ConfigException이 발생하지 않음
max.in.flight.requests.per.connection1 ~ 5ACK를 받지 않은 상태에서 하나의 커넥션에서 보낼 수 있는 최대 요청 수. 기본 값은 5이며 5 이하로 설정해야 함
acksall프로듀서 acks와 관련된 옵션으로서, 기본값은 1이며 all로 설정해야 함
retries5ACK를 받지 못한 경우 재시도를 해야 하므로 0보다 큰 값으로 설정 해야함

정확히 한 번 전송

  • 트랜잭션과 같은 전체적인 프로세스 처리를 의미하며, 중복 없는 전송은 정확히 한 번 전송의 일부 기능이라 할 수 있음.

트랜잭션 코디네이터

  • 프로듀서에 의해 전송된 메시지를 관리하며, 커밋 또는 중단 등을 표기함
  • 트랜잭션 로그를 카프카의 내부 토픽인 _transaction_state에 저장함
    • 이 역시 토픽이므로 파티션 수와 리플리케이션 팩터 수가 존재하며, 브로커의 설정을 통해 관리자가 설정 할 수 있음
  • 프로듀서는 트랜잭션 관련 정보를 트랜잭션 코디네이터에게 알리고, 모든 정보의 로그는 트랜잭션 코디네이터가 직접 기록함
  • 정확히 한 번 전송을 이용해 전송된 메시지들이 카프카에 저장되면, 카프카의 메시지를 다루는 클라이언트들은 해당 메시지들이 정상적으로 커밋된 것인지 또는 실패한 것인지 식별할 수 있어야함
    • 이를 식별하기 위해, 컨트롤 메시지라고 불리는 특별한 타입의 메시지가 추가로 사용됨
    • 컨트롤 메시지는 오직 브로커와 클라이언트 통신에서만 사용됨

단계별 동작

  1. 트랜잭션 코디네이터 찾기
  • 프로듀서는 브로커에게 FindCoordinator Request를 보내서 트랜잭션 코디네이터의 위치를 찾음
  • 트랜잭션 코디네이터는 브로커에 위치함
  • 트랜잭션 코디네이터의 주 역할은 PID와 transactional.id를 매핑하고 해당 트랜잭션 전체를 관리함
    • 만약 트랜잭션 코디네이터가 존재하지 않는다면 신규 트랜잭션 코디네이터가 생성됨
  • _transaction_state 토픽의 파티션 번호는 transactional.id를 기반으로 해시하여 결정되고, 이 파티션의 리더가 있는 브로커가 트랜잭션 코디네이터 브로커로 최종 선정됨
    • 이는 transactional.id가 정확히 하나의 코디네이터만 갖고 있다는 의미와도 같음
1
2
3
4
5
 Producer                              Transaction Coordinator
 ┌─────────────┐                      ┌────────────────────────┐
 │             │ ── FindCoordinator ▶ │                        │
 │             │ ◀── Coordinator 위치 ─│  (TID 기반으로 결정됨)  │
 └─────────────┘                      └────────────────────────┘
  1. 프로듀서 초기화
  • 프로듀서는 initTransactions() 메소드를 이용해 트랜잭션 전송을 위한 InitPidRequeset를 트랜잭션 코디네이터로 보냄
  • 이때 TID(Transactional.id)가 설정된 경우에는 InitPidRequest와 함께 TID가 트랜잭션 코디네이터에게 전송됨
  • 트랜잭션 코디네이터는 TID, PID를 매핑하고 해당 정보를 트랜잭션 로그에 기록함
  • PID 에포크를 한 단계 올리는 동작을 하게 되고, PID 에포크가 올라감에 따라 이전의 동일한 PID와 이전 에포크에 대한 쓰기 요청은 무시됨
    • 에포크를 활용하는 이유는 신뢰성 있는 메시지 전송을 위함
1
2
3
4
5
6
7
8
9
 Producer                              Transaction Coordinator
 ┌──────────────────┐                 ┌────────────────────────┐
 │ TID 설정됨         │ ── InitPidRequest(TID) ▶ │              │
 │                   │                 │  TID ↔ PID 매핑 기록    │
 │                   │                 │  PID epoch += 1        │
 │ (PID, epoch 수신) │ ◀──── PID, epoch 응답 ── │  트랜잭션 로그 기록 │
 └──────────────────┘                 └────────────────────────┘

  epoch가 올라가면 이전 epoch로 도착하는 쓰기 요청은 전부 무시됨(좀비 프로듀서 차단)
  1. 트랜잭션 시작
  • 프로듀서는 beginTransaction() 메소드를 이용해 새로운 트랝개션의 시작을 알리게 됨
  • 프로듀서는 내부적으로 트랜잭션이 시작됐음을 기록하지만, 트랜잭션 코디네이터 관점에서는 첫 번째 레코드가 전송될 때까지 트랜잭션이 시작된 것은 아님
1
2
3
4
5
 Producer
 ┌────────────────────────────┐
 │ beginTransaction()         │
 │ → Producer 내부 상태만 기록  │   (Coordinator에는 아직 통보 안 됨)
 └────────────────────────────┘
  1. 트랜잭션 상태 주가
  • 프로듀서는 토픽 파티션 정보를 트랜잭션 코디네이터에게 전달하고, 트랜잭션 코디네이터는 해당 정보를 트랜잭션 로그에 기록함
  • TID와 파티션의 정보가 트랜잭션 로그에 기록되며, 트랜잭션 현재 상태를 Ongoing으로 표시함
  • 만약 트랜잭션 로그에 추가되는 첫 번째 파티션이라면, 트랜잭션 코디네이터는 해당 트랜잭션에 대한 타이머를 시작함
  • 기본값으로 1분 동안 트랜잭션 상태에 대한 업데이트가 없다면, 해당 트랜잭션은 실패로 처리함
1
2
3
4
5
6
7
 Producer                              Transaction Coordinator
 ┌──────────────────┐                 ┌────────────────────────┐
 │ 대상 파티션 정보    │ ── AddPartitionsToTxnRequest ▶ │        │
 └──────────────────┘                 │  트랜잭션 로그 기록      │
                                       │  상태 : Ongoing         │
                                       │  타이머 시작 (기본 1분)  │
                                       └────────────────────────┘
  1. 메시지 전송
  • 프로듀서는 대상 토픽의 파티션으로 메시지를 전송함
1
2
3
4
 Producer                              Broker (Partition P0)
 ┌─────────────┐                      ┌──────────────────────┐
 │ record A    │ ── send(record A) ─▶ │  offset 0 : record A │
 └─────────────┘                      └──────────────────────┘
  1. 트랜잭션 종료 요청
  • 메시지 전송을 완료한 프로듀서는 commit Transaction() 메소든 또는 abortTransaction() 메소드 중 하나를 반드시 호출해야함
  • 해당 메소드의 호출을 통해 트랜잭션이 완료됨을 트랜잭션 코디네이터에게 알림
  • 트랜잭션 코디네이터는 두 단계 커밋 과정을 시작하게 되며, 첫 번째 단계로 트랜잭션 로그에 해당 트랜잭션에 대한 prepareCommit 또는 prepareAbort를 기록함
1
2
3
4
5
6
7
 Producer                              Transaction Coordinator
 ┌──────────────────┐                 ┌────────────────────────┐
 │ commitTransaction()│ ── EndTxnRequest ▶ │                    │
 └──────────────────┘                 │  트랜잭션 로그 기록      │
                                       │  상태 : PrepareCommit   │
                                       │        (or PrepareAbort)│
                                       └────────────────────────┘
  1. 사용자 토픽에 표시 요청
  • 트랜잭션 코디네이터는 두 번째 단계로서 트랜잭션 로그에 기록된 토픽의 파티션에 트랜잭션 커밋 표시를 기록함
  • 여기서 기록하는 메시지가 바로 컨트롤 메시지
1
2
3
4
5
6
 Transaction Coordinator               Broker (Partition P0)
 ┌────────────────────────┐           ┌──────────────────────┐
 │ 상태 : PrepareCommit     │ ─ WriteTxnMarkers ▶ │           │
 └────────────────────────┘           │ offset 1 : 컨트롤 메시지│
                                       │ (Commit Marker)       │
                                       └──────────────────────┘
  1. 트랜잭션 완료
  • 트랜잭션 코디네이터는 완료됨이라고 트랜잭션 로그에 기록함
1
2
3
4
5
 Transaction Coordinator
 ┌────────────────────────┐
 │ 트랜잭션 로그 기록        │
 │ 상태 : Complete          │
 └────────────────────────┘

컨슈머는 컨트롤 메시지(Commit/Abort Marker)를 보고 해당 트랜잭션의 메시지를 읽어도 되는지 판단하며, isolation.level=read_committed로 설정하면 커밋된 메시지만 읽고 abort된 메시지는 걸러냄

TID, PID, epoch 정리

구분생성 시점변경 시점사용되는 곳
TID (transactional.id)프로듀서 애플리케이션 설정으로 개발자가 직접 지정변경되지 않음 (프로듀서가 재시작돼도 동일하게 유지)Producer ↔ Transaction Coordinator 간 통신 (FindCoordinator, AddPartitionsToTxnRequest, EndTxnRequest 등)
PID (Producer ID)특정 TID로 최초 initTransactions() 호출 시 코디네이터가 발급변경되지 않음 (같은 TID로 재초기화해도 기존 PID를 그대로 재사용)Producer ↔ Broker 간 실제 Produce 요청 (시퀀스 번호와 함께 실려감)
epochPID와 함께 최초 발급 시 0으로 시작같은 TID로 initTransactions()가 다시 호출될 때(=프로듀서 재시작)마다 +1Produce 요청에서 PID와 함께 실려가며, 브로커가 좀비 프로듀서의 이전 세대 요청을 걸러내는 기준이 됨
  • TID는 고정된 값: 개발자가 지정하고, 프로듀서가 재시작되어도 절대 바뀌지 않음. 트랜잭션 코디네이터가 “이게 같은 프로듀서인지”를 판별하는 조회 키 역할만 함
  • PID+epoch는 TID를 기준으로 파생되는 값: 최초 initTransactions() 호출 시 PID가 발급되고 epoch=0으로 시작하며, 하나의 프로듀서 세션이 수행하는 모든 트랜잭션(1번째, 2번째, …)은 동일한 PID+epoch를 그대로 재사용함
  • epoch는 트랜잭션 단위가 아니라 “재초기화” 단위로 증가: 같은 TID를 가진 프로듀서가 재시작되어 initTransactions()를 다시 호출할 때만 +1 되며, 이를 통해 이전 프로세스(좀비 프로듀서)가 뒤늦게 보내는 쓰기 요청을 무효화함
  • 실제 메시지 전송 경로(Produce 요청)에는 TID가 실리지 않음: Producer가 파티션 브로커로 메시지를 보낼 때는 PID+epoch+시퀀스 번호만 실려가고, TID는 오직 트랜잭션 상태를 관리하는 코디네이터와의 통신에서만 사용됨
This post is licensed under CC BY 4.0 by the author.

Trending Tags