Kafka KRaft — 컨트롤러 장애와 리더 선출
액티브 컨트롤러가 죽는 순간부터 새 리더가 뽑히고 낡은 리더가 펜싱되기까지 — KIP-500/595의 실제 프로토콜(타임아웃·에포크·과반·로그 최신성 검사)을 20단계로 따라간다.
단계 목록 · 키보드 ←/→ 이동, Space 재생
출처: KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum · KIP-595: A Raft Protocol for the Metadata Quorum · Apache Kafka 공식 문서 — KRaft
KRaft 선출은 네 가지 장치의 조합이다 — 임의화 타임아웃(동시 입후보 방지), 로그 최신성 검사(커밋 손실 방지), 과반 득표(이중 리더 방지), 에포크(낡은 리더 펜싱).
왜 헷갈리는가
'ZooKeeper가 빠졌으니 선출도 없어졌겠지'가 흔한 오해다. 반대다 — 선출이 Kafka 안으로 들어왔다. 컨트롤러들이 스스로 Raft 변형(KRaft)으로 쿼럼을 이루고, 메타데이터를 ZooKeeper의 znode가 아니라 Kafka 자신의 로그(__cluster_metadata 토픽)로 관리한다. 또 하나: 컨트롤러가 죽어도 프로듀서/컨슈머 트래픽은 멈추지 않는다.
애니메이션이 보여주는 것
평시 루프부터 시작한다 — 리더가 메타데이터 변경을 로그에 적고, 팔로워가 Fetch로 당겨가고, 과반 오프셋이 커밋 지점이 된다. 그 다음 리더를 죽인다. 팔로워의 임의화된 타임아웃이 만료되고, 에포크를 올려 입후보하고, 로그 최신성을 검증받아 과반 표를 얻는 전 과정이 배지(Leader/Candidate/epoch)로 표시된다.
마지막 국면이 실무에서 가장 중요한 부분이다 — 죽었던 옛 리더가 '자기가 아직 리더인 줄 알고' 돌아왔을 때, 더 높은 에포크를 목격하고 즉시 강등되는 펜싱, 그리고 커밋 안 된 로그 꼬리의 절단.
표준 Raft와 다른 점 — pull 복제
Raft 논문의 리더는 팔로워에게 AppendEntries를 밀어넣는다(push). KRaft의 팔로워는 리더에게 Fetch를 보내 당겨온다(pull). Kafka에 이미 파티션 복제용 Fetch 경로가 있었기 때문에 이를 재사용한 것이다(KIP-595).
pull이라 리더는 팔로워의 Fetch 오프셋만 봐도 복제 진행률을 안다. 별도의 진행률 추적 상태가 필요 없다.
왜 과반이어야 하는가
3대 중 2대, 5대 중 3대 — 어떤 두 과반 집합도 반드시 한 노드 이상 겹친다. 그래서 서로 모르는 두 리더가 동시에 뽑히는 일이 수학적으로 불가능하다. 겹친 노드는 이미 한 에포크에 투표했으므로 두 번째 후보에게 표를 주지 않는다.
같은 이유로 3노드 클러스터는 1대 손실까지만 버틴다. 2대가 죽으면 과반을 만들 수 없어 선출 자체가 불가능하다 — 가용성보다 일관성을 택한 설계다.
- 3노드: 1대 손실 허용, 5노드: 2대 손실 허용
- 짝수 노드는 이득이 없다 — 4노드도 1대 손실까지(과반=3)
- 에포크에 한 표, 투표 기록은 디스크에 — 재시작해도 번복 없음
기억할 것
- KRaft = 컨트롤러 쿼럼이 Raft 변형으로 스스로 합의, 메타데이터는 Kafka 로그로.
- 선출 트리거는 임의화 타임아웃 — 동시 입후보(split vote)를 확률적으로 회피.
- 표를 받으려면 후보의 로그가 유권자보다 뒤처지지 않아야 — 커밋 손실 방지.
- 에포크가 펜싱을 만든다 — 낡은 리더의 쓰기는 전부 거부된다.
- 컨트롤러 장애 중에도 데이터 평면(프로듀서/컨슈머)은 계속 흐른다.