Blog
kafkaperformancetuningbenchmarkingproducerconsumer

[Kafka 성능 ①] 추측 말고 측정하라 — 벤치마킹과 프로듀서·컨슈머 파라미터

Kafka 성능 튜닝의 출발점은 추측이 아니라 측정입니다. kafka-producer-perf-test·kafka-consumer-perf-test로 베이스라인을 잡고, 백분위 기반으로 한 번에 한 변수씩 바꾸는 방법론, 그리고 처리량과 지연을 좌우하는 프로듀서·컨슈머 파라미터를 정리합니다.

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

"linger.ms를 20으로 올렸더니 빨라졌어요"라는 말을 들으면 가장 먼저 묻고 싶은 게 있습니다. "무엇과 비교해서 빨라졌나요? 처리량인가요, p99 지연인가요? 메시지 크기는 운영과 같았나요?" 베이스라인 없는 성능 튜닝은 결국 추측입니다. 운이 좋으면 맞고, 대부분은 한 지표를 좋게 만들면서 다른 지표를 조용히 망가뜨립니다. 이 글은 Kafka 성능 미니 시리즈 3부작의 1편으로, 측정을 먼저 하고, 그다음 한 번에 하나의 변수만 바꾸는 규율을 다룹니다.

이 글에서 배우는 것

  • 왜 측정이 튜닝보다 먼저여야 하는가 — 처리량·지연·내구성은 하나의 다이얼이 아니라 트레이드오프 공간이다
  • kafka-producer-perf-test.sh·kafka-consumer-perf-test.sh로 베이스라인을 잡는 법과 출력 읽는 법
  • 평균이 아니라 p99/p999 백분위를 측정하고, 한 번에 한 변수만 바꾸는 벤치마킹 방법론
  • 처리량과 지연을 움직이는 프로듀서 파라미터(batch.size·linger.ms·acks 등)
  • 의외로 자주 놓치는 컨슈머 페치 파라미터(fetch.min.bytes·fetch.max.wait.ms·max.poll.records 등)

1. 왜 측정이 먼저인가

성능은 하나의 다이얼이 아니다

성능 튜닝을 처음 접하면 "더 빠르게"라는 단일 목표를 떠올리기 쉽습니다. 하지만 Kafka에서 "빠르다"는 말은 세 가지 서로 다른 축을 가립니다.

  • 처리량(Throughput): 초당 처리하는 레코드 수 또는 바이트(records/sec, MB/sec)
  • 지연(Latency): 한 메시지가 프로듀서에서 컨슈머까지 도달하는 데 걸리는 시간, 특히 꼬리 지연(p99/p999)
  • 내구성(Durability): 장애가 나도 메시지를 잃지 않는 정도(acks, 복제, min.insync.replicas)

이 셋은 트레이드오프 공간을 이룹니다. 배치를 키우고 linger.ms를 늘리면 처리량은 오르지만 개별 메시지의 지연은 늘어납니다. acks=all로 내구성을 높이면 프로듀서는 ISR 복제를 기다려야 하므로 지연이 커집니다. 하나를 당기면 다른 하나가 따라 움직입니다. 그래서 "튜닝했더니 빨라졌다"는 항상 "무엇을 희생해서"라는 질문을 동반해야 합니다.

Loading diagram…

추측의 비용

베이스라인이 없으면 다음과 같은 함정에 빠집니다.

  • 확증 편향: 바꾼 파라미터가 효과 있었다고 믿고 싶어, 평균이 살짝 좋아진 것을 "개선"으로 해석합니다.
  • 교란 변수: 두 파라미터를 동시에 바꾸면 어느 쪽이 효과였는지 영원히 알 수 없습니다.
  • 비현실적 워크로드: 100바이트 메시지로 튜닝한 값이 운영의 10KB 메시지에서는 정반대로 작동합니다.

측정의 목적은 "정답 설정"을 찾는 게 아니라, 변경의 효과를 인과적으로 분리하는 데 있습니다. 그러려면 재현 가능한 베이스라인과 도구가 필요합니다.


2. 벤치마킹 도구

Kafka는 배포판에 성능 측정 CLI를 기본 포함합니다. 별도 설치 없이 베이스라인을 잡을 수 있습니다.

kafka-producer-perf-test.sh

프로듀서가 낼 수 있는 처리량과 지연을 측정합니다. 핵심 옵션은 다음과 같습니다.

옵션의미
--num-records전송할 총 레코드 수
--record-size레코드 바이트 크기(운영과 비슷하게)
--throughput목표 처리량 제한(records/sec). -1이면 무제한(포화 측정)
--producer-propsacks, batch.size, linger.ms, compression.type 등 프로듀서 설정
--print-metrics종료 시 프로듀서 내부 메트릭 출력

예시 — 1KB 레코드 500만 건을 무제한으로 보내 프로듀서 포화 처리량을 측정합니다.

kafka-producer-perf-test.sh \
  --topic perf-test \
  --num-records 5000000 \
  --record-size 1024 \
  --throughput -1 \
  --producer-props \
    bootstrap.servers=broker1:9092,broker2:9092,broker3:9092 \
    acks=all \
    batch.size=16384 \
    linger.ms=5 \
    compression.type=lz4

출력 예시(말미 요약 줄):

5000000 records sent, 412371.4 records/sec (402.71 MB/sec),
  18.43 ms avg latency, 412.00 ms max latency,
  12 ms 50th, 35 ms 95th, 78 ms 99th, 142 ms 99.9th.

여기서 봐야 할 것은 평균 지연(18.43 ms)이 아니라 백분위입니다. p50은 12 ms인데 p99가 78 ms, p999가 142 ms라면, 대부분은 빠르지만 1%의 요청은 6배 이상 느립니다. 운영에서 사용자가 체감하는 것은 이 꼬리입니다.

kafka-consumer-perf-test.sh

컨슈머가 브로커에서 데이터를 얼마나 빨리 끌어올 수 있는지 측정합니다.

kafka-consumer-perf-test.sh \
  --bootstrap-server broker1:9092,broker2:9092,broker3:9092 \
  --topic perf-test \
  --messages 5000000 \
  --fetch-size 1048576 \
  --consumer.config consumer-perf.properties \
  --show-detailed-stats \
  --reporting-interval 1000

consumer-perf.properties에는 fetch.min.bytes, fetch.max.wait.ms, max.partition.fetch.bytes 같은 컨슈머 페치 파라미터를 넣습니다(5장에서 상세히 다룹니다). 출력은 CSV 형태로 구간별 MB/sec, records/sec, 누적 수치를 보여 줍니다.

start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec
2026-07-21 10:00:00:000, 2026-07-21 10:00:11:842, 4882.81, 412.34, 5000000, 422301.7

더 본격적인 벤치마크

CLI 도구는 베이스라인용으로 훌륭하지만, 프로듀서와 컨슈머가 동시에 돌고 엔드투엔드 지연을 측정하는 시나리오에는 한계가 있습니다. 이럴 때는 다음을 고려합니다.

  • OpenMessaging Benchmark (OMB): 프로듀서·컨슈머를 동시에 구동하고 발행 지연과 엔드투엔드 지연을 백분위로 리포트하는 표준 워크로드 프레임워크. 메시징 시스템 간 비교에도 자주 쓰입니다.
  • Trogdor: Kafka 자체의 부하·장애 주입 테스트 하니스. 장기 부하 테스트나 장애 시나리오 재현에 적합합니다.

3. 측정 방법론

도구보다 중요한 것은 규율입니다. 같은 도구라도 방법론이 없으면 노이즈를 측정하게 됩니다.

워밍업과 정상 상태

JVM은 JIT 컴파일·클래스 로딩으로 초반 수십 초가 느립니다. 페이지 캐시도 처음엔 비어 있습니다. 따라서 워밍업 구간(초기 1~2분)의 수치는 버리고, 시스템이 **정상 상태(steady state)**에 들어간 뒤의 구간만 분석합니다. --reporting-interval로 구간 통계를 보면 정상 상태 진입 시점을 눈으로 확인할 수 있습니다.

평균이 아니라 백분위

평균 지연은 꼬리를 숨깁니다. p999가 1초여도 평균은 15 ms로 멀쩡해 보입니다. GC 정지, 리더 선출, 페치 대기 누적은 모두 꼬리에 나타나므로 p95·p99·p999를 반드시 함께 봅니다. SLO를 정의할 때도 "평균 20 ms"가 아니라 "p99 < 100 ms"처럼 백분위로 적습니다.

한 번에 한 변수만

이것이 가장 중요한 규칙입니다. batch.sizelinger.ms를 동시에 올리고 처리량이 좋아졌다면, 둘 중 무엇이 효과였는지 알 수 없습니다. 변경은 항상 하나씩, 그리고 매번 베이스라인과 비교합니다.

운영과 닮은 워크로드

  • 메시지 크기: 운영 평균/최대 크기로 측정합니다. 압축 효과는 페이로드 내용(반복성)에 크게 좌우되므로 합성 랜덤 데이터는 압축률을 왜곡합니다.
  • 키 분포: 키가 한쪽 파티션에 쏠리면 핫 파티션이 생깁니다. 운영의 키 카디널리티를 모사해야 파티션 분배 효과가 드러납니다.
  • 파티션 수·컨슈머 수: 병렬성은 처리량의 1차 변수이므로 운영 토폴로지를 맞춥니다.
Loading diagram…

이 루프를 한 변수씩 돌리면, 끝났을 때 각 파라미터가 처리량과 p99에 미친 영향을 표로 정리할 수 있습니다. 그게 진짜 튜닝의 산출물입니다.


4. 처리량을 움직이는 프로듀서 파라미터

프로듀서 튜닝의 깊은 분석은 별도 글 [Kafka 운영 ⑧] 프로듀서 처리량 튜닝에서 다루므로, 여기서는 벤치마크에서 무엇을 변수로 잡을지 관점에서 요약합니다.

파라미터역할올리면
batch.size파티션별 배치 버퍼 크기(바이트)처리량↑(왕복 감소), 채워질 때까지 약간 지연↑
linger.ms배치를 채우려 기다리는 시간처리량↑, 지연↑(대기 시간만큼)
compression.typelz4/zstd/snappy/gzip네트워크·디스크↓, CPU↑. lz4는 속도, zstd는 압축률
buffer.memory프로듀서 전체 송신 버퍼부족하면 send()가 블록되어 처리량 급락
acks0/1/all 확인 강도all은 내구성↑·지연↑, 1은 지연↓·내구성↓
max.in.flight.requests.per.connection미확인 인플라이트 요청 수처리량↑, 단 순서·재시도와 상호작용
enable.idempotence멱등 프로듀서(중복·재정렬 방지)true 권장. 정확히 한 번 전송 의미 + 안전한 재시도

처리량 vs 지연 한눈에

변경처리량지연(p99)내구성
batch.size ↑ + linger.ms▲ 크게▼ 나빠짐
compression.type=lz4▲ (네트워크 절감)≈ (CPU 여유 시)
acks=allacks=1▲ 좋아짐▼ 위험
enable.idempotence=true

주의: acks를 내구성 관점에서 다루는 깊은 분석은 [Kafka 운영 ④] acks · min.insync.replicas를 참고하세요. 벤치마크에서 acks를 바꿀 때는 항상 내구성 변화를 함께 기록해야 합니다. 빨라진 게 아니라 안전을 판 것일 수 있습니다.

권장 베이스라인 설정

# 처리량 지향 프로듀서 베이스라인 (그다음 한 변수씩 조정)
acks=all
enable.idempotence=true
compression.type=lz4
batch.size=32768
linger.ms=10
max.in.flight.requests.per.connection=5
buffer.memory=67108864

이 값에서 시작해 linger.ms만 5→20으로 바꿔 보고, 처리량과 p99가 어떻게 움직이는지 측정하는 식으로 진행합니다.


5. 컨슈머 파라미터 — 의외로 자주 놓치는 곳

프로듀서 튜닝은 많이 회자되지만, 컨슈머 페치 파라미터는 상대적으로 덜 다뤄집니다. 그런데 컨슈머 측 처리량과 지연을 크게 좌우합니다. 핵심은 "브로커가 데이터를 얼마나 모아서 한 번에 줄 것인가" 라는 페치 경제학입니다.

페치 파라미터의 작동 원리

컨슈머는 poll()을 호출하고, 컨슈머 내부에서 브로커로 fetch 요청을 보냅니다. 브로커는 다음 규칙으로 응답을 결정합니다.

Loading diagram…
파라미터의미트레이드오프
fetch.min.bytes브로커가 응답 전 모을 최소 바이트크게 → 페치 횟수↓, 처리량↑, 지연↑
fetch.max.wait.msfetch.min.bytes를 못 채워도 기다리는 상한min.bytes 대기의 지연 상한선. 낮추면 지연↓, 처리량↓
max.partition.fetch.bytes파티션당 한 번에 가져올 최대 바이트크게 → 파티션별 처리량↑, 메모리↑
fetch.max.bytes한 fetch 요청 전체의 최대 바이트응답 크기 상한. 메모리·지연과 상호작용
max.poll.recordspoll() 한 번이 반환할 최대 레코드 수처리 배치 크기. 너무 크면 max.poll.interval.ms 초과 위험
receive.buffer.bytes소켓 수신 버퍼(TCP) 크기고지연·고대역 링크에서 처리량↑

fetch.min.bytes와 fetch.max.wait.ms의 줄다리기

이 둘은 한 쌍입니다. fetch.min.bytes=1(기본값)이면 브로커는 데이터가 1바이트만 있어도 즉시 응답합니다. 지연은 최소지만, 트래픽이 적을 때 fetch 요청이 폭증해 브로커 CPU와 네트워크를 낭비합니다.

fetch.min.bytes를 예컨대 64KB로 올리면 브로커는 그만큼 쌓일 때까지 기다립니다. 페치 횟수가 줄어 처리량과 효율은 오르지만, 그만큼 데이터가 컨슈머에 늦게 도착합니다. 이 대기를 무한정 두면 곤란하니 fetch.max.wait.ms(기본 500ms)가 상한 역할을 합니다. 즉 "64KB가 모이거나, 안 모여도 500ms 지나면 무조건 응답"입니다.

설정처리량지연적합한 상황
fetch.min.bytes=1, fetch.max.wait.ms=100보통매우 낮음실시간 알림, 낮은 지연 우선
fetch.min.bytes=65536, fetch.max.wait.ms=500높음중간일반 스트리밍·ETL 처리량 우선
fetch.min.bytes=1048576, fetch.max.wait.ms=1000매우 높음높음배치성 대량 적재, 지연 둔감

max.poll.records와 max.poll.interval.ms의 상호작용

max.poll.records는 네트워크가 아니라 애플리케이션 처리 배치 크기를 정합니다. 한 번의 poll()이 500개를 반환하면, 다음 poll()까지 그 500개를 모두 처리해야 합니다. 처리 시간이 max.poll.interval.ms(기본 5분)를 넘기면 컨슈머는 죽은 것으로 간주되어 리밸런스가 일어나고, 그 사이 처리한 결과를 커밋하지 못해 중복 처리가 발생합니다.

따라서 레코드당 처리 비용이 큰 컨슈머(예: 외부 API 호출, DB 업서트)라면 max.poll.records를 줄여 한 배치를 인터벌 안에 끝내도록 맞춥니다. 반대로 가벼운 처리라면 키워서 루프 오버헤드를 줄입니다. 이 상호작용은 리밸런스 폭주와도 직결되므로, lag·페치 관점의 깊은 분석은 [Kafka 운영 ②] Consumer Lag이 줄지 않는 7가지 원인을 함께 보세요.

권장 컨슈머 베이스라인

# 처리량 지향 컨슈머 베이스라인
fetch.min.bytes=65536
fetch.max.wait.ms=500
max.partition.fetch.bytes=1048576
fetch.max.bytes=52428800
max.poll.records=500
receive.buffer.bytes=1048576
max.poll.interval.ms=300000

여기서도 규칙은 같습니다. fetch.min.bytes만 1→65536으로 바꿔 처리량과 p99 변화를 측정하고, 다음으로 max.poll.records만 조정하는 식으로 한 변수씩 진행합니다.


마치며

성능 튜닝의 첫걸음은 더 좋은 파라미터를 찾는 게 아니라, 변경의 효과를 측정으로 분리할 수 있는 환경을 만드는 것입니다. 재현 가능한 베이스라인, 운영과 닮은 워크로드, 백분위 기반 측정, 그리고 한 번에 한 변수 — 이 네 가지가 갖춰지면 그다음부터 파라미터 튜닝은 추측이 아니라 실험이 됩니다.

  • 측정 먼저: 베이스라인 없는 튜닝은 추측입니다. 처리량·지연·내구성은 하나의 다이얼이 아니라 트레이드오프 공간입니다.
  • 백분위로 본다: 평균은 꼬리를 숨깁니다. p99·p999가 사용자 경험과 SLO를 결정합니다.
  • 한 변수씩: 두 개를 동시에 바꾸면 인과를 잃습니다. 매번 베이스라인과 비교하세요.
  • 프로듀서batch.size·linger.ms·compression.type·acks가 처리량/지연/내구성을 가릅니다.
  • 컨슈머fetch.min.bytes·fetch.max.wait.ms의 줄다리기와 max.poll.recordsmax.poll.interval.ms 상호작용이 핵심입니다.

다음 편 예고입니다. **[Kafka 성능 ②]**에서는 브로커와 파티션 레벨 파라미터(파티션 수, num.io.threads, num.network.threads, num.replica.fetchers, 세그먼트·페이지 캐시)를 다룹니다. **[Kafka 성능 ③]**에서는 OS 레벨 튜닝(파일 디스크립터, vm.dirty_ratio, 네트워크 스택)과 워크로드별 결합 프로파일을 묶어 마무리합니다.

참고 자료


— Data Dynamics 엔지니어링 팀