Kafka Producer·Consumer 지연 문제 분석 및 해결(ing) (RPS 1000 부하 테스트)

콜 파머가 될 남자·2025년 11월 9일

성장하기

목록 보기
10/11

rps500의 부하는 견뎠지만 rps1000에서 장애가 발생하였고, 성능 개선을 위해 실험한 결과를 정리한 포스팅입니다.

RPS 500

Grafana dashboard

K6

RPS 1000

Grafana dashboard

K6

성능 테스트 결과 정리 표

구분500 RPS1000 RPS변화
p95 응답 시간7.95 ms61.2 ms🔺 약 7.7배 증가
평균 응답 시간4.77 ms36.0 ms🔺 약 7.5배 증가
Kafka Producer 지연200 ~ 300 ms 내외2000 ~ 9000 ms 폭증🚨 명확한 병목 지점
Kafka Consumer 처리속도안정 (1.5 ~ 2 ms 수준)10 ms 이상으로 증가Producer→Consumer 전달지연 누적
DB 커넥션 사용률20 ~ 30 %40 ~ 50 %정상 범위지만 확실히 부하 상승
Thread Pool 사용률15 %15 % 유지Async 풀은 여유 있음

해당 테스트 결과에서 알 수 있듯, DBCP 커넥션 사용률이 증가하였고, 응답 시간과 Kafka Producer 지연이 확인되었습니다.

그리고 궁금한 점으로 Kafka Consumer 처리속도가 평균 1.5ms -> 10ms 이상으로 증가하였는데 처리 속도가 빠르면 좋은거 아닌가? 왜 병목이 생기지 생각하였습니다.

Kafka Consumer 처리속도 promql은 아래와 같습니다.

(
  rate(spring_kafka_listener_seconds_sum{name="org.springframework.kafka.KafkaListenerEndpointContainer#0-0"}[1m])
  /
  rate(spring_kafka_listener_seconds_count{name="org.springframework.kafka.KafkaListenerEndpointContainer#0-0"}[1m])
) * 1000

생각해보면... 처리속도가 늘어났다는건 처리하는데 Consumer가 소비하는데 시간이 더 오래걸린다는 뜻이므로 성능이 저하된것이 맞았습니다.

Producer는 메시지를 계속 보내는데, Consumer가 늦게 처리하니까 Broker 큐에 메시지가 쌓이기 시작한다.

라고 받아들였습니다.

하지만 Spring 서버에서 아래와 같은 예외를 확인하였습니다.

문제의 시작: Producer TimeoutException

부하 테스트 중 org.apache.kafka.common.errors.TimeoutException: Expiring record(s) 예외가 반복적으로 발생하였습니다.
특이한 점은, Consumer lag은 0인데도 Producer에서만 timeout이 발생했다는 것이였습니다.

즉, Kafka Consumer는 정상적으로 메시지를 소비 중인데, Producer가 Broker로 메시지를 전송하다가 타임아웃으로 실패하는 상황이라고 생각합니다.

혹시나해서 Consumer 튜닝을 먼저 진행해봤습니다.

1. 병렬 처리 시도 — Consumer 파티션 확장

처음에는 Consumer의 처리 성능(p95 latency) 을 높이기 위해 병렬 처리를 하기 위해
기존 1개였던 파티션을 3개로 확장하고 3개의 Consumer를 병렬로 실행하였습니다.


그런데 예상과 달리 여전히 하나의 Consumer만 메시지를 처리하는것을 확인하였습니다.
이는 Producer에서 articleId를 Kafka send key로 사용하고 있었기 때문이였습니다.

Kafka는 같은 key를 가진 메시지는 항상 같은 partition으로 보내게되고,
즉, articleId가 같아서 3개의 파티션 중 하나의 파티션에만 메시지가 몰린것입니다.

그 결과, Consumer 한 개만 일하게 되고 나머지는 대기 상태에 빠졌습니다.

2. Key 제거 — 병렬 처리는 됐지만 무결성은 깨졌다

이번엔 key를 제거한 채로 테스트를 진행하였습니다.

이 경우, 메시지는 라운드 로빈 방식으로 각 파티션에 고르게 분배되었고
덕분에 3개의 Consumer가 병렬로 메시지를 처리하기 시작했습니다.

하지만 곧 새로운 문제가 발생했는데,
동일한 articleId에 대한 like/unlike 요청이 서로 다른 Consumer에서 동시에 처리되며 DB의 unique 제약 조건을 위반하여 데이터 무결성 예외가 발생하였습니다.

즉, 병렬 처리는 성공했지만, 논리적 일관성이 깨졌고, 같은 key에 대한 순서 보장과 원자성이 사라지게 되었습니다.

결국 이 방법은 포기하고, 다시 key 기반 구조로 되돌렸습니다.

3. Broker 설정 변경 — I/O 확장 시도

현재 지식의 한계로 Consumer의 설정을 여기까지 하고, 브로커 레벨의 성능 병목을 의심하였습니다.
근본적으로 Producer -> Broker 전송시 TimeOut이 발생하였기 때문입니다.

Kafka의 내부 네트워크 및 I/O 스레드 수와 내부 버퍼를 다음과같이 확장하였습니다.

  • KAFKA_CFG_NUM_NETWORK_THREADS=8
  • KAFKA_CFG_NUM_IO_THREADS=16
  • KAFKA_CFG_SOCKET_SEND_BUFFER_BYTES=1048576
  • KAFKA_CFG_SOCKET_RECEIVE_BUFFER_BYTES=1048576
  • KAFKA_CFG_SOCKET_REQUEST_MAX_BYTES=104857600

결과적으로 전송 처리량은 약간 개선되었지만, TimeoutException은 여전히 잔존하였습니다...

4. Producer 설정 튜닝 — RecordAccumulator 최적화

마지막으로, Producer 설정을 집중적으로 변경하였습니다.

  • acks=all
  • enable.idempotence=true
  • linger.ms=5
  • batch.size=128KB
  • buffer.memory=512MB
  • max.in.flight.requests.per.connection=1

현재 브로커는 한 대로 구성하였기 때문에 ack 값을 1, all 같은 의미일 것입니다.
linger.ms로 배치 주기를 조정하고, batch.sizebuffer.memory로 버퍼 효율을 개선했지만, 여전히 일부 batch가 expire되어 TimeoutException이 발생하였습니다.

원인 분석: RecordAccumulator와 단일 스트림의 한계

결과적으로 다음 사실이 명확해졌습니다.

  • Consumer lag = 0 → 브로커와 Consumer는 정상.
  • Timeout은 Producer 내부에서만 발생.

즉, 문제는 RecordAccumulator → Sender 전송 구간의 지연이였습니다.

특히 max.in.flight.requests.per.connection=1 설정이 결정적인 역할을 한다고 생각합니다.
이 설정은 순서 보장(멱등성)을 위해 한 번에 하나의 요청만 Broker로 보내게 됩니다.
ACK를 받기 전에는 다음 배치를 보내지 않으므로,
hot key(articleId 하나에 RPS 1000)가 몰릴 경우, 병목이 심화된 것입니다.

즉, 단일 파티션 구조에서 ack가 늦어지는 순간,
뒤에 쌓인 batch들은 delivery.timeout.ms를 초과하며 expire 되었습니다.

실험 결과 요약

구분조치결과
Consumer 병렬화파티션 3개 / key 유지병렬화 실패 (1 consumer만 작동)
Key 제거병렬화 성공, 무결성 깨짐데이터 무결성 에러
Broker 튜닝네트워크 및 I/O 스레드 확장전송속도 소폭 개선, timeout 지속
Producer 튜닝linger/batch/buffer 확장p95 개선, 일부 timeout 잔존

배운 점

  1. Kafka의 병목은 Cousumer의 lag=0이어도 timeout이 Producer에서 발생할 수 있다.
  2. key 기반 파티셔닝은 순서를 보장하지만 병렬성을 제한한다.
    특히 단일 key가 존재할 경우 병목이 불가피한 것 같다
    (? 불확실
    단일키를 여러 파티션으로 나누는 방법이 있는지?
    key를 지정하지 않고 파티션을 나눴을때, 병렬처리로 인해 서로다른 스레드의 DB 접근시 데이터 무결성 예외가 발생했지만 개선할 여지가 있는지?
  3. 실제 트래픽 패턴이 중요하다.
    articleId가 다양하게 분산된 실제 환경에서는 병목이 거의 발생하지 않는다. (이 상황에서도 하나의 article에 1000rps는 부하가 있을 법 하다)
profile
꾸준함 빼면 시체

0개의 댓글