rps500의 부하는 견뎠지만 rps1000에서 장애가 발생하였고, 성능 개선을 위해 실험한 결과를 정리한 포스팅입니다.
| 구분 | 500 RPS | 1000 RPS | 변화 |
|---|---|---|---|
| p95 응답 시간 | 7.95 ms | 61.2 ms | 🔺 약 7.7배 증가 |
| 평균 응답 시간 | 4.77 ms | 36.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 서버에서 아래와 같은 예외를 확인하였습니다.
부하 테스트 중 org.apache.kafka.common.errors.TimeoutException: Expiring record(s) 예외가 반복적으로 발생하였습니다.
특이한 점은, Consumer lag은 0인데도 Producer에서만 timeout이 발생했다는 것이였습니다.
즉, Kafka Consumer는 정상적으로 메시지를 소비 중인데, Producer가 Broker로 메시지를 전송하다가 타임아웃으로 실패하는 상황이라고 생각합니다.
혹시나해서 Consumer 튜닝을 먼저 진행해봤습니다.
처음에는 Consumer의 처리 성능(p95 latency) 을 높이기 위해 병렬 처리를 하기 위해
기존 1개였던 파티션을 3개로 확장하고 3개의 Consumer를 병렬로 실행하였습니다.

그런데 예상과 달리 여전히 하나의 Consumer만 메시지를 처리하는것을 확인하였습니다.
이는 Producer에서 articleId를 Kafka send key로 사용하고 있었기 때문이였습니다.
Kafka는 같은 key를 가진 메시지는 항상 같은 partition으로 보내게되고,
즉, articleId가 같아서 3개의 파티션 중 하나의 파티션에만 메시지가 몰린것입니다.
그 결과, Consumer 한 개만 일하게 되고 나머지는 대기 상태에 빠졌습니다.
이번엔 key를 제거한 채로 테스트를 진행하였습니다.

이 경우, 메시지는 라운드 로빈 방식으로 각 파티션에 고르게 분배되었고
덕분에 3개의 Consumer가 병렬로 메시지를 처리하기 시작했습니다.
하지만 곧 새로운 문제가 발생했는데,
동일한 articleId에 대한 like/unlike 요청이 서로 다른 Consumer에서 동시에 처리되며 DB의 unique 제약 조건을 위반하여 데이터 무결성 예외가 발생하였습니다.
즉, 병렬 처리는 성공했지만, 논리적 일관성이 깨졌고, 같은 key에 대한 순서 보장과 원자성이 사라지게 되었습니다.
결국 이 방법은 포기하고, 다시 key 기반 구조로 되돌렸습니다.
현재 지식의 한계로 Consumer의 설정을 여기까지 하고, 브로커 레벨의 성능 병목을 의심하였습니다.
근본적으로 Producer -> Broker 전송시 TimeOut이 발생하였기 때문입니다.
Kafka의 내부 네트워크 및 I/O 스레드 수와 내부 버퍼를 다음과같이 확장하였습니다.
결과적으로 전송 처리량은 약간 개선되었지만, TimeoutException은 여전히 잔존하였습니다...
마지막으로, Producer 설정을 집중적으로 변경하였습니다.
현재 브로커는 한 대로 구성하였기 때문에 ack 값을 1, all 같은 의미일 것입니다.
linger.ms로 배치 주기를 조정하고, batch.size와 buffer.memory로 버퍼 효율을 개선했지만, 여전히 일부 batch가 expire되어 TimeoutException이 발생하였습니다.
결과적으로 다음 사실이 명확해졌습니다.

즉, 문제는 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 잔존 |
lag=0이어도 timeout이 Producer에서 발생할 수 있다.