[Kafka 성능 ①] 추측 말고 측정하라 — 벤치마킹과 프로듀서·컨슈머 파라미터
Kafka 성능 튜닝의 출발점은 추측이 아니라 측정입니다. kafka-producer-perf-test·kafka-consumer-perf-test로 베이스라인을 잡고, 백분위 기반으로 한 번에 한 변수씩 바꾸는 방법론, 그리고 처리량과 지연을 좌우하는 프로듀서·컨슈머 파라미터를 정리합니다.
"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 복제를 기다려야 하므로 지연이 커집니다. 하나를 당기면 다른 하나가 따라 움직입니다. 그래서 "튜닝했더니 빨라졌다"는 항상 "무엇을 희생해서"라는 질문을 동반해야 합니다.
추측의 비용
베이스라인이 없으면 다음과 같은 함정에 빠집니다.
- 확증 편향: 바꾼 파라미터가 효과 있었다고 믿고 싶어, 평균이 살짝 좋아진 것을 "개선"으로 해석합니다.
- 교란 변수: 두 파라미터를 동시에 바꾸면 어느 쪽이 효과였는지 영원히 알 수 없습니다.
- 비현실적 워크로드: 100바이트 메시지로 튜닝한 값이 운영의 10KB 메시지에서는 정반대로 작동합니다.
측정의 목적은 "정답 설정"을 찾는 게 아니라, 변경의 효과를 인과적으로 분리하는 데 있습니다. 그러려면 재현 가능한 베이스라인과 도구가 필요합니다.
2. 벤치마킹 도구
Kafka는 배포판에 성능 측정 CLI를 기본 포함합니다. 별도 설치 없이 베이스라인을 잡을 수 있습니다.
kafka-producer-perf-test.sh
프로듀서가 낼 수 있는 처리량과 지연을 측정합니다. 핵심 옵션은 다음과 같습니다.
| 옵션 | 의미 |
|---|---|
--num-records | 전송할 총 레코드 수 |
--record-size | 레코드 바이트 크기(운영과 비슷하게) |
--throughput | 목표 처리량 제한(records/sec). -1이면 무제한(포화 측정) |
--producer-props | acks, 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 1000consumer-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.size와 linger.ms를 동시에 올리고 처리량이 좋아졌다면, 둘 중 무엇이 효과였는지 알 수 없습니다. 변경은 항상 하나씩, 그리고 매번 베이스라인과 비교합니다.
운영과 닮은 워크로드
- 메시지 크기: 운영 평균/최대 크기로 측정합니다. 압축 효과는 페이로드 내용(반복성)에 크게 좌우되므로 합성 랜덤 데이터는 압축률을 왜곡합니다.
- 키 분포: 키가 한쪽 파티션에 쏠리면 핫 파티션이 생깁니다. 운영의 키 카디널리티를 모사해야 파티션 분배 효과가 드러납니다.
- 파티션 수·컨슈머 수: 병렬성은 처리량의 1차 변수이므로 운영 토폴로지를 맞춥니다.
이 루프를 한 변수씩 돌리면, 끝났을 때 각 파라미터가 처리량과 p99에 미친 영향을 표로 정리할 수 있습니다. 그게 진짜 튜닝의 산출물입니다.
4. 처리량을 움직이는 프로듀서 파라미터
프로듀서 튜닝의 깊은 분석은 별도 글 [Kafka 운영 ⑧] 프로듀서 처리량 튜닝에서 다루므로, 여기서는 벤치마크에서 무엇을 변수로 잡을지 관점에서 요약합니다.
| 파라미터 | 역할 | 올리면 |
|---|---|---|
batch.size | 파티션별 배치 버퍼 크기(바이트) | 처리량↑(왕복 감소), 채워질 때까지 약간 지연↑ |
linger.ms | 배치를 채우려 기다리는 시간 | 처리량↑, 지연↑(대기 시간만큼) |
compression.type | lz4/zstd/snappy/gzip | 네트워크·디스크↓, CPU↑. lz4는 속도, zstd는 압축률 |
buffer.memory | 프로듀서 전체 송신 버퍼 | 부족하면 send()가 블록되어 처리량 급락 |
acks | 0/1/all 확인 강도 | all은 내구성↑·지연↑, 1은 지연↓·내구성↓ |
max.in.flight.requests.per.connection | 미확인 인플라이트 요청 수 | 처리량↑, 단 순서·재시도와 상호작용 |
enable.idempotence | 멱등 프로듀서(중복·재정렬 방지) | true 권장. 정확히 한 번 전송 의미 + 안전한 재시도 |
처리량 vs 지연 한눈에
| 변경 | 처리량 | 지연(p99) | 내구성 |
|---|---|---|---|
batch.size ↑ + linger.ms ↑ | ▲ 크게 | ▼ 나빠짐 | – |
compression.type=lz4 | ▲ (네트워크 절감) | ≈ (CPU 여유 시) | – |
acks=all → acks=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 요청을 보냅니다. 브로커는 다음 규칙으로 응답을 결정합니다.
| 파라미터 | 의미 | 트레이드오프 |
|---|---|---|
fetch.min.bytes | 브로커가 응답 전 모을 최소 바이트 | 크게 → 페치 횟수↓, 처리량↑, 지연↑ |
fetch.max.wait.ms | fetch.min.bytes를 못 채워도 기다리는 상한 | min.bytes 대기의 지연 상한선. 낮추면 지연↓, 처리량↓ |
max.partition.fetch.bytes | 파티션당 한 번에 가져올 최대 바이트 | 크게 → 파티션별 처리량↑, 메모리↑ |
fetch.max.bytes | 한 fetch 요청 전체의 최대 바이트 | 응답 크기 상한. 메모리·지연과 상호작용 |
max.poll.records | poll() 한 번이 반환할 최대 레코드 수 | 처리 배치 크기. 너무 크면 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.records↔max.poll.interval.ms상호작용이 핵심입니다.
다음 편 예고입니다. **[Kafka 성능 ②]**에서는 브로커와 파티션 레벨 파라미터(파티션 수, num.io.threads, num.network.threads, num.replica.fetchers, 세그먼트·페이지 캐시)를 다룹니다. **[Kafka 성능 ③]**에서는 OS 레벨 튜닝(파일 디스크립터, vm.dirty_ratio, 네트워크 스택)과 워크로드별 결합 프로파일을 묶어 마무리합니다.
참고 자료
- Apache Kafka — Producer Configs: https://kafka.apache.org/documentation/#producerconfigs
- Apache Kafka — Consumer Configs: https://kafka.apache.org/documentation/#consumerconfigs
- Apache Kafka — Tools (
kafka-producer-perf-test,kafka-consumer-perf-test): https://kafka.apache.org/documentation/- OpenMessaging Benchmark: https://openmessaging.cloud/docs/benchmarks/
- Apache Kafka — Trogdor 테스트 프레임워크: https://github.com/apache/kafka/tree/trunk/trogdor
— Data Dynamics 엔지니어링 팀