Kafka Consumer 설정을 개선하는 과정에서 겪은 문제를 정리한다.
한 문제를 해결하려고 설정을 바꾸면 다른 문제가 생기는 상황이 이어졌다.
1. 리밸런싱이 너무 자주 일어난다
운영 중인 Kafka Consumer들이 리밸런싱이 너무 자주 발생하는 문제가 있었다.
배포할 때마다, 컨테이너가 재시작될 때마다 리밸런싱이 발생하면서:
- 일시적인 consume 중단
- 메시지 처리 지연
- 로그에 쏟아지는 리밸런싱 관련 경고
원인은 Dynamic Membership 특성이었다.
Consumer가 종료되면 즉시 Rebalance가 발생하기 때문이다.
2. 첫 번째 시도: Static Membership 도입
리밸런싱 문제를 해결하기 위해 Static Membership을 도입했다.
GROUP_INSTANCE_ID_CONFIG를 설정하면 Static Membership이 활성화된다:
// 각 Consumer마다 고유한 ID 부여
// topicName: 해당 Consumer가 구독하는 토픽 이름
ConsumerConfig.GROUP_INSTANCE_ID_CONFIG to "${topicName}-${UUID.randomUUID()}"Static Membership의 장점
- Consumer 종료 →
session.timeout.ms까지 파티션 유지 - 같은 ID로 재연결하면 즉시 기존 파티션 할당
- 롤링 배포 시 불필요한 rebalance 최소화
처음에는 문제가 없어 보였다.
3. UUID가 매번 바뀐다
ConsumerConfig.GROUP_INSTANCE_ID_CONFIG to "${topicName}-${UUID.randomUUID()}"이 코드에는 함정이 있다.
서버가 재시작될 때마다 UUID가 새로 생성된다.
결과:
- 매번 다른 ID로 연결 → 결국 리밸런싱 발생
- Static Membership의 장점이 전혀 발휘되지 않음
- 오히려 session timeout 동안 이전 ID가 남아 혼란만 커짐
4. 두 번째 시도: 모든 Consumer에 동일한 ID
"그러면 모든 Consumer가 같은 고정 ID를 쓰면 되지 않을까?"
// 모든 Consumer가 동일한 ID 사용
ConsumerConfig.GROUP_INSTANCE_ID_CONFIG to KafkaGroup.KotlinIntegrationServer
// 값: "KotlinIntegrationServer"이렇게 바꾸자 더 큰 문제가 발생했다.
5. 특정 토픽만 consume이 안 된다
FcmPushTopic → 정상 동작
SendKotlinFcmTopic → 메시지 consume 안 됨분명 같은 Consumer Group인데, 하나는 되고 하나는 안 된다.
kafka-console-consumer로 확인하면 메시지는 분명히 존재한다:
{"projectCode":"GSL","regionCode":"eu","pushHistoryId":299}하지만 애플리케이션 로그에는 consume 관련 로그가 전혀 찍히지 않았다.
원인 분석
모든 consumer가 동일한 GROUP_INSTANCE_ID를 사용하면:
- 두 consumer가 같은 ID로 Join 요청
- Kafka는 "같은 consumer가 두 번 연결했다"고 판단
- 한 consumer만 파티션을 할당받고, 나머지는 할당받지 못함
FcmPushTopic은 먼저 연결된 consumer가 할당받아서 동작SendKotlinFcmTopic은 할당받지 못해서 consume이 안 됨
시간순 시나리오
| 시점 | 동작 |
|---|---|
| T1 | 서버 시작, FcmPushConsumer 연결 (ID: "KotlinIntegrationServer") |
| T2 | Kafka: "KotlinIntegrationServer 연결됨, FcmPushTopic 파티션 할당" |
| T3 | SendFcmTopicConsumer 연결 (ID: "KotlinIntegrationServer") |
| T4 | Kafka: "같은 ID가 또 연결? Rebalance..." |
| T5 | 결과: 일부 토픽만 파티션 할당됨 |
6. GROUP_INSTANCE_ID 제거
결국 가장 단순한 해결책으로 돌아왔다.
// 변경 전
ConsumerConfig.GROUP_INSTANCE_ID_CONFIG to KafkaGroup.KotlinIntegrationServer
// 변경 후: 해당 설정 삭제
// Static Membership 비활성화 → Dynamic Membership 사용Static Membership을 포기하고 Dynamic Membership으로 복귀했다.
리밸런싱이 잦은 문제에는:
session.timeout.ms조정max.poll.interval.ms조정- Consumer 로직 최적화
로 대응하기로 했다.
7. Uncommitted Transaction
첫 번째 문제를 해결했는데도 특정 메시지 하나가 계속 consume되지 않는 현상이 있었다.
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
KotlinIntegrationServer SendKotlinFcmTopic 0 351 352 1LAG이 1인데 아무리 기다려도 줄어들지 않는다.
원인
Producer가 트랜잭션을 사용하고 있었다:
// Producer
ProducerConfig.TRANSACTIONAL_ID_CONFIG to "TX-Integration-KotlinServer-${UUID.randomUUID()}"
// Consumer
ConsumerConfig.ISOLATION_LEVEL_CONFIG to "read_committed"read_committed isolation level에서 uncommitted 메시지는 영원히 읽히지 않는다.
Offset Reset
# Consumer group 상태 확인 (STATE가 Empty가 될 때까지 대기)
kafka-consumer-groups.sh --bootstrap-server <broker> \
--group KotlinIntegrationServer --describe
# Offset reset
kafka-consumer-groups.sh --bootstrap-server <broker> \
--group KotlinIntegrationServer \
--topic SendKotlinFcmTopic \
--reset-offsets --to-offset 352 --execute8. 결과 및 교훈
| 항목 | Before | After |
|---|---|---|
| SendKotlinFcmTopic 수신률 | 0% | 100% |
| LAG | 1 (stuck) | 0 |
| 파티션 미할당 | 발생 | 해결 |
모든 Consumer가 같은 ID를 쓰면 Kafka는 이를 같은 consumer의 중복 연결로 판단하고, UUID를 붙이면 재시작할 때마다 ID가 바뀌어 Static Membership의 장점이 사라진다.
트랜잭션 Producer와 read_committed Consumer 조합도 주의해야 한다.
commit되지 않은 메시지는 영원히 consume되지 않으므로, LAG이 줄지 않을 때는 kafka-consumer-groups.sh로 consumer group 상태부터 확인하는 게 빠르다.
수정 요약
| 파일 | 변경 내용 |
|---|---|
KafkaConfiguration.kt | GROUP_INSTANCE_ID_CONFIG 제거 |
KafkaConfiguration.kt | Error Handler 추가 |
MessagePublisher.kt | token.isNullOrEmpty() 체크 추가 |
마무리
"리밸런싱이 자주 발생한다" → "Static Membership으로 해결하자" → "ID를 어떻게 설정하지?" → "동일 ID 쓰면 되겠지" → "특정 토픽이 consume 안 된다?!"
여러 Kafka 문제를 해결하는 과정이었다.
결국 핵심은 Static Membership을 쓰려면 각 Consumer가 고유하면서도 재시작 시 동일한 ID를 가져야 한다는 점이다.
이게 쉽지 않아서 우리는 Dynamic Membership으로 돌아갔다.