Blog
kafkaproducerperformancetuningthroughput

[Kafka 운영 ⑧] 프로듀서 처리량 튜닝 — 배치·압축·linger의 트레이드오프

Kafka 프로듀서의 batch.size, linger.ms, compression.type, buffer.memory, max.in.flight, acks를 정확히 이해하고 처리량과 지연 사이의 트레이드오프를 조율하는 실전 튜닝 가이드입니다.

Data Dynamics2026年6月7日20 min read
This post is not yet translated. The original Korean version is shown below.

프로듀서 튜닝은 "더 빠르게"라는 한 단어로 정리되지 않습니다. 처리량(throughput)을 올리면 지연(latency)이 늘고, 지연을 줄이면 처리량이 떨어집니다. 둘은 같은 다이얼의 양 끝이라서, 좋은 튜닝이란 "두 마리 토끼를 다 잡는 것"이 아니라 우리 워크로드가 어느 쪽 끝에 있어야 하는지를 정하고 거기에 맞추는 일입니다. 이 글은 프로듀서가 레코드를 어떻게 모아 보내는지부터 짚고, 그 위에서 batch.size·linger.ms·compression.type·acks가 어떻게 맞물리는지 정확한 설정 키로 풀어냅니다.

이 글에서 배우는 것

  • 프로듀서가 레코드를 파티션별로 버퍼링하고 배치로 전송하는 멘탈 모델
  • batch.sizelinger.ms가 처리량·지연을 어떻게 맞바꾸는지
  • buffer.memorymax.block.ms로 버퍼가 가득 찰 때의 백프레셔
  • compression.type(lz4/zstd 권장)과 배치 크기의 상호작용
  • max.in.flight.requests.per.connection·enable.idempotence와 순서 보장
  • 처리량 최적화 vs 저지연 최적화 설정 세트와 의사결정 기준

1. 멘탈 모델 — 프로듀서는 모아서 보낸다

producer.send()를 호출한다고 해서 레코드가 그 즉시 네트워크로 나가지는 않습니다. 프로듀서는 레코드를 토픽-파티션 단위로 메모리에 버퍼링한 뒤, 일정 조건이 만족되면 같은 파티션으로 가는 레코드들을 하나의 **배치(batch)**로 묶어 브로커에 전송합니다. 이 구조를 이해하지 못하면 어떤 설정도 직관과 반대로 동작하는 것처럼 보입니다.

Loading diagram…

핵심 흐름은 이렇습니다.

  1. send()는 레코드를 직렬화하고 파티셔너로 대상 파티션을 정한 뒤, RecordAccumulator의 해당 파티션 배치에 추가만 하고 즉시 리턴합니다(논블로킹).
  2. 배경의 **Sender 스레드(kafka-producer-network-thread)**가 전송 준비된 배치를 골라 브로커별로 묶어 보냅니다.
  3. 배치가 "전송 준비됨"이 되는 조건은 두 가지입니다: 배치가 batch.size만큼 찼거나, 또는 그 배치가 만들어진 지 linger.ms가 지났거나.

그래서 처리량과 지연의 긴장은 전부 이 한 줄로 압축됩니다. 배치를 크게 모을수록 요청당 효율(처리량)은 올라가지만, 모으는 동안 레코드는 기다린다(지연).

단위무엇인가영향
레코드(record)send() 한 건직렬화 후 배치에 들어감
배치(batch)한 파티션으로 갈 레코드 묶음batch.size/linger.ms로 마감
요청(request)한 브로커로 가는 여러 배치의 묶음max.request.size·in-flight 제한 적용

2. batch.size와 linger.ms — 처리량·지연 다이얼

이 두 값이 프로듀서 처리량 튜닝의 90%입니다.

batch.size

batch.size하나의 파티션 배치가 가질 수 있는 최대 바이트 수입니다(기본값 16384 = 16KB). 레코드 개수가 아니라 바이트라는 점에 주의하세요. 배치가 이 크기에 도달하면 linger.ms를 기다리지 않고 곧바로 전송 대상이 됩니다.

  • 너무 작으면: 배치가 금방 차서 작은 요청이 자주 나갑니다 → 요청 오버헤드·압축 효율 저하 → 처리량 손해.
  • 너무 크면: 트래픽이 적은 파티션에서는 배치가 잘 안 차서 결국 linger.ms에 의존하게 되고, buffer.memory도 더 많이 먹습니다.
  • 흔한 처리량 튜닝값: 32KB256KB(32768262144).

linger.ms

linger.ms배치를 곧바로 보내지 않고, 같은 파티션의 레코드를 더 모으려 기다리는 시간입니다(기본값 0). 0이어도 Sender가 바쁘면 자연스럽게 배치가 생기지만, 명시적으로 5~100ms를 주면 의도적으로 배치를 키울 수 있습니다.

# 처리량 지향 예시
batch.size=131072        # 128KB
linger.ms=20             # 최대 20ms 모았다가 전송

이 설정은 "최대 20ms 동안, 또는 128KB가 찰 때까지 모았다가 한 번에 보낸다"는 뜻입니다. 둘 중 먼저 충족되는 조건이 전송을 트리거합니다.

설정 방향batch.sizelinger.ms효과
저지연작게(16KB)0레코드가 거의 즉시 나감, 처리량 손해
균형32~64KB5~10약간의 지연으로 처리량 개선
고처리량128~256KB20~100배치 효율 극대화, 지연 증가

포인트: linger.ms는 "지연을 추가하는" 설정이 아니라 "처리량을 사는" 설정입니다. 트래픽이 충분히 많아 배치가 batch.size로 금방 차는 환경이라면, linger.ms를 올려도 실제 추가 지연은 거의 없습니다. 배치가 어차피 꽉 차서 나가니까요.


3. buffer.memory와 max.block.ms — 버퍼가 가득 찰 때

배치는 공짜로 쌓이지 않습니다. 모든 파티션의 배치는 buffer.memory라는 공유 메모리 풀(기본 32MB) 안에서 할당됩니다. 브로커가 느리거나 네트워크가 막혀 Sender가 배치를 비워내지 못하면, 이 풀이 가득 찹니다.

이때 send()는 더 이상 논블로킹이 아닙니다. 새 레코드를 넣을 공간이 생길 때까지 블록되고, 그 최대 대기 시간이 max.block.ms(기본 60000 = 60초)입니다. 이 시간을 넘기면 send()TimeoutException을 던집니다.

buffer.memory=67108864   # 64MB — 처리량이 높거나 브로커 지연이 잦으면 늘린다
max.block.ms=60000       # send()/partitionsFor()가 블록될 최대 시간

이것이 프로듀서의 백프레셔(back-pressure) 지점입니다. 동작을 정리하면 이렇습니다.

상황결과
버퍼에 여유 있음send() 즉시 리턴(논블로킹)
버퍼 가득 참send()가 최대 max.block.ms까지 블록
max.block.ms 초과TimeoutException — 호출 스레드가 받음

증상 진단: 애플리케이션 스레드가 send()에서 멈춰 있거나 TimeoutException이 보이면, 거의 항상 "프로듀서가 보내는 속도보다 빨리 만들어내고 있다"는 신호입니다. buffer.memory를 늘리는 건 임시방편이고, 근본 원인은 브로커 처리량 부족·acks=all 지연·파티션 수 부족·네트워크인 경우가 많습니다.


4. compression.type — 배치를 압축한다

압축은 처리량 튜닝에서 가장 비용 대비 효과가 큰 레버입니다. compression.type은 프로듀서가 배치 단위로 페이로드를 압축하도록 합니다.

압축률CPU 비용비고
none없음없음기본값
gzip높음높음압축률은 좋지만 CPU 부담 큼
snappy중간낮음빠르지만 압축률은 보통
lz4중간~높음낮음처리량/CPU 균형이 좋음 — 권장
zstd높음중간압축률·속도 균형 우수 — 권장

핵심은 "압축이 배치 단위로 일어난다"는 점입니다. 그래서 배치가 클수록(많은 레코드가 함께 압축될수록) 압축률이 좋아집니다. 즉 batch.size/linger.ms를 키우는 튜닝과 압축은 서로를 증폭합니다. 작은 배치를 압축하면 압축할 데이터가 적어 효과가 미미합니다.

compression.type=lz4
batch.size=131072
linger.ms=20

권장은 처리량·압축률 균형에서 lz4 또는 zstd입니다. 로그처럼 반복 패턴이 많은 데이터는 zstd가 망 대역폭과 디스크를 크게 아껴 줍니다. 다만 압축은 프로듀서 CPU를 쓰므로, CPU 코어가 빠듯한 환경에서는 zstd보다 lz4가 안전합니다. 브로커는 기본적으로 압축된 배치를 그대로 저장하므로 디스크와 복제 트래픽도 함께 줄어드는 부수 효과가 있습니다(브로커 compression.typeproducer로 두면 재압축 없이 보존).


5. max.in.flight와 순서 보장

max.in.flight.requests.per.connection한 브로커 연결당 응답을 받지 않은 채로 동시에 떠 있을 수 있는 요청 수입니다(기본 5). 이 값이 클수록 네트워크 왕복을 기다리지 않고 다음 배치를 계속 밀어 넣을 수 있어 처리량이 올라갑니다. 하지만 재시도(retry)와 만나면 순서가 깨질 수 있습니다.

시나리오를 보죠. in-flight가 2이고 idempotence가 꺼져 있다고 합시다.

  1. 배치 A, 배치 B를 연달아 전송(둘 다 in-flight).
  2. 배치 A가 일시적 오류로 실패, 배치 B는 성공.
  3. 프로듀서가 배치 A를 재시도 → A가 B보다 나중에 브로커에 기록됨.
  4. 결과: 같은 파티션 안에서 A·B 순서가 뒤집힘.

이를 막는 정석은 멱등 프로듀서입니다.

enable.idempotence=true            # (최신 기본값) 시퀀스 번호로 중복·재정렬 방지
max.in.flight.requests.per.connection=5   # 멱등이면 5까지 순서 보장
acks=all                           # 멱등성의 전제 조건

enable.idempotence=true이면 프로듀서가 각 레코드에 시퀀스 번호를 붙이고, 브로커가 이를 검증해 재시도가 일어나도 파티션 내 순서와 정확히-한-번 기록(중복 없음)을 보장합니다. 이 경우 in-flight를 5까지 올려도 순서가 보존됩니다(Kafka가 브로커에서 재정렬을 잡아 줍니다). 멱등성을 켜면 acks=all, retries>0, max.in.flight<=5가 자동 전제로 요구됩니다.

설정순서 보장처리량
idempotence OFF, in-flight=1보장(재시도해도 한 번에 하나라 안전)낮음
idempotence OFF, in-flight>1 + retries깨질 수 있음높음
idempotence ON, in-flight<=5보장높음

순서 보장 메커니즘 전반은 시리즈 **10편(메시지 순서와 파티셔닝)**에서 더 깊게 다룹니다. 여기서는 "처리량을 위해 in-flight를 올리려면 멱등성을 켜라"만 기억하면 됩니다.


6. acks와 처리량의 상호작용

acks는 프로듀서가 전송을 "성공"으로 간주하기 위해 필요한 확인 수준입니다. 내구성(durability)과 직결되며, 처리량·지연에도 영향을 줍니다.

acks의미내구성처리량·지연
0응답을 기다리지 않음매우 낮음(유실 가능)가장 빠름
1리더만 기록 확인중간(리더 장애 시 유실)빠름
all(-1)ISR 전체 복제 확인높음(권장 기본)추가 지연

acks=all은 리더가 ISR(in-sync replica) 전체에 복제될 때까지 기다리므로 요청당 지연이 늘어납니다. 그래서 "처리량을 위해 acks=1로 낮추자"는 유혹이 생기지만, 이는 내구성을 파는 거래입니다. 핵심은 acks=all의 추가 지연이 요청 단위라는 점입니다. 즉 배치를 키워(2~4절) 요청 수 자체를 줄이면, acks=all을 유지하면서도 전체 처리량을 크게 회복할 수 있습니다. 지연을 줄이는 올바른 첫 수는 acks 낮추기가 아니라 배치·압축·in-flight 튜닝입니다.

acks의 내구성 의미와 ISR·min.insync.replicas의 관계는 시리즈 **4편(복제와 acks, 그리고 데이터 내구성)**에서 자세히 설명합니다.


7. 워크로드별 튜닝 세트

지금까지의 설정을 두 가지 대표 목표로 묶어 봅니다. 절댓값은 출발점일 뿐, 항상 본인 워크로드에서 측정하며 조정하세요.

처리량 최적화 (배치 ETL, 로그 수집, 대량 적재)

batch.size=262144                 # 256KB — 큰 배치
linger.ms=50                      # 모아서 보냄
compression.type=zstd             # 압축률 높게 (CPU 여유 있을 때)
buffer.memory=134217728           # 128MB
acks=all                          # 내구성 유지 (배치로 비용 상쇄)
enable.idempotence=true
max.in.flight.requests.per.connection=5

저지연 최적화 (실시간 알림, 트랜잭션 이벤트, 사용자 반응 경로)

batch.size=16384                  # 작은 배치(기본)
linger.ms=0                       # 즉시 전송
compression.type=lz4              # 가벼운 압축
buffer.memory=33554432            # 32MB(기본)
acks=all                          # 내구성은 그대로
enable.idempotence=true
max.in.flight.requests.per.connection=5
다이얼처리량 우선저지연 우선
batch.size크게 (128~256KB)작게 (16KB)
linger.ms20~1000
compression.typezstdlz4 또는 none
buffer.memory크게 (64~128MB)기본(32MB)
acksallall
enable.idempotencetruetrue

두 세트 모두 acks=allenable.idempotence=true공통으로 유지한다는 점에 주목하세요. 처리량이든 저지연이든, 내구성·순서 보장은 협상 대상이 아니라 출발선입니다.


8. 내구성 기본값을 함부로 팔지 말 것

처리량 압박이 오면 가장 먼저 손이 가는 게 acks=0/1로 낮추고 enable.idempotence=false로 끄는 것입니다. 즉각적인 숫자는 좋아지지만, 그 대가로 메시지 유실과 중복·재정렬이라는, 운영에서 가장 디버깅하기 어려운 종류의 문제를 떠안게 됩니다.

올바른 우선순위는 이렇습니다.

  1. 먼저 배치·압축으로 짜낸다batch.size↑, linger.ms↑, compression.type=lz4/zstd. 대부분의 처리량 문제는 여기서 해결됩니다.
  2. 그래도 부족하면 in-flight와 버퍼를 키운다 — 단, 멱등성을 켠 채로.
  3. 파티션 수와 브로커 자원을 의심한다 — 프로듀서 한 대로 안 되면 토픽 파티션을 늘려 병렬도를 확보한다.
  4. 내구성 설정은 마지막의 마지막에, 그리고 명시적 합의 하에만 건드린다 — "이 토픽은 유실을 감수해도 되는 메트릭이다" 같은 합의가 있을 때만.

체크리스트: 처리량이 안 나올 때 → ① batch.size/linger.ms를 올렸는가 ② compression을 켰는가 ③ buffer.memory가 충분한가 ④ 파티션 수가 병렬도를 막고 있지 않은가. 이 네 가지를 다 거친 뒤에야 acks를 의심하세요.


마치며

  • 프로듀서는 레코드를 파티션별로 버퍼링해 배치로 전송합니다. 처리량과 지연은 같은 다이얼의 양 끝이며, 튜닝은 "어느 끝에 설 것인가"를 정하는 일입니다.
  • batch.size(배치 최대 바이트)와 linger.ms(채우길 기다리는 시간)가 그 다이얼입니다. 둘을 키우면 처리량↑·지연↑.
  • buffer.memory가 차면 send()max.block.ms까지 블록됩니다 — 프로듀서의 백프레셔 지점입니다.
  • compression.type배치 단위로 압축하므로 큰 배치와 시너지가 납니다. 균형점은 lz4 또는 zstd.
  • in-flight를 올려 처리량을 얻으려면 **enable.idempotence=true**로 순서를 지키세요. 멱등이면 in-flight 5까지 안전합니다.
  • acks=all의 지연은 요청 단위라서, 배치를 키우면 내구성을 지키면서도 처리량을 회복할 수 있습니다. 내구성 기본값을 처리량과 맞바꾸지 마세요.

참고 자료


— Data Dynamics 엔지니어링 팀