Kafka 컨슈머 lag 모니터링: 폭발하던 파이프라인을 빠르게 정상화한 실전 튜닝 가이드
카프카(Kafka) 컨슈머 그룹의 lag이 빠르게 치솟는 상황, 정말 골치 아프시죠? 프로듀서는 정상적으로 데이터를 보내고 있는데 컨슈머가 따라가지 못해 실시간성이 무너지고, 급기야 적재 지연으로 이어지는 막막함은 운영을 해본 사람만 공감할 수 있습니다. 대용량 스트리밍 파이프라인을 운영하다 보면 lag 그래프가 직선으로 상승하는 모습을 보며 원인 분석에 몇 시간을 허비한 경험이 한 번쯤 있을 텐데요. 이 글에서는 제가 실제로 경험한 Kafka 컨슈머 lag 모니터링 이슈를 바탕으로, 폭발하던 파이프라인을 빠르게 정상화했던 과정과 핵심 체크리스트를 공유합니다. 단순히 파티션 수를 늘리는 임시방편이 아닌, 근본 원인을 찾아 해결하는 실전 노하우를 확인해 보세요.
- 증상: Kafka 컨슈머 lag 급증, 실시간 적재 지연.
- 1차 원인: 컨슈머 poll() 호출 간격 초과로 인한 리밸런스 반복.
- 2차 원인: 단일 레코드의 대용량 페이로드로 인한 batch.size 및 처리 시간 초과.
- 해결책: fetch.max.bytes 및 max.poll.interval.ms 튜닝과 함께 워커 스레드 병렬화.
- 결과: lag 약 120만 건 → 0건, 적재 지연 시간 기준 환경에서 약 95% 감소.
1. 증상 파악: 단순히 느린 것이 아니라 ‘멈춤’에 가까웠다
문제가 발생한 파이프라인은 트위터의 실시간 트렌드 데이터를 수집해 Elasticsearch에 적재하는 구조였습니다. 평소 시간당 약 200만 건의 이벤트를 무리 없이 처리했지만, 특정 대형 이벤트가 터진 뒤 컨슈머 로그에 리밸런스가 반복되는 패턴이 나타나기 시작했습니다.
일반적인 성능 저하와 달리 CPU 및 메모리 사용률은 높지 않았지만 처리량이 0에 수렴하는 특이한 현상이 발생했습니다. 리소스가 부족해서 처리하지 못하는 상황이라기보다, 특정 스레드가 블로킹되면서 컨슈머 그룹 전체가 멈춘 형태였습니다.
2. 첫 번째 시도: 파티션 수 증가는 오히려 독이 되었다
저도 처음에는 단순히 처리 용량이 부족하다고 판단하고 컨슈머 그룹의 파티션 수를 12개에서 24개로 늘렸습니다. 하지만 각 파티션에서 비동기로 커밋을 수행하는 과정에서 KafkaConsumer가 단일 스레드에서만 안전하다는 제약을 위반하며 ConcurrentModificationException이 발생했습니다. 결과적으로 리밸런스 주기가 더 짧아지고, 오히려 상황이 악화되었습니다.
| 구분 | 파티션 12개 (기존) | 파티션 24개 (증설) | 튜닝 후 (24개 + 설정 변경) |
|---|---|---|---|
| 적재 지연 시간 | 약 3분 | 약 15분 (악화) | 약 8초 |
| 컨슈머 리밸런스 횟수 | 1시간에 1회 | 5분에 3회 | 1시간에 0회 |
| 커밋 실패율 | 0.1% | 12.5% | 0.0% |
3. 근본 원인 분석: poll() 메서드의 함정
파티션 수를 늘린 것이 실패한 후, 컨슈머의 핵심 로직을 다시 살펴봤습니다. 문제의 원인은 생각보다 단순했습니다. 각 레코드의 평균 크기가 15KB에서 120KB로 급증하면서 max.poll.records에 설정된 500건을 처리하는 데 걸리는 시간이 크게 늘어났고, 대형 이벤트가 겹칠 때는 poll() 호출 간격이 max.poll.interval.ms 기본값(5분)을 넘어섰습니다. Kafka는 일정 시간 동안 poll()을 호출하지 않는 컨슈머를 그룹에서 제외하고, 이로 인해 리밸런스가 반복되면서 lag이 급증했습니다.
토픽 특성상 특정 대형 이벤트가 발생하면 단일 배치에 수십 MB의 JSON이 몰릴 수 있었고, 이를 파싱하고 Elasticsearch 벌크 인덱싱을 수행하는 과정에서 처리 지연이 발생했습니다.
3-1. 핵심 파라미터 진단 포인트
이 문제를 해결하기 위해 단순히 타임아웃 값을 늘리는 대신, 데이터 처리 흐름 자체를 비동기로 전환했습니다. poll()로 가져온 레코드를 별도의 워커 스레드 큐에 전달하고, 오프셋 커밋은 데이터 정합성을 확인할 수 있는 시점에 수행하는 전략을 사용했습니다. 아래 표는 실제 적용한 컨슈머 설정값입니다.
| 설정 항목 | 기존 값 | 변경 값 | 변경 이유 |
|---|---|---|---|
| max.poll.interval.ms | 300,000 (5분) | 600,000 (10분) | 재처리 시간 마진 확보 |
| max.poll.records | 500 | 100 | 대용량 레코드 고려하여 축소 |
| fetch.max.bytes | 52,428,800 (50MB) | 134,217,728 (128MB) | 단일 fetch에서 더 많은 데이터 처리 |
| enable.auto.commit | false | false (유지) | 수동 커밋으로 정합성 유지 |
Kafka 컨슈머 lag 모니터링을 빠르게 확인하려면 다음 명령어를 사용할 수 있습니다. kafka-consumer-groups.sh --describe --group my-consumer-group --bootstrap-server broker:9092 이때 각 파티션의 LAG과 LOG-END-OFFSET을 함께 보면 특정 파티션에만 지연이 쏠리는지, 전체적으로 지연되는지 구분할 수 있습니다.
4. 병렬 처리와 백프레셔 제어로 정상화
설정값 변경과 함께 가장 효과를 본 것은 레코드 처리 로직을 ExecutorService 기반의 병렬 워커로 분리한 점입니다. 기존에는 싱글 스레드로 500개의 레코드를 순차적으로 적재했다면, 이제는 큐에 담긴 데이터를 여러 워커 스레드가 동시에 처리합니다. 그 결과 Kafka 컨슈머 lag이 빠르게 감소했습니다.
동시에 백프레셔(Backpressure) 문제를 해결하기 위해 Semaphore를 도입해 임계치 이상의 데이터가 쌓이면 poll()을 일시 중지시키는 로직을 추가했습니다. 이때 Semaphore의 동시 허용 수는 max.poll.records와 워커 큐 용량을 함께 고려해서 정해야 합니다. 이렇게 운영하니 컨슈머는 더 이상 리밸런스에 시달리지 않았고, lag 수치는 약 2시간 만에 0에 가까워졌습니다.
5. 결론: lag 폭발의 핵심은 설정보다 설계
이번 경험을 통해 깨달은 점은, 카프카 컨슈머 lag이 폭발했을 때 가장 먼저 의심해야 할 것은 파티션 수나 하드웨어 스펙이 아니라 컨슈머의 poll() 처리 시간과 커밋 전략이라는 것입니다. 특히 대용량 페이로드를 다루는 스트리밍 파이프라인이라면 max.poll.records를 줄이고, 레코드 처리와 커밋을 분리하는 구조를 적극적으로 고려해볼 필요가 있습니다.
실무에서 빠르게 대응하려면 다음 3단계를 권장합니다. 첫째, 리밸런스 로그가 찍히는지 확인하세요. 둘째, poll()이 반환되는 주기를 모니터링하세요. 셋째, 타임아웃을 늘리기 전에 레코드 처리 방식을 비동기로 바꾸는 리팩토링을 먼저 검토하세요. 이 순서만 지켜도 Kafka 컨슈머 lag 폭발 문제의 상당 부분을 해결할 수 있습니다. 지금 lag 그래프가 상승하고 있다면, 무작정 스케일업부터 하지 말고 소스 코드의 poll() 로직부터 점검해 보세요.