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 | 동작 | 내구성 | 지연/처리량 |
|---|---|---|---|
0 | broker 응답 안 기다림 | 유실 가능 | 가장 빠름 |
1 | leader가 받은 즉시 응답 | 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 | 특징 |
|---|---|---|---|
| none | 1.0x | 0 | 기본 |
| 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 전략.
