| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- nginx
- 코테
- SQL
- 일정관리프로젝트
- 포트폴리오
- 디자인 패턴
- 알고리즘
- 연습문제
- 이것이 자바다
- CI/CD
- JPA
- 프로그래머스
- Kafka
- Redis
- LV03
- JMeter
- LV02
- spring boot
- Java
- AWS
- mysql
- docker
- LV.02
- Join
- CoffiesVol.02
- 데이터 베이스
- Lv.0
- LV01
- 일정관리 프로젝트
- LV0
- Today
- Total
코드 저장소.
Kafka KRaft 기반 3-Broker 클러스터 전환 과정 본문
목차
1. Zookeeper를 걷어낸 이유?
2. 마이그레이션 과정
3.회고
1. Zookeeper를 걷어낸 이유?
기존의 Kafka는 분산 시스템의 특성상 클러스터의 상태를 관리할 '코디네이터'가 필수적이었습니다. "어떤 브로커가 살아있는지", "누가 리더인지", "설정 정보는 무엇인지"와 같은 핵심 메타데이터를 저장할 별도의 공간이 필요했습니다. 그 역할을 오랫동안 담당해 온 것이 바로 Zookeeper입니다.
하지만 Kafka 2.8 버전부터 KRaft(Kafka Raft)가 등장하며 이 구조는 완전히 뒤바뀌었습니다. 저는 왜 이번 프로젝트에 기존 Zookeeper 기반 구조 대신 KRaft 기반 구조를 선택했는지, 그 이유를 정리해 보았습니다.
1-1. Zookeeper 방식의 한계
- 운영 복잡성의 증가 (이원화된 구조) Kafka 클러스터를 하나 운영하기 위해 사실상 Zookeeper라는 또 다른 분산 시스템 클러스터를 관리해야 했습니다. 이는 설정, 모니터링, 유지보수 포인트가 두 배로 늘어남을 의미하며 운영의 비효율성을 초래합니다.
- 메타데이터 동기화의 오버헤드 상태 정보가 변경되면 Zookeeper -> Kafka Controller -> 나머지 Broker 순으로 데이터가 전파됩니다. 외부 시스템인 Zookeeper를 거치는 이 단계에서 필연적으로 지연(Latency)이 발생하며, 클러스터 규모가 커질수록 이 동기화 과정은 시스템의 발목을 잡게 됩니다.
- 메타데이터 관리 부담 증가 Zookeeper의 성능 한계로 인해 하나의 클러스터 규모가 커질수록 외부 의존성으로 인한 메타데이터 관리 부담이 증가하며, 이는 전체 운영 복잡도를 높일 수 있습니다. 그 이상의 확장을 시도할 경우 병목 현상이 발생하여 클러스터 전체의 안정성을 해치는 원인이 되기도 했습니다.
특히 도커(Docker) 환경에서 클러스터를 구축하며 마주한 '리소스 점유' 문제도 KRaft 전환의 큰 이유였습니다. Zookeeper라는 별도의 컨테이너를 제거함으로써, 제한된 서버 자원(EC2 등) 내에서 메모리 사용량과 운영 복잡도를 줄이고자 했습니다.
2. 마이그레이션 과정
다음은 Docker-Compose에서 기존의 Zookeeper를 걷어내고 KRaft로 전환을 하는 과정입니다.
마이그레이션을 한 환경은 아래와 같습니다.
환경: AWS EC2 t3.medium
2-1. Docker-Compose 변경
우선 기존의 인프라서버에 있는 도커 컴포즈에서 쥬키퍼를 걷어낸 도커 컴포즈에서 핵심만 추린 부분입니다.
# 핵심 설정 요약
environment:
KAFKA_NODE_ID: 1 # 각 브로커를 식별하는 고유 ID
KAFKA_PROCESS_ROLES: 'broker,controller' # 브로커와 컨트롤러 역할을 동시에 수행 (Combined Mode)
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093' # 투표권을 가진 노드 리스트
CLUSTER_ID: 임의의 클러스터링 ID # 모든 노드가 공유해야 하는 클러스터 고유 식별자
KAFKA_MIN_INSYNC_REPLICAS: 2 # 데이터 안정성을 위한 최소 ISR 수
KAFKA_HEAP_OPTS: "-Xmx256m -Xms256m" # t3.medium 환경에 맞춘 메모리 최적화
위의 환경설정에 대한 설명은 아래와 같습니다.
- Combined Mode 활용 (broker,controller)
- 별도의 컨트롤러 노드를 두지 않고, 3개의 노드가 브로커와 컨트롤러 역할을 동시에 수행하도록 설정했습니다. 이는 관리 포인트가 줄어드는 KRaft의 장점을 극대화하며, 리소스가 제한적인 EC2 환경에서 효율적입니다.
- Quorum Voters의 명시적 지정
- 9093 포트를 컨트롤러 전용 리스너로 할당하고, KAFKA_CONTROLLER_QUORUM_VOTERS를 통해 3대의 브로커가 서로 투표를 주고받을 수 있도록 통로를 열어주었습니다.
- Cluster ID의 일치
- KRaft에서는 클러스터에 참여하는 모든 브로커가 동일한 CLUSTER_ID를 공유해야 합니다. 이를 환경 변수로 고정하여 초기 포맷팅 과정에서의 실수를 방지했습니다.
- 메모리 최적화 (HEAP_OPTS)
- AWS t3.medium 인스턴스는 메모리가 넉넉하지 않습니다. Kafka의 기본 힙 사이즈를 그대로 쓰면 OOM(Out Of Memory)이 발생할 수 있어, 각 브로커당 256MB로 제한하여 3대의 브로커가 안정적으로 돌아갈 수 있는 환경을 만들었습니다.
2-2. 기존 Zookeeper의 메타 데이터를 삭제
Zookeeper와 KRaft는 데이터를 저장하는 바이너리 구조가 근본적으로 다릅니다. 운영 환경이 아닌 개발/테스트 단계였기에, docker-compose down -v 명령어를 통해 기존 볼륨을 완전히 초기화하고 새 인프라에 맞는 데이터 구조를 생성했습니다.
2-3. 변경된 도커를 구동 및 KRaft의 작동 과정
다음으로는 docker-compose -f docker-compose.infra.yml up -d 로 어떻게 작동이 되는지를 확인을 해보겠습니다.
2-3-1.컨트롤러 쿼럼(Quorum)의 생성
컨테이너가 기동되자마자 자신이 어떤 클러스터에 속해 있는지 확인하고 관리자(Controller) 역할을 준비합니다.

- 로그 분석: 컨테이너가 뜨자마자 QuorumController id=1이 생성되면서, 컴포즈에 설정했던 CLUSTER_ID(MkU3O...)를 정상적으로 인식합니다.
- 작동 원리: 이 ID가 일치하지 않으면 브로커는 클러스터에 합류할 수 없습니다. 로그를 통해 새로운 컨트롤러가 고유 ID를 가지고 미션 수행을 시작했음을 알 수 있습니다.
2-3-2.리더 선출을 위한 'Raft' 합의 과정
모든 노드가 동시에 준비되지 않기 때문에, 누가 대장(리더)이 될지 정하는 과정에서 투표와 거절이 반복됩니다.

- 로그 분석: node.id=1 브로커가 리더가 되기 위해 투표를 요청했지만, 다른 브로커(2, 3번)가 아직 준비되지 않아 정족수(Quorum)를 채우지 못하고 거절(REJECTED)되었습니다.
- 작동 원리: 이는 분산 시스템에서 아주 정상적인 과정입니다.

- 로그 내용: Completed transition to FollowerState... leaderId=3
- 작동 원리: 곧이어 재투표를 통해 3번 브로커가 리더로 선출되었습니다. 모니터링 중인 1번 브로커는 리더를 따르는 팔로워(Follower) 상태가 되며 클러스터가 안정화됩니다.
2-3-3. 브로커 등록 및 최종 가용 상태 전환
리더 선출이 완료되면 각 브로커는 자신의 가용 상태를 컨트롤러에 등록합니다.

- 로그 분석: 이제 브로커 1은 클러스터의 정식 일원이 되어 메시지를 처리할 준비가 되었음을 선언합니다.
- 작동 원리: Zookeeper 방식에서는 이 정보가 외부 시스템에 저장되었지만, KRaft에서는 내부 메타데이터 로그(__cluster_metadata)에 직접 기록되어 관리 경로가 단순해졌습니다.
2-3. 최종 구축 결과 확인
3대의 브로커가 모두 정상적으로 통신하고 있는지 최종 점검합니다.



- 설명: 하나의 호스트 IP 위에서 서로 다른 포트(9092, 9094, 9096)를 가진 3대의 브로커가 유기적으로 연결된 것을 확인할 수 있습니다.
3. 회고
- 브로커 강제 종료 테스트: 리더 브로커를 죽였을 때 즉시 새로운 리더가 선출되는가?
- ISR(In-Sync Replicas) 데이터 보존: 한 대가 죽어도 acks=all 설정이 데이터를 끝까지 지켜내는가?
- Consumer 재연결: 클라이언트 애플리케이션은 장애 상황에서도 끊김 없이 메시지를 읽어오는가?
이번 작업은 운영 환경 수준의 완성보다 KRaft 구조를 직접 이해하고 검증하는 데 의미가 있었습니다.
'포폴 > 일정관리 프로젝트' 카테고리의 다른 글
| 분산 서버 전환 후 부하 테스트로 병목 구간 찾기1 (0) | 2026.04.28 |
|---|---|
| 일정추천 기능 고도화 - 일정추천 챗봇으로 고도화 (0) | 2026.04.27 |
| AWS EC2 기반 Spring Boot 분산 인프라 구축기 2 (0) | 2026.04.19 |
| AWS EC2 기반 Spring Boot 분산 인프라 구축기 1 (0) | 2026.04.17 |
| Docker + Nginx 로드밸런싱 구성과 SPOF 검증 (0) | 2026.04.13 |