Kafka Shallow Dive - Producer 심화

Published:

이번 주는 쓰는 쪽. Producer가 그냥 send() 한 번 호출하는 것처럼 보여도 안에선 배치, 재시도, ack 조율, 파티셔닝이 다 벌어지고 있다.

Producer가 하는 일

app → send(record)
        │
        ▼
   [Serializer]            key/value → bytes
        │
        ▼
   [Partitioner]           어느 partition으로 보낼지 결정
        │
        ▼
   [Record Accumulator]    파티션별 batch 버퍼 (메모리)
        │
        ▼
   [Sender thread]         broker로 배치 전송 + retry + ack 처리

내부적으로 거의 로그 집계기. 호출 스레드는 accumulator까지만 쌓고 리턴하고, 실제 네트워크 I/O는 sender 스레드가 비동기로 처리한다.

Serializer

key/value를 byte로 바꿔주는 컴포넌트. Kafka는 byte[]만 취급하니까 직렬화가 필수.

props.put("key.serializer",   "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");

흔한 선택지:

  • StringSerializer — 단순 텍스트/JSON
  • ByteArraySerializer — 이미 직렬화한 payload
  • Avro/Protobuf/JSON Schema — Schema Registry 연동 (Schema Registry 편에서)

Partitioner

어느 파티션으로 보낼지 결정하는 로직. Record의 key 유무로 동작이 달라짐.

조건동작
key가 있음murmur2(key) % numPartitions — 같은 key는 같은 파티션
key가 없음 (≥ 2.4)Sticky partitioner — 한 파티션에 배치 차도록 몰아서 보냄 (처리량↑)
key가 없음 (≤ 2.3)round-robin — 배치가 잘게 쪼개져서 비효율

Key 설계 포인트:

  • 순서가 필요한 단위로 key를 잡는다 (예: user_id, order_id)
  • Key 분포가 한쪽으로 쏠리면 특정 파티션이 hot spot 됨 → 파티셔너 커스텀 고려
  • 파티션 수를 바꾸면 같은 key의 목적지가 바뀌니 주의

acks — 내구성 vs 지연

Producer가 broker로부터 어느 시점까지 기다릴지 정하는 옵션. 가장 중요한 설정.

acks동작내구성지연/처리량
0broker 응답 안 기다림유실 가능가장 빠름
1leader가 받은 즉시 응답leader 죽으면 유실 가능중간
all (= -1)ISR 전체에 복제 후 응답가장 안전가장 느림

acks=all만 쓰면 되는 거 아님? — 거의 그렇지만, min.insync.replicas 와 세트로 봐야 함 (Replication 편).

로그나 메트릭처럼 유실이 허용되는 대용량 파이프라인은 acks=1도 충분하고, 결제/주문처럼 한 건도 잃으면 안 되는 건 acks=all + min.insync.replicas=2가 정석.

Idempotent Producer

enable.idempotence=true — 기본값은 true (3.0+부터).

네트워크 재시도 때문에 같은 메시지가 두 번 쓰이는 걸 막음. Producer마다 PID(Producer ID)를 부여하고, 각 파티션에 대해 시퀀스 번호를 매김.

Producer → [PID=42, seq=0, msg] → Broker (기록)
Producer ✗ ack 못 받음
Producer → [PID=42, seq=0, msg] → Broker (seq 중복 감지, 무시)
  • 설정하는 순간 acks=all, retries=Integer.MAX_VALUE, max.in.flight.requests.per.connection ≤ 5 로 제약이 걸림
  • 단일 producer 세션, 단일 파티션 기준으로만 중복 방지
  • producer 재시작하면 PID이 새로 생기므로 그 경계를 넘는 중복은 못 막음 → Exactly-once 편의 transaction이 필요

Batching & Linger

Producer는 메시지를 모아서 배치로 보냄. 두 가지 트리거:

┌─────────────── batch ─────────────────┐
│ msg  msg  msg  msg  msg  ...          │
└───────────────────────────────────────┘
  ↑                                     ↑
  batch.size (기본 16KB) 찼거나         linger.ms (기본 0ms) 지나면 flush
  • batch.size — 배치 하나의 최대 바이트 (파티션별)
  • linger.ms — 배치가 덜 찼어도 이 시간 지나면 보냄
  • buffer.memory — accumulator 전체 메모리 (기본 32MB)

기본값(linger=0)은 “메시지 하나 와도 바로 보냄”. 처리량이 중요하면 linger.ms=5~20으로 올려서 배치 채우기. 지연이 중요하면 그대로 둠.

꽉 차서 buffer.memory가 부족해지면 producer는 블록되거나 예외 던짐. max.block.ms 조정.

Compression

배치 단위로 압축됨. 네트워크/디스크 대역폭을 확 줄임.

코덱비율CPU특징
none1.0x0기본
gzip좋음높음범용
snappy중간낮음균형형
lz4중간낮음snappy와 비슷, 약간 빠름
zstd매우 좋음중간신형 (2.1+), 요즘 선호

현업에선 lz4 또는 zstd가 일반적. 로그성 대용량엔 zstd가 비율이 좋아서 스토리지 비용까지 아낀다.

compression.type=zstd

Broker는 압축된 채로 저장하고, consumer가 풀어서 읽음. 압축/해제는 양 끝단에서만 일어남.

실전 Producer 설정 템플릿

로그/이벤트 수집 (처리량 우선):

acks=1
enable.idempotence=false
linger.ms=20
batch.size=262144
compression.type=zstd
buffer.memory=67108864

주문/결제 (내구성 우선):

acks=all
enable.idempotence=true
linger.ms=5
compression.type=zstd
max.in.flight.requests.per.connection=5
delivery.timeout.ms=120000

자주 하는 실수

  • key=null인데 순서 보장되겠지? — 안 됨. 파티션이 바뀜
  • send().get()으로 매번 블록 — 처리량 박살남. 콜백이나 Future로 비동기 처리
  • close() 안 호출 — 배치에 남은 메시지 유실. 종료 시 producer.close() 또는 try-with-resources
  • retries=0으로 “빠르게” — 일시적 네트워크 장애에도 메시지 버림. 재시도 비활성화는 거의 함정
  • 너무 큰 batch.size — 작은 파티션 수에선 메모리만 먹고 효과 없음

정리

Producer는 send 한 줄이지만 내부에 파티셔닝, 배치, 압축, ack 조율, idempotence까지 다 들어있다. 내구성(acks+idempotence)과 처리량(linger+batch.size+compression)을 축으로 놓고 케이스별로 조합하는 게 핵심. “안전하게 빠르게”의 실제 의미를 이번 주에야 체감했다.

다음 주는 반대편 — Consumer group과 리밸런싱, offset commit 전략.