Kafka Static Membership 트러블슈팅 - 고유 ID와 동일 ID 설정의 트레이드오프
Kafka Consumer 설정을 개선하는 과정에서 겪었던 문제들을 정리한다. 한 문제를 해결하려고 수정했더니 또 다른 문제가 발생하고, 그걸 해결하니 또 다른 문제가 생기는 상황을 겪었다.
1. 발단: 리밸런싱이 너무 자주 일어난다
운영 중인 Kafka Consumer들이 리밸런싱이 너무 자주 발생는 문제가 있었다.
배포할 때마다, 컨테이너가 재시작될 때마다 리밸런싱이 발생하면서:
- 일시적인 consume 중단
- 메시지 처리 지연
- 로그에 쏟아지는 리밸런싱 관련 경고
원인은 Dynamic Membership 특성이었다. Consumer가 종료되면 즉시 Rebalance가 발생하기 때문.
2. 첫 번째 시도: Static Membership 도입
리밸런싱 문제를 해결하기 위해 Static Membership 도입했다.
GROUP_INSTANCE_ID_CONFIG를 설정하면 Static Membership이 활성화된다:
// 각 Consumer마다 고유한 ID 부여
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 관련 로그가 전혀 찍히지 않았습니다.
원인 분석
flowchart LR
subgraph GROUP["Consumer Group"]
A["FcmPushConsumer<br/>ID: KotlinIntegrationServer"]
B["SendFcmTopicConsumer<br/>ID: KotlinIntegrationServer"]
end
A --> C["동일한 ID!"]
B --> C
C --> D["Kafka: 같은 consumer로 인식"]
style C fill:#ef4444,color:#fff,stroke:#dc2626
style D fill:#f97316,color:#fff,stroke:#ea580c모든 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 1
LAG이 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 |
| 파티션 미할당 | 발생 | 해결 |
얻은 것
-
Static Membership의 GROUP_INSTANCE_ID는 Consumer별로 고유해야 한다
- 모든 Consumer가 같은 ID를 쓰면 Kafka가 같은 consumer로 인식
- UUID를 쓰면 재시작 시 ID가 바뀌어서 Static Membership 장점 상실
-
Static Membership이 만능은 아니다
- 구현 복잡도가 올라감
- 잘못 설정하면 오히려 문제 발생
- 심플하게 Dynamic Membership + 설정 튜닝이 나을 수 있음
-
트랜잭션 + read_committed 조합 주의
- uncommitted 메시지는 영원히 consume 불가
- Kafka Producer 호출 시 트랜잭션 commit 여부 확인 필요
-
kafka-consumer-groups.sh는 디버깅의 친구
- LAG, CURRENT-OFFSET, LOG-END-OFFSET 확인
- Consumer group 상태 (Active/Empty/Dead) 모니터링
수정 요약
| 파일 | 변경 내용 |
|---|---|
KafkaConfiguration.kt | GROUP_INSTANCE_ID_CONFIG 제거 |
KafkaConfiguration.kt | Error Handler 추가 |
MessagePublisher.kt | token.isNullOrEmpty() 체크 추가 |
마무리
"리밸런싱이 자주 발생한다" → "Static Membership으로 해결하자" → "ID를 어떻게 설정하지?" → "동일 ID 쓰면 되겠지" → "특정 토픽이 consume 안 된다?!"
여러 카프카 문제들을 해결하는 과정이었다.
결국 핵심은 Static Membership을 쓰려면 각 Consumer가 고유하면서도 재시작 시 동일한 ID를 가져야 한다는 점이다. 이게 쉽지 않아서 우리는 Dynamic Membership으로 돌아갔다.
여러분의 Kafka 트러블슈팅에 도움이 되기를 바란다.
참고 자료
Loading comments...