카프카 입문 시리즈 9편: KRaft, 주키퍼를 떠난 카프카의 새 두뇌
이 글은 카프카 시리즈의 9편으로, Apache Kafka 4.0에서 ZooKeeper가 완전히 사라진 배경과 그 자리를 채운 KRaft의 동작 원리를 정리합니다. 운영 환경에서 KRaft 클러스터를 다루거나 ZooKeeper 기반 클러스터를 마이그레이션하려는 분을 대상으로 합니다.
오랫동안 Apache Kafka는 두 개의 분산 시스템을 함께 운영해야 했습니다. 데이터 평면을 담당하는 카프카 자신과, 메타데이터·컨트롤러 선출을 담당하는 ZooKeeper입니다. 2025년 3월 18일 출시된 Apache Kafka 4.0은 이 두 시스템을 하나로 합쳤습니다. 4.0부터는 ZooKeeper 모드가 아예 제거되었고, KRaft (Kafka Raft) 모드만 남았습니다.
이번 글에서는 KRaft가 어떤 문제를 풀려고 등장했는지, 컨트롤러 쿼럼과 메타데이터 로그가 어떻게 동작하는지, 브로커가 어떻게 등록되고 펜싱되는지를 다이어그램과 함께 정리하겠습니다.
1. ZooKeeper 시절의 카프카
KRaft를 이해하려면 먼저 그 전에 무엇을 풀어야 했는지부터 봐야 합니다. ZooKeeper 기반 카프카는 다음과 같은 구조였습니다.
flowchart TB
subgraph ZK[ZooKeeper Ensemble]
Z1[ZK 1]
Z2[ZK 2]
Z3[ZK 3]
end
subgraph KafkaCluster[Kafka Cluster]
B1[Broker 1<br/>Controller]
B2[Broker 2]
B3[Broker 3]
end
B1 -- read/write metadata --> ZK
B2 -- read metadata --> ZK
B3 -- read metadata --> ZK
B1 -- LeaderAndIsr / UpdateMetadata --> B2
B1 -- LeaderAndIsr / UpdateMetadata --> B3
여기서 굵은 책임을 맡은 것은 컨트롤러(Controller) 라는 특수한 브로커입니다. 클러스터 전체에서 단 하나만 활성화되는 이 컨트롤러가 토픽 생성, 파티션 리더 선출, ISR 변경, 브로커 등록 같은 메타데이터 변경을 모두 처리했습니다. ZooKeeper는 이 컨트롤러가 누구인지 합의하는 도구이자, 메타데이터의 영속 저장소였습니다.
문제는 이 구조가 두 가지 한계를 안고 있었다는 점입니다.
첫째, 컨트롤러 페일오버가 느렸습니다. 활성 컨트롤러가 죽으면 새 컨트롤러는 ZooKeeper에서 모든 메타데이터를 다시 읽어 메모리에 올린 뒤에야 일을 시작할 수 있었습니다. 파티션이 많을수록 이 시간은 길어지고, 그 사이 토픽 생성·리더 선출 같은 메타데이터 작업이 모두 멈췄습니다. 실제 사례에서 컨트롤러 페일오버는 5~7초 걸리는 경우가 흔했습니다.
둘째, 메타데이터가 두 곳에 살았습니다. 진실은 ZooKeeper에 있었지만, 빠른 처리를 위해 각 브로커도 캐시를 들고 있었습니다. 컨트롤러가 UpdateMetadataRequest로 변경 사항을 RPC 푸시할 때 일부 브로커에 도달하지 못하면 메타데이터 발산(metadata divergence)이 발생할 수 있었습니다. 두 분산 시스템(ZK와 카프카)을 동시에 운영하는 부담은 별개의 문제였습니다.
셋째, 파티션 수가 늘어나면 한계가 빨리 왔습니다. 컨트롤러가 ZK 워치(watch) 콜백 기반의 이벤트로 동작하다 보니, 클러스터당 약 20만 파티션이 사실상 한계였습니다.
이 세 가지가 KIP-500의 출발점이었습니다.
2. KIP-500: ZooKeeper를 자체 메타데이터 쿼럼으로 대체하자
KIP-500은 2019년에 제안되어, 2022년 10월 출시된 Apache Kafka 3.3에서 KIP-833을 통해 새 클러스터에 한해 production ready로 선언되었습니다. 그리고 2025년 3월의 Apache Kafka 4.0에서 ZooKeeper 모드 자체가 제거되며 마무리되었습니다.
핵심 아이디어는 단순합니다.
메타데이터를 별도 시스템에 두지 말고, 카프카가 가장 잘 다루는 자료구조(분산 로그)에 그대로 담아 보자.
이 결정 위에 세 개의 KIP가 얹혀 있습니다.
| KIP | 다룬 부분 |
|---|---|
| KIP-595 | 메타데이터 쿼럼을 위한 Raft 변형 프로토콜 |
| KIP-631 | 쿼럼 기반 컨트롤러 설계 |
| KIP-853 | 동적 쿼럼 멤버십 변경 |
여기에 마이그레이션을 위한 KIP-866이 더해져 ZK→KRaft 전환 경로가 완성되었습니다.
3. KRaft 아키텍처 한눈에 보기
KRaft 모드의 구조는 다음과 같습니다.
flowchart TB
subgraph ControllerQuorum[Controller Quorum]
C1[Controller 1<br/>Active / Leader]
C2[Controller 2<br/>Follower]
C3[Controller 3<br/>Follower]
end
subgraph Brokers[Brokers]
B1[Broker 1]
B2[Broker 2]
B3[Broker 3]
B4[Broker 4]
end
C2 -- Fetch metadata --> C1
C3 -- Fetch metadata --> C1
B1 -- Fetch metadata + heartbeat --> C1
B2 -- Fetch metadata + heartbeat --> C1
B3 -- Fetch metadata + heartbeat --> C1
B4 -- Fetch metadata + heartbeat --> C1
Producers --> Brokers
Consumers --> Brokers
ZooKeeper 시절과 비교했을 때 달라진 점이 세 가지 있습니다.
- 외부 합의 시스템이 사라졌습니다. 컨트롤러들이 직접 Raft 합의를 수행해 자신들 중 한 명을 활성 컨트롤러로 뽑습니다.
- 메타데이터가 카프카 로그가 되었습니다. 모든 메타데이터 변경은
__cluster_metadata라는 내부 토픽에 이벤트로 기록됩니다. - 브로커가 메타데이터를 푸시 받지 않고 직접 가져옵니다. 브로커는 활성 컨트롤러로부터 변경 사항을 fetch해서 자기 캐시를 갱신합니다.
이 세 가지가 합쳐져, 컨트롤러 페일오버 시간이 ZK 시절의 수 초 단위에서 1초 미만으로 떨어졌습니다.
process.roles로 노드의 역할을 정한다
KRaft에서는 모든 노드가 다음 세 가지 역할 중 하나로 시작합니다. process.roles 설정으로 결정합니다.
| 값 | 의미 |
|---|---|
controller |
컨트롤러 쿼럼에만 참여 (메타데이터 관리 전담) |
broker |
데이터 평면만 담당 (Producer/Consumer 처리) |
controller,broker |
결합(combined) 모드. 한 프로세스가 양쪽을 모두 함 |
운영 환경에서는 controller와 broker를 분리하는 것이 권장됩니다. Combined 모드는 개발·테스트용입니다. 분리 운영의 이유는 자원 격리와 장애 격리 모두에 있습니다. 메타데이터 처리가 데이터 평면의 부하에 휘둘리지 않게 하려는 것입니다.
컨트롤러는 보통 3대 또는 5대로 구성합니다. 이는 Raft 가용성 공식(f + 1 다수결, 2f + 1개 노드로 f개 장애 허용)을 따릅니다. 3대면 1대 장애, 5대면 2대 장애까지 견딥니다. 짝수는 의미가 없어서 피합니다.
4. 메타데이터 로그: __cluster_metadata
KRaft의 영속 상태는 모두 __cluster_metadata라는 단일 토픽에 들어갑니다. 이 토픽은 다음 특징을 가집니다.
- 단일 파티션 — 메타데이터에는 전역 순서가 필요하므로 파티션을 쪼갤 수 없습니다.
- Raft로 복제 — 일반 토픽처럼 ISR로 복제되는 것이 아니라, 컨트롤러 쿼럼 사이에서 Raft 변형 합의로 복제됩니다.
- 이벤트 소싱 — 토픽 생성, 파티션 리더 변경, 브로커 등록 같은 모든 변경이 레코드로 기록되고, 모든 노드는 이 로그를 재생해 메타데이터를 메모리에 만듭니다.
왜 Raft "변형"인가
KIP-595는 표준 Raft가 아닌 Raft 변형을 사용한다고 명시합니다. 가장 큰 차이는 복제 방식입니다.
| 항목 | 표준 Raft | KRaft |
|---|---|---|
| 복제 방식 | 리더가 팔로워에게 push | 팔로워가 리더에게 pull (Fetch) |
| 용어 | term, index | epoch, offset |
| 기반 | 로그 인덱스 기반 | 카프카 오프셋·에포크 모델 재사용 |
이 결정은 자연스럽습니다. 카프카는 이미 pull 기반 복제 프로토콜을 잘 운영해 왔기 때문에, 컨트롤러 쿼럼 합의도 같은 모델을 그대로 가져왔습니다. 팔로워 컨트롤러는 활성 컨트롤러에 Fetch 요청을 보내 새 메타데이터 레코드를 가져오고, 브로커도 똑같은 방식으로 메타데이터를 따라잡습니다.
Active controller와 follower
Raft 합의로 뽑힌 리더 컨트롤러를 active controller, 나머지를 follower controller라고 부릅니다.
flowchart LR
Client[Admin Client] -- CreateTopicsRequest --> Active[Active Controller]
Active -- append record --> Log[__cluster_metadata]
Log -- replicated via Fetch --> F1[Follower Controller]
Log -- replicated via Fetch --> F2[Follower Controller]
Active -- ack to client --> Client
Brokers -- Fetch metadata --> Active
활성 컨트롤러만이 클라이언트의 CreateTopicsRequest, AlterPartitionRequest 같은 메타데이터 변경 RPC를 처리합니다. 변경 사항을 메타데이터 로그에 추가하고, 다수결로 커밋된 시점에 클라이언트에 응답합니다. 팔로워 컨트롤러는 같은 로그를 재생해 동일한 메타데이터 상태를 유지하며, 활성 컨트롤러가 죽었을 때 즉시 인계받을 수 있는 hot standby 역할을 합니다.
ZooKeeper 시절과 결정적으로 다른 점은 여기서 드러납니다. 새 활성 컨트롤러는 메타데이터를 외부에서 다시 로딩하지 않습니다. 이미 메모리에 모든 커밋된 레코드가 있기 때문에, 리더십이 넘어오는 즉시 일을 시작할 수 있습니다. 이 사실 하나가 컨트롤러 페일오버 시간을 결정짓습니다.
5. 브로커 등록과 펜싱
브로커가 KRaft 클러스터에 합류하는 흐름은 ZK 시절과 완전히 달라졌습니다. ZK 시절에는 브로커가 ZK에 ephemeral znode를 등록하고, 그 znode가 사라지면 컨트롤러가 죽음을 감지했습니다. KRaft에서는 RPC로 모든 것이 처리됩니다.
flowchart TB
Start([Broker boot]) --> Reg[Send BrokerRegistrationRequest]
Reg --> Epoch[Active controller assigns broker epoch]
Epoch --> Fenced[State: FENCED]
Fenced --> HB[Send BrokerHeartbeatRequest periodically]
HB --> CatchUp{Caught up to<br/>metadata log?}
CatchUp -- No --> HB
CatchUp -- Yes --> Unfence[Request unfencing in heartbeat]
Unfence --> Active2[State: UNFENCED / ACTIVE]
Active2 --> Serve[Serve produce/fetch traffic]
Serve --> HBLoop[Continue heartbeats]
HBLoop -- timeout --> Fenced2[Re-fenced]
핵심은 두 단계입니다.
1) 등록(BrokerRegistrationRequest). 브로커는 부팅 시 활성 컨트롤러에게 자신의 ID, 호스트, 디스크 정보 등을 담아 등록 요청을 보냅니다. 컨트롤러는 메타데이터 로그의 다음 오프셋을 기반으로 이 브로커에게 새로운 broker epoch를 발급합니다. 이 에포크는 등록 사이클을 식별하는 단조 증가 값으로, 좀비 브로커 차단의 핵심 장치입니다.
2) 하트비트(BrokerHeartbeatRequest). 등록만으로는 트래픽을 받지 않습니다. 브로커는 처음에 fenced 상태로 시작하고, 하트비트를 주기적으로 보내며 메타데이터 로그를 따라잡습니다. 충분히 따라잡으면 하트비트에 unfencing 요청을 담고, 컨트롤러가 이를 승인하면 비로소 unfenced 상태가 되어 클라이언트 트래픽을 처리합니다.
하트비트가 끊기면 컨트롤러는 그 브로커를 다시 fenced로 표시합니다. ZK의 ephemeral znode가 하던 "살아있음 신호" 역할을 BrokerHeartbeatRequest가 직접 수행하는 것입니다.
좀비 브로커 차단
같은 ID로 두 개의 브로커 프로세스가 동시에 돌아가면 카프카가 위험해집니다. KRaft는 broker epoch로 이를 막습니다. 새 등록 사이클이 시작되면 새 에포크가 발급되고, 모든 후속 RPC는 자기 에포크를 함께 보냅니다. 활성 컨트롤러가 더 새로운 에포크를 알고 있다면, 옛 에포크로 들어오는 요청은 거부되어 좀비 브로커가 자연스럽게 격리됩니다.
6. 메타데이터 스냅샷
이벤트 소싱 로그는 시간이 지날수록 무한히 커지는 문제가 있습니다. 토픽이 만들어지고 지워질 때마다 레코드가 쌓이면, 새로 합류하는 노드가 모든 변경 이력을 처음부터 재생해야 합니다.
KRaft는 이 문제를 스냅샷으로 해결합니다. 컨트롤러는 주기적으로 현재 메모리 상태를 디스크에 스냅샷으로 저장하고, 그 시점 이전의 메타데이터 로그를 잘라낼 수 있게 합니다.
flowchart LR
NewNode[New / lagging node] -- FetchSnapshot --> Active[Active controller]
Active -- snapshot up to offset N --> NewNode
NewNode -- Fetch from offset N+1 --> Active
Active -- newer records --> NewNode
NewNode --> InMemory[In-memory metadata cache ready]
새 컨트롤러나 한참 뒤처진 브로커는 먼저 FetchSnapshot으로 가장 최근 스냅샷을 받아 메모리에 올린 뒤, 그 이후 오프셋부터 일반 Fetch로 따라잡습니다. 이 두 단계 덕에, 클러스터에 새 노드를 붙여도 그 노드가 메타데이터 전체 이력을 재생하지 않아도 됩니다.
스냅샷 자체는 각 복제본 로컬에 저장됩니다. 그래서 컨트롤러 쿼럼의 모든 멤버가 자기 스냅샷을 가지고, 자기가 fetch 요청을 받으면 자기 것을 그대로 내려줍니다.
7. 정적 쿼럼과 동적 쿼럼
KRaft에는 컨트롤러 쿼럼 구성 방식이 두 가지 있습니다.
정적 쿼럼 (KRaft 0)
초기 KRaft는 정적 쿼럼만 지원했습니다. 모든 브로커와 컨트롤러의 설정 파일에 controller.quorum.voters를 하드코딩합니다.
process.roles=controller
node.id=1
controller.quorum.voters=1@host1:9093,2@host2:9093,3@host3:9093
listeners=CONTROLLER://:9093
여기서 controller.quorum.voters의 각 항목은 id@host:port 형식이며, 모든 노드(컨트롤러뿐 아니라 브로커도)에 같은 목록을 적어 줍니다.
문제는 컨트롤러 멤버를 바꾸기가 까다롭다는 점입니다. 정적 쿼럼에서 노드를 추가하거나 빼려면 모든 노드의 설정을 직접 수정하고 재시작해야 합니다.
동적 쿼럼 (KIP-853, Kafka 3.9+)
KIP-853이 Apache Kafka 3.9에 들어가며 동적 쿼럼이 가능해졌습니다. 핵심 변화는 두 가지입니다.
controller.quorum.voters를controller.quorum.bootstrap.servers로 대체합니다. 이 키는 카프카 클라이언트의bootstrap.servers와 비슷한 역할로, 모든 컨트롤러 목록을 전부 담을 필요는 없고 쿼럼을 찾을 정도면 됩니다.kafka-metadata-quorum.sh도구나AdminClientAPI로 컨트롤러를 동적으로 추가·제거할 수 있습니다.
# 컨트롤러 추가 예시
bin/kafka-metadata-quorum.sh --bootstrap-controller host:9093 add-controller \
--config controller.properties
KIP-853은 안전을 위해 한 번에 하나의 컨트롤러만 추가하거나 제거할 수 있도록 제한합니다. 두 개를 동시에 바꾸면 가용성이 깨질 수 있기 때문입니다.
운영 권고는 분명합니다. 새 클러스터는 처음부터 동적 쿼럼으로 시작합니다. 기존 정적 쿼럼 클러스터는 KRaft 버전이 1 이상으로 올라간 뒤 controller.quorum.voters를 제거하고 controller.quorum.bootstrap.servers로 마이그레이션합니다.
8. ZooKeeper에서 KRaft로 마이그레이션
KIP-866은 운영 중인 ZK 기반 클러스터를 무중단으로 KRaft로 옮기기 위해 만들어졌습니다. 핵심은 dual-write 모드 입니다.
flowchart TB
subgraph Phase1[Phase 1: ZooKeeper]
ZK1[ZooKeeper] --- BR1[ZK-mode brokers]
end
subgraph Phase2[Phase 2: Hybrid - Dual Write]
ZK2[ZooKeeper] --- KRaftCtrl[KRaft controller<br/>writes to both]
KRaftCtrl --- ZKBroker[ZK-mode brokers<br/>still reading ZK]
end
subgraph Phase3[Phase 3: KRaft only]
KRaftCtrl2[KRaft controller] --- KRaftBroker[KRaft-mode brokers]
end
Phase1 --> Phase2 --> Phase3
Phase 1. 기존처럼 ZK가 진실의 소스이고 ZK 컨트롤러가 동작합니다.
Phase 2 (dual-write). KRaft 컨트롤러를 별도로 띄우고, 마이그레이션 모드로 진입합니다. KRaft 컨트롤러가 새 활성 컨트롤러가 되어 메타데이터 변경을 KRaft 로그에 기록하면서, 동시에 ZooKeeper에도 같은 변경을 써 줍니다. 브로커는 아직 ZK 모드라서 ZK에서 메타데이터를 읽습니다. ZK와 KRaft 양쪽이 같은 상태를 가지게 됩니다.
Phase 3. 브로커들을 차례로 KRaft 모드로 재시작해 ZK 의존을 떼어 냅니다. 모든 브로커가 KRaft가 되면 ZK 쓰기를 멈추고 ZooKeeper 앙상블을 종료합니다.
마이그레이션을 시작하려면 브로커가 미리 충분히 새 버전으로 올라와 있어야 합니다. 공식 가이드는 마이그레이션 전 브로커를 3.7.2 이상으로 업그레이드하고, inter.broker.protocol.version을 적절히 맞추도록 안내합니다. Phase 2까지는 ZK로 롤백할 수 있지만, Phase 3가 끝나는 순간부터는 되돌릴 수 없습니다.
마지막 주의 사항은 시점입니다. Apache Kafka 4.0 이상으로는 ZK 모드 클러스터를 직접 업그레이드할 수 없습니다. 4.0이 ZK 모드를 아예 제거했기 때문에, 4.0으로 가려면 반드시 그 전 단계인 3.x에서 KRaft로 먼저 마이그레이션을 끝내야 합니다.
9. Apache Kafka 4.0이 가져온 변화
KRaft GA가 Kafka 3.3이라면, KRaft only가 된 시점은 Kafka 4.0입니다. 4.0 릴리스 노트는 이 결정을 명시적으로 정리합니다.
- ZooKeeper 모드 완전 제거. 4.0은 KRaft만 지원합니다.
- KIP-848의 차세대 컨슈머 재밸런스 프로토콜이 GA로 승격.
- KIP-932의 share group(큐 시맨틱) early access 도입.
KRaft만 남게 되자 운영의 그림도 달라졌습니다. 이제 카프카 클러스터를 띄우는 데 필요한 분산 시스템은 카프카 자신뿐입니다. 모니터링 대상도, 백업 대상도, 보안 정책 대상도 한 줄로 줄었습니다.
10. 운영 시 자주 부딪히는 함정
KRaft가 단순해진 만큼, 새로운 함정도 함께 왔습니다.
1) 컨트롤러를 짝수로 두지 않기
Raft 다수결 특성상, 컨트롤러 4대는 3대보다 가용성이 높지 않습니다. 한 대 장애 허용을 위해 둘 다 한 대까지 견딜 뿐이고, 4대는 자원만 더 쓸 뿐입니다. 3대(소규모) 또는 5대(대규모)로 둡니다.
2) Combined 모드는 운영용이 아니다
process.roles=controller,broker로 한 프로세스가 양쪽을 다 하는 모드는 단일 노드 개발·테스트용입니다. 운영에서는 메타데이터 처리가 데이터 평면 부하에 휘둘리지 않도록 컨트롤러와 브로커를 분리하는 것이 권장됩니다.
3) 메타데이터 로그 디스크는 분리
metadata.log.dir를 데이터 로그 디스크와 분리하면 좋습니다. 메타데이터 쓰기는 양은 적지만 fsync 지연이 생기면 모든 컨트롤 평면이 느려지므로, 로그가 가장 빠른 디스크에 있는 것이 안전합니다.
4) controller.quorum.voters → controller.quorum.bootstrap.servers
새 클러스터를 만들 때는 처음부터 동적 쿼럼(controller.quorum.bootstrap.servers)으로 가는 것이 좋습니다. 정적 쿼럼은 향후 컨트롤러 멤버 변경을 어렵게 만듭니다.
5) 브로커가 fenced에 갇힌다면 메타데이터를 못 따라잡고 있는 것
브로커가 등록은 됐는데 unfenced로 안 넘어간다면, 그 브로커가 메타데이터 로그를 충분히 따라잡지 못했다는 뜻입니다. 디스크 IO나 네트워크가 막혀 있는지, 메타데이터 로그가 너무 커서 스냅샷 fetch가 오래 걸리는 건 아닌지부터 봅니다.
11. 정리
ZooKeeper 시절의 카프카는 두 분산 시스템의 협업 위에 서 있었습니다. KRaft는 그 협업을 없애고, 메타데이터를 카프카 자신의 로그로 가져와서 다음을 가능하게 했습니다.
- 컨트롤러 페일오버가 거의 즉시 끝납니다. 새 활성 컨트롤러는 외부에서 상태를 다시 읽지 않고, 이미 자기 메모리에 있는 커밋된 레코드로 일을 시작합니다.
- 브로커는 푸시 받기를 멈추고 직접 fetch합니다. 메타데이터 발산이 구조적으로 사라집니다.
- 운영 대상이 한 시스템으로 줄어들었습니다. ZK 앙상블 모니터링, 백업, 보안 설정이 모두 빠집니다.
- 파티션 수의 한계가 크게 늘어났습니다.
process.roles로 노드 역할을 정하고, controller.quorum.bootstrap.servers로 부트스트랩하며, __cluster_metadata 단일 파티션 로그가 진실의 소스로 자리 잡는다는 사실 — 이 세 가지가 KRaft 운영의 출발점입니다.
다음 편에서는 카프카의 운영 측면을 다른 각도에서 보겠습니다.
자주 묻는 질문
Q1. KRaft로 가면 ZooKeeper처럼 별도 디스크 IO가 사라지는가?
답변 보기
ZK가 쓰던 트랜잭션 로그(txn log)와 스냅샷 디스크 IO는 사라집니다. 그 대신 컨트롤러 쿼럼이 __cluster_metadata 토픽에 메타데이터 변경을 직접 씁니다. 양은 메타데이터 변경 빈도에 비례하는데, 일반적인 운영에서는 ZK보다 부담이 작거나 비슷합니다. 컨트롤러 노드의 디스크는 데이터 브로커 디스크와 분리하는 것이 권장됩니다.
Q2. 컨트롤러를 따로 두는 게 좋은지, broker와 합치는 게 좋은지
답변 보기
운영 환경에서는 분리가 권장됩니다. 이유는 두 가지입니다. 첫째, 데이터 평면(produce/fetch) 부하가 메타데이터 처리에 영향을 주지 않게 격리됩니다. 둘째, 장애 도메인이 분리되어 한 노드 장애의 영향이 좁아집니다. Combined 모드(process.roles=controller,broker)는 단일 머신 개발·테스트용으로 충분합니다.
Q3. 컨트롤러는 몇 대가 적정한가
답변 보기
소규모는 3대, 대규모·고가용성이 필요한 환경은 5대가 일반적입니다. Raft 가용성 공식상 2f+1 노드로 f 장애를 허용하므로, 3대는 1대 장애, 5대는 2대 장애까지 견딥니다. 4대처럼 짝수는 가용성을 추가로 올리지 않으면서 비용만 늘리므로 피합니다. 7대 이상은 합의 round trip이 늘어나 메타데이터 변경 지연이 커지기 때문에 특수한 경우 외에는 권장되지 않습니다.
Q4. 컨트롤러 페일오버가 ZK 시절보다 정말 빠른가
답변 보기
구조적으로 그렇습니다. ZK 시절 새 활성 컨트롤러는 ZK에서 모든 메타데이터를 다시 읽어 메모리에 적재해야 했습니다. 파티션이 많을수록 이 시간이 길어졌고, 실제 운영에서 5~7초가 흔했습니다. KRaft에서는 팔로워 컨트롤러가 이미 같은 메타데이터 로그를 재생하고 있어, 리더십이 넘어오는 즉시 일을 시작할 수 있습니다. 페일오버 자체보다 다른 노드들이 새 리더를 인지하는 데 걸리는 시간이 더 큰 경우가 많습니다.
Q5. controller.quorum.voters와 controller.quorum.bootstrap.servers의 차이는
답변 보기
controller.quorum.voters는 정적 쿼럼용으로, 모든 컨트롤러의 id@host:port를 빠짐없이 적어야 합니다. 멤버 변경에는 모든 노드 설정 수정과 재시작이 필요합니다.
controller.quorum.bootstrap.servers는 KIP-853 동적 쿼럼용으로, 클라이언트의 bootstrap.servers처럼 쿼럼을 찾을 정도의 일부 컨트롤러 주소만 있으면 됩니다. 멤버 변경은 kafka-metadata-quorum.sh나 AdminClient API로 동적으로 처리됩니다. Apache Kafka 3.9 이상이 필요하며, 새 클러스터는 처음부터 동적 쿼럼으로 시작하는 것이 권장됩니다.
Q6. ZK에서 KRaft로 마이그레이션 중 문제가 생기면 롤백할 수 있는가
답변 보기
가능한 시점이 정해져 있습니다. KIP-866 마이그레이션 절차상 dual-write 단계(Phase 2)까지는 ZooKeeper로 롤백할 수 있습니다. 이 단계에서는 ZK와 KRaft가 같은 메타데이터를 들고 있기 때문에 ZK 컨트롤러로 다시 돌리는 것이 가능합니다.
브로커들을 KRaft 모드로 재시작해 Phase 3로 넘어간 뒤에는 KRaft 로그가 진실의 소스가 되고, ZK 쓰기가 멈춥니다. 이 시점부터는 ZK로 되돌릴 수 없습니다. 그래서 마이그레이션 계획은 Phase 2에서 충분한 검증 시간을 확보하는 것이 핵심입니다.
참고자료
- Apache Kafka 4.0.0 Release Announcement
- KRaft - Apache Kafka 4.1 Operations Guide
- KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum
- KIP-595: A Raft Protocol for the Metadata Quorum
- KIP-631: The Quorum-based Kafka Controller
- KIP-833: Mark KRaft as Production Ready
- KIP-853: KRaft Controller Membership Changes
- KIP-866: ZooKeeper to KRaft Migration
- KIP-848: The Next Generation of the Consumer Rebalance Protocol
- KIP-932: Queues for Kafka
- KRaft - Apache Kafka Without ZooKeeper (Confluent Developer)

