
특정 다운로드 기능에서 이상한 현상이 발생했다.
약 29만 건의 데이터를 처리하는 로직이었는데, 로그를 확인해보니 약 1,000만 건이 처리된 것처럼 기록되어 있었다.
단순 계산으로도 약 34배의 중복 처리가 발생한 셈이다.
처리 시간 역시 비정상적이었다.
29만 건을 처리하는 데 총 5시간 40분이 소요되었고, 로그 상으로는 같은 메시지를 계속 반복 처리하고 있는 흔적이 보였다.
Kafka 메시지 상태를 모니터링해보니, Consumer가 동일한 메시지를 반복 처리하다가 결국 다음 메시지로 진행하지 못하고 멈춘 것처럼 보이는 현상이 발생하고 있었다.
문제의 원인은 Kafka Consumer Rebalancing이었다.
Kafka에서 Consumer Group은 여러 Consumer가 하나의 토픽을 나눠 처리하도록 설계된 구조다.
이때 각 Consumer는 하나 이상의 파티션(Partition) 을 할당받아 메시지를 소비한다.
Rebalancing은 이 Consumer Group 내부에서 “어떤 Consumer가 어떤 파티션을 처리할지”
를 다시 결정하는 과정이다.
쉽게 말해, 팀원 중 누군가 자리를 비우거나 새로 합류하면 남은 업무를 다시 나누는 것과 같다.
Rebalancing이 발생하면 Kafka는 다음과 같은 일을 수행한다.
이 과정 자체는 정상적인 동작이지만,
Rebalancing이 너무 자주 발생하거나, 처리 시간이 긴 Consumer 로직과 결합되면 치명적인 문제가 된다.
Rebalancing은 다음과 같은 상황에서 발생한다.
이번 이슈는 3번 케이스에 해당했다.
Kafka는 Consumer가 살아 있는지를 판단하기 위해 두 가지 기준을 사용한다.
session.timeout.ms)session.timeout.ms) 동안 도착하지 않으면 Consumer가 죽었다고 판단max.poll.interval.ms)poll()을 주기적으로 호출해야 한다.poll() 이후 max.poll.interval.ms 시간 동안 다음 poll()이 호출되지 않으면 “이 Consumer는 일을 못 하고 있다”고 판단한다.이번 케이스의 흐름은 다음과 같다.
poll()로 메시지를 가져옴poll() 호출이 지연됨여기서 중요한 포인트가 있다.
Kafka는 Consumer 애플리케이션의 로직을 강제로 중단시키지 않는다.
즉, Kafka 입장에서는 이 Consumer는 죽었으니 파티션을 회수 하지만 애플리케이션 입장에서는 이미 받아온 메시지를 끝까지 처리 하는 엇갈린 상태가 발생한다.
그리고 이 시점에 offset 커밋이 되지 않았다면, Rebalancing 이후 새로 파티션을 할당받은 Consumer는 같은 offset부터 다시 메시지를 소비하게 된다.
결과적으로, 기존 Consumer는 메시지를 끝까지 처리 하고, 새로운 Consumer는 같은 메시지를 다시 처리 하는 상황이 발생 하게 되었다.
이 상황이 반복되면서 29만 건의 메시지가 Rebalancing 마다 다시 소비되었고, 그 결과 약 34회 중복 처리되어 1,000만 건으로 기록된 것이었다.
외형적으로는 Consumer가 멈춘 것처럼 보였지만,
실제로는 Rebalancing → 재처리 → 또 Rebalancing의 무한 루프에 빠져 있었던 셈이다.
나는 offset을 이동 시켜서 일단 급한 문제를 해결 하였고, max.poll.interval.ms 값을 별도로 설정 및 로직을 최적화 하여 해당 문제가 재발 안하도록 조치 하였다.
단 이 때 max.poll.interval.ms 값을 무작정 크게 설정하면 진짜 장애 상황에서도 Consumer를 늦게 감지하게 되므로 로직 처리 시간에 맞춰 신중하게 조정해야 한다.
Kafka Consumer Rebalancing은 장애가 아니라 정상적인 동작이지만, Consumer 로직이 무거워질수록 운영 리스크로 변한다.
이번 이슈를 통해 느낀 점은 다음과 같다.
Kafka는 메시지를 책임지지만,
Consumer의 생명주기는 개발자가 책임진다.