백엔드 데이터 인프라 112편. Kafka Transaction Protocol — EOS (Exactly-Once Semantics) 의 내부 메커니즘. Transactional Coordinator·Transaction Log·Producer Fencing·Two-Phase Commit·Control Batch 같은 깊은 구현까지 풀어쓴 학습 노트. Part 5-7 Internals 마무리.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 112편이에요. Part 5-7 Internals 의 마지막 글. 111편 까지 rebalance 까지 잡았다면, 이번 112편은 Kafka 의 가장 복잡한 영역 — Transaction Protocol (EOS, Exactly-Once Semantics 의 내부).
Transaction Protocol이 어렵게 느껴지는 이유
88편 delivery semantics 에서 exactly-once 를 짚었어요. 거기서 "이게 어떻게 작동하나" 의 내부 메커니즘 이 이 글의 주제.
Kafka 의 transaction 이 분산 시스템에서도 가장 까다로운 영역에 속하는 건 세 가지가 한꺼번에 얽혀서 그래요. 우선 여러 topic·partition 에 걸친 atomic (전부 성공 아니면 전부 실패) 작업을 묶어야 하는데, 단일 partition atomic 은 단순해도 multi-partition 으로 넘어가면 단숨에 복잡해져요. 거기에 Producer Failure 시나리오 — Producer 가 죽거나 재시작되거나 zombie (좀비처럼 살아 돌아온 옛 인스턴스) 가 되는 exotic case 처리까지 들어가요. 마지막으로 Consumer 의 read_committed 동작 — commit/abort marker 가 도착하기 전에 메시지가 보이느냐 안 보이느냐를 정해야 해요.
이 글에서 Transaction 의 모든 부품 정리.
핵심 부품
Transactional Producer
↓ transactional.id
[Transactional Coordinator (broker)]
↓ 메타데이터 저장
[__transaction_state topic (internal)]
↓
│ commit/abort marker write
↓
[User topics]
↑
│ read with isolation.level=read_committed
[Consumer]
3 부품:
- Transactional Coordinator — broker 안 component
- Transaction Log =
__transaction_stateinternal topic - Control Batch (마커) — 110편에서 본 special batch
1. Transactional Coordinator
각 broker 가 coordinator 역할 가능. 특정 transactional.id (producer 식별 키) 가 특정 coordinator 에 매핑돼요 (consistent hashing, 키를 해시 링에 분산).
역할
- Producer 의 transaction state 추적
- Two-Phase Commit (두 단계로 나눠 커밋) 조율
- Producer Fencing 관리
- Transaction timeout 감지
Producer 가 coordinator 찾기
Producer → FindCoordinatorRequest(transactional.id)
Broker → 해당 ID 의 coordinator broker 알려줌
Producer → 해당 broker 에 InitProducerIdRequest
Coordinator → Producer ID + Producer Epoch 부여
2. Transaction Log — __transaction_state
Kafka 내부 topic. 50 partition 기본. 각 transaction 의 state 영속 저장.
Key: transactional.id
Value: { producer_id, epoch, state, partitions, timestamp }
State 변화 흐름:
Empty → Ongoing → PrepareCommit/PrepareAbort → CompleteCommit/CompleteAbort → Empty
각 state 변화 = __transaction_state 에 append. crash 후 복구 가능.
3. Two-Phase Commit 흐름
Step 1: BeginTransaction
Producer.beginTransaction()
↓
Coordinator: state = Ongoing
__transaction_state 에 write
Step 2: Add Partitions
Producer.send(record) — topic-A, partition 0
↓
Producer 가 Coordinator 에 AddPartitionsToTxn(topic-A:0)
Coordinator: 해당 partition 을 transaction state 에 추가
__transaction_state 에 write
이후 같은 transaction 의 다른 topic·partition 도 추가됨.
Step 3: Send Records
Producer.send(record1) → topic-A:0
Producer.send(record2) → topic-B:0
Producer.send(record3) → topic-A:1
각 record 는 해당 broker 의 topic-partition 에 write. 모든 record 는 transactional flag 가 attribute 에 설정.
Step 4: Send Offsets (consume-then-produce 패턴)
Producer.sendOffsetsToTransaction(offsets, consumerGroupMetadata)
↓
Producer 가 Coordinator 에 AddOffsetsToTxnRequest
Coordinator 가 `__consumer_offsets` (consumer 의 offset topic) 에 *transactional offset* 으로 write
Kafka Streams 의 핵심 — input 처리한 offset + output 메시지를 함께 atomic.
Step 5: Commit Transaction (Phase 1)
Producer.commitTransaction()
↓
Coordinator: state = PrepareCommit
__transaction_state 에 write
Step 6: Write Markers (Phase 2)
Coordinator: 각 partition leader 에 WriteTxnMarkersRequest
↓
각 partition leader 가 COMMIT control batch 를 log 에 append
↓
모든 marker 완료 응답
110편 message format 의 control batch (transaction 경계를 알리는 특수 배치) 가 곧 commit marker.
Step 7: Complete
Coordinator: state = CompleteCommit
__transaction_state 에 write
↓
Producer 에 응답
전체 = distributed two-phase commit.
Consumer 의 read_committed
isolation.level=read_committed
활성 시:
Consumer 가 partition 의 메시지 fetch
↓
Broker 가 LSO (Last Stable Offset, 안전하게 읽어도 되는 마지막 offset) 까지만 반환
↓
LSO = "이 offset 이전의 모든 transaction 이 commit 또는 abort 됨"
Ongoing transaction 의 메시지는 안 보임. Transaction commit 후 control batch 가 read 되고 그 이전 메시지가 visible.
Aborted Transaction
Abort 된 메시지도 디스크에 있음 (제거 X). 단 consumer 에게 안 보임 — control batch 가 ABORT marker 면 그 batch 의 메시지들 skip.
Aborted records 는 log compaction 또는 retention 으로 결국 제거.
Producer Fencing — Zombie Producer 방지
문제
Producer A (transactional.id = "tx-1") = 실행 중
↓ 일시 멈춤 (긴 GC)
Producer B (같은 transactional.id) = 다른 인스턴스에서 시작 (오류로 같은 ID)
↓
B 가 transaction 시작·commit
A 가 깨어남 → 자기 transaction commit 시도
→ ?
Zombie producer 가 동시에 commit = 일관성 깨짐.
Fencing 메커니즘
Producer A: ProducerID=12345, Epoch=0
↓ 일시 멈춤
Producer B (같은 transactional.id): Coordinator 가 새 Epoch=1 부여
↓
A 가 깨어남 → Epoch (세대 번호) 0 으로 요청
Coordinator → "Epoch mismatch! You are fenced."
A → ProducerFencedException → 종료
Epoch 비교로 zombie 자동 차단.
Transaction Timeout
transaction.timeout.ms=60000 # 60초 (기본)
transaction.max.timeout.ms=900000 # broker 한계 15분
Producer 가 transaction 시작 후 60초 안에 commit/abort 안 하면 coordinator 가 자동 abort.
이유 — Producer crash 후 transaction 영원히 ongoing 방지.
여기서 시험 함정이 하나 있어요 — Long-running transaction 환경 — transaction.timeout.ms 늘림. Kafka Streams 같은 환경.
Coordinator Failover
Coordinator broker 가 죽으면?
Coordinator broker 다운
↓
Controller 가 __transaction_state 의 해당 partition 새 leader 선출
↓
새 broker 가 transaction state 받아 coordinator 역할 인수
↓
Producer 가 다음 request 시 NOT_COORDINATOR_FOR_TRANSACTION 받음
↓
FindCoordinatorRequest 다시 → 새 coordinator 발견 → 정상 운영
자동 failover. Producer 는 코드 변경 없이 회복.
Kafka Streams + EOS
StreamsConfig:
PROCESSING_GUARANTEE_CONFIG = StreamsConfig.EXACTLY_ONCE_V2
내부적으로 자동 처리:
- 각 task 가 별도 transactional.id (예:
appId-0_0,appId-0_1, ...) - Input read → process → output write → offset commit = 한 transaction
- 실패 시 abort → 재시도
Kafka Streams 가 transaction 의 가장 흔한 활용.
EOS V1 vs V2
| 항목 | V1 (옛) | V2 (Kafka 3.0+) |
|---|---|---|
| Producer per task | 별도 인스턴스 | 공유 인스턴스 |
| 메모리 사용 | 큼 | 작음 |
| 성능 | 보통 | 빠름 |
| 권장 | deprecated | 표준 |
신규 환경 = V2 사용. processing.guarantee=exactly_once_v2.
모니터링
JMX (Java Management Extensions, 자바 표준 운영 메트릭) :
TransactionCoordinator관련 메트릭 (transaction count·state distribution)transaction-markerwrite rate__transaction_statepartition lag
운영 상태 확인.
한계·실무 함정
1. Transaction 비용
EOS = 성능 약 10~20% 감소 (대부분 환경). 정말 필요한 자리만.
2. transactional.id 의 unique 보장
여러 인스턴스가 같은 ID = zombie producer 문제. unique 보장 + Fencing 메커니즘.
3. Long transaction
transaction.timeout.ms 초과 = 자동 abort → 데이터 손실. 처리 길면 timeout 늘림.
4. read_committed 의 지연
LSO 까지만 read = transaction commit 까지 약간 지연. 실시간 환경에서 약간 추가 latency.
5. External system 과 EOS
Kafka → DB·HTTP 같은 외부 sink = Kafka transaction 밖. 88편의 Transactional Outbox (DB 트랜잭션 안에 메시지 적재 후 별도 발행) 패턴 필요.
6. Cross-cluster transaction X
Transaction 은 한 cluster 안에서만. MirrorMaker 2 로 다른 cluster 로 mirror 후 EOS 보장 X.
Part 5-7 Internals 마무리
5편 (108~112):
- 108 Log — 파일 구조·segment·index·offset
- 109 Network Layer — NIO·Selector·thread pool
- 110 Message Format — Record Batch v2·varint·CRC
- 111 Rebalance Protocol — Eager·Cooperative·KIP-848
- 112 Transaction Protocol — EOS 내부 메커니즘
Kafka 의 가장 깊은 영역. 일상 운영에서 직접 만지지 않지만 디버깅·성능 분석·고급 운영 에 필수.
시험 직전 한 번 더 — Kafka Transaction 함정 압축 노트
- EOS (Exactly-Once Semantics) = 88편의 3가지 의미 보장 중 가장 강한 보장
- 3 부품 = Transactional Coordinator (broker 안) ·
__transaction_statetopic · Control Batch (마커) - Coordinator = 각 broker 가 역할 가능,
transactional.id별 매핑 __transaction_state= 50 partition, transaction state 영속 저장- State = Empty → Ongoing → Prepare → Complete → Empty
- Two-Phase Commit 흐름 = BeginTransaction → AddPartitions → Send Records → SendOffsets (consume-then-produce) → CommitTransaction (Phase 1 PrepareCommit) → WriteMarkers (Phase 2) → Complete
- Control Batch = COMMIT 또는 ABORT marker
- 110편 message format 의 control batch bit
- Consumer
isolation.level=read_committed= LSO 까지만 read - LSO = Last Stable Offset = 이 offset 이전 모든 transaction commit/abort 완료
- Ongoing transaction 메시지 = consumer 에게 안 보임
- Aborted 메시지 = 디스크 있지만 consumer 에게 안 보임 (compaction/retention 으로 제거)
- Producer Fencing =
ProducerID + Epoch으로 zombie 차단 - 같은 transactional.id 새 producer = 새 Epoch → 옛 producer =
ProducerFencedException transaction.timeout.ms(60초 기본) = 초과 시 자동 abort- 긴 처리 = 늘림
- Coordinator Failover = controller 가 새 leader 선출, producer 자동 회복
- Kafka Streams + EOS =
processing.guarantee=exactly_once_v2 - 각 task 별 transactional.id
- input read → process → output write → offset commit = 한 transaction
- EOS V2 (Kafka 3.0+) = producer 공유, 메모리·성능 V1 보다 좋음
- 신규 = V2 표준
- 함정 — EOS 성능 비용 ~10~20%
- 함정 —
transactional.idunique 보장 - 함정 — Long transaction = timeout 늘림
- 함정 —
read_committed약간 지연 - 함정 — External system EOS = Transactional Outbox (88편)
- 함정 — Cross-cluster transaction X
- Part 5-7 Internals 5편 = Log·Network·Message Format·Rebalance·Transaction
공식 문서: KIP-98 Transactional Messaging 에서 자세한 설계를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 107편 — Kafka Log Compaction (Key 별 최신만 유지)
- 108편 — Kafka Log 파일 구조 (Segment · Index · Offset)
- 109편 — Kafka Network Layer (NIO · Selector · Thread Pool)
- 110편 — Kafka Message Format (Record Batch v2 · 바이트 구조)
- 111편 — Kafka Consumer Rebalance Protocol (KIP-848 새 모델)
다음 글: