클러스터 멤버십과 컨트롤러, 그리고 복제의 기본 구조
Kafka: The Definitive Guide (O'Reilly, 2017, 1st Edition) — Neha Narkhede, Gwen Shapira, Todd Palino Chapter 5: Kafka Internals — Cluster Membership / The Controller / Replication (pp.95-98)
Kafka 클러스터는 어떤 브로커가 살아 있는지를 ZooKeeper의 임시(ephemeral) 노드로 관리하고, 그중 한 브로커가 컨트롤러가 되어 파티션 리더 선출을 전담한다. 이 모듈에서는 브로커가 클러스터에 등록되고 이탈하는 과정, 컨트롤러 선출과 컨트롤러 에포크가 split brain을 막는 원리, 리더/팔로워 복제본의 역할과 ISR 판정 기준, preferred leader 개념까지를 다룬다. 이 세 가지가 Kafka의 가용성과 내구성을 떠받치는 뼈대다.
브로커는 어떻게 클러스터의 멤버가 되는가
모든 브로커는 설정 파일에 지정되었거나 자동 생성된 고유한 broker ID를 가진다. 브로커 프로세스가 시작되면 자신의 ID로 ZooKeeper에 임시 노드를 만들어 `/brokers/ids` 경로 아래에 자신을 등록한다. 임시 노드는 이를 만든 세션이 살아 있는 동안만 존재하는 노드다.
다른 Kafka 컴포넌트들은 이 경로를 구독하고 있어서, 브로커가 추가되거나 제거되면 알림을 받는다. 이미 사용 중인 ID로 다른 브로커를 시작하려 하면 같은 노드가 이미 존재하므로 등록이 실패하고 오류가 난다.
브로커가 ZooKeeper와의 연결을 잃으면 — 보통은 브로커가 정지했기 때문이지만 네트워크 분단이나 긴 GC 정지 때문일 수도 있다 — 시작할 때 만들었던 임시 노드가 자동으로 사라지고, 브로커 목록을 지켜보던 컴포넌트들이 이탈을 통지받는다.
핵심 포인트
- 브로커는 시작 시 ZooKeeper에 broker ID로 임시 노드를 만들어 등록한다
- 중복된 broker ID로는 두 번째 브로커가 등록에 실패한다
- 정지·네트워크 분단·긴 GC 정지 모두 임시 노드 소멸(=이탈 감지)로 이어진다
브로커 ID는 노드가 사라져도 남는다
브로커를 나타내는 임시 노드는 브로커가 멈추면 사라지지만, broker ID 자체는 다른 자료 구조에 그대로 남아 있다. 대표적으로 각 토픽 파티션의 복제본 목록에는 그 복제본을 가진 브로커의 ID가 기록되어 있다.
덕분에 브로커 장비를 완전히 잃어버렸더라도, 예전과 같은 ID로 새 브로커를 띄우면 그 브로커는 즉시 사라진 브로커의 자리를 대신해 클러스터에 합류하고 같은 파티션과 토픽을 할당받는다. 장애 장비를 교체할 때 ID를 그대로 물려주는 것이 중요한 이유가 여기에 있다.
핵심 포인트
- 임시 노드는 사라져도 복제본 목록 등에 broker ID는 남는다
- 같은 ID로 새 브로커를 띄우면 이전 브로커의 파티션 할당을 그대로 이어받는다
컨트롤러: 파티션 리더를 선출하는 특별한 브로커
컨트롤러는 일반적인 브로커 기능에 더해 파티션 리더 선출을 책임지는 브로커 한 대다. 클러스터에서 가장 먼저 시작한 브로커가 ZooKeeper에 `/controller`라는 임시 노드를 만들면서 컨트롤러가 된다. 뒤이어 시작하는 브로커들도 같은 노드를 만들려고 시도하지만 "node already exists" 예외를 받고, 이미 컨트롤러가 있다는 사실을 인지한 뒤 그 노드에 watch를 건다. 이렇게 해서 클러스터에는 항상 컨트롤러가 하나만 존재하게 된다.
컨트롤러가 정지하거나 ZooKeeper와의 연결을 잃으면 임시 노드가 사라지고, watch를 걸어두었던 다른 브로커들이 알림을 받아 저마다 컨트롤러 노드를 만들려고 시도한다. 가장 먼저 만드는 데 성공한 브로커가 새 컨트롤러가 되고, 나머지는 다시 "node already exists" 예외를 받은 뒤 새 노드에 watch를 다시 건다. 투표나 과반수 합의를 거치는 것이 아니라 "먼저 만든 쪽이 이긴다"는 단순한 규칙이다.
핵심 포인트
- 컨트롤러는 ZooKeeper의 /controller 임시 노드를 먼저 생성한 브로커가 된다
- 나머지 브로커는 그 노드에 watch를 걸어 컨트롤러 소멸을 감지한다
- 재선출은 과반 투표가 아니라 먼저 노드를 만드는 브로커가 차지하는 방식이다
컨트롤러 에포크와 split brain 방지
컨트롤러가 선출될 때마다 ZooKeeper의 조건부 증가(conditional increment) 연산을 통해 이전보다 더 큰 새 컨트롤러 에포크 번호를 부여받는다. 브로커들은 현재 에포크 번호를 알고 있으므로, 그보다 낮은 에포크를 가진 컨트롤러에게서 온 메시지를 받으면 그것을 무시한다.
이 장치가 없다면, 네트워크 문제로 잠시 격리되었던 옛 컨트롤러가 자신이 여전히 컨트롤러라고 믿고 명령을 내려 두 컨트롤러가 공존하는 split brain이 발생할 수 있다. 에포크 번호는 이런 낡은 명령을 걸러내는 역할을 한다.
핵심 포인트
- 컨트롤러 선출 시마다 더 큰 에포크 번호가 부여된다(조건부 증가 연산)
- 브로커는 더 낮은 에포크의 컨트롤러 명령을 무시한다
- 에포크는 두 컨트롤러가 동시에 명령을 내리는 split brain을 방지하는 장치다
브로커가 떠나고 합류할 때 컨트롤러가 하는 일
컨트롤러는 ZooKeeper 경로를 감시하다가 브로커가 클러스터를 떠난 것을 알아채면, 그 브로커에 리더가 있던 모든 파티션에 새 리더가 필요하다는 사실을 안다. 컨트롤러는 새 리더가 필요한 파티션들을 하나씩 훑으면서 다음 리더를 결정하는데, 그 규칙은 단순히 해당 파티션의 복제본 목록에서 다음 순서의 복제본이다. 그런 다음 새 리더가 될 브로커와 그 파티션의 팔로워들이 있는 모든 브로커에게 요청을 보낸다. 요청에는 새 리더와 팔로워 정보가 들어 있어서, 새 리더는 클라이언트의 produce/fetch 요청을 받기 시작해야 한다는 것을 알고 팔로워들은 새 리더로부터 복제를 시작해야 한다는 것을 안다.
반대로 브로커가 클러스터에 합류하면, 컨트롤러는 broker ID로 그 브로커에 존재하는 복제본이 있는지 확인한다. 있다면 신규 브로커와 기존 브로커 모두에게 변경 사실을 알리고, 새 브로커의 복제본들이 기존 리더로부터 복제를 시작한다.
핵심 포인트
- 컨트롤러는 이탈한 브로커가 리더였던 파티션들의 새 리더를 결정한다
- 새 리더는 복제본 목록에서 다음 순서의 복제본을 고르는 방식으로 정해진다
- 관련 브로커들에게 새 리더/팔로워 정보를 담은 요청을 보내 역할을 전환시킨다
리더 복제본과 팔로워 복제본
각 토픽은 파티션으로 나뉘고, 각 파티션은 여러 복제본을 가질 수 있다. 복제본은 브로커에 저장되며, 브로커 한 대가 서로 다른 토픽·파티션에 속한 수백에서 수천 개의 복제본을 가지는 것이 보통이다.
복제본에는 두 종류가 있다. 리더 복제본은 파티션당 하나이며 모든 produce와 consume 요청이 이 리더를 통해 처리되어 일관성이 보장된다. 팔로워 복제본은 리더가 아닌 나머지 전부로, 클라이언트 요청을 처리하지 않고 오직 리더로부터 메시지를 복제해 최신 상태를 유지하는 일만 한다. 리더가 죽으면 팔로워 중 하나가 새 리더로 승격된다.
핵심 포인트
- 파티션당 리더는 하나이며 모든 produce/consume 요청을 처리한다
- 팔로워는 클라이언트 요청을 처리하지 않고 복제만 담당한다
- 리더 장애 시 팔로워 중 하나가 승격된다
ISR 판정: 팔로워는 어떻게 '동기화 상태'로 인정받는가
팔로워는 리더에게 Fetch 요청을 보내 복제한다. 이는 컨슈머가 메시지를 소비할 때 보내는 것과 완전히 동일한 종류의 요청이다. Fetch 요청에는 다음에 받고 싶은 메시지의 오프셋이 담겨 있고 그 순서는 항상 오름차순이다. 복제본은 메시지 1, 2, 3을 차례로 요청하며 앞의 것들을 모두 받기 전에는 4번을 요청하지 않는다. 따라서 리더는 복제본이 메시지 4를 요청하는 순간 그 복제본이 3번까지는 모두 받았다는 것을 알 수 있고, 각 복제본이 마지막으로 요청한 오프셋만 보면 얼마나 뒤처졌는지 판단할 수 있다.
책이 다루는 버전 기준으로, 복제본이 10초가 넘도록 메시지를 요청하지 않았거나, 요청은 하고 있지만 10초가 넘도록 최신 메시지를 따라잡지 못하면 out of sync로 간주된다. 뒤처진 복제본은 모든 메시지를 가지고 있지 않으므로 장애 시 새 리더가 될 자격을 잃는다. 반대로 꾸준히 최신 메시지를 요청하는 복제본을 in-sync replica(ISR)라 하고, 오직 ISR만이 리더로 선출될 수 있다. 이 허용 지연 시간은 `replica.lag.time.max.ms`로 조정한다(책 집필 당시 기본 10초, 최신 버전에서는 KIP-537로 30초).
핵심 포인트
- 팔로워도 컨슈머와 같은 Fetch 요청으로 복제한다
- 요청 오프셋이 오름차순이므로 리더는 마지막 요청 오프셋만으로 지연을 판단한다
- replica.lag.time.max.ms 안에 따라잡지 못하면 out of sync가 되어 리더 후보에서 제외된다
- ISR만 리더로 선출될 수 있다
preferred leader와 자동 리더 리밸런스
현재 리더와 별개로, 각 파티션에는 preferred leader가 있다. 토픽이 처음 생성될 때 리더였던 복제본이다. 파티션이 처음 만들어질 때 리더들은 브로커 사이에 고르게 분산되도록 배치되므로, 클러스터의 모든 파티션에서 preferred leader가 실제 리더인 상태라면 부하가 브로커 사이에 균등하게 분산되어 있다고 기대할 수 있다.
Kafka는 기본적으로 `auto.leader.rebalance.enable=true`로 설정되어 있어, preferred leader가 현재 리더가 아니면서 ISR에 속해 있는지 확인하고 리더 선출을 트리거해 preferred leader를 다시 리더로 만든다.
preferred leader를 확인하는 가장 좋은 방법은 파티션의 복제본 목록을 보는 것이다. 목록의 첫 번째 복제본이 언제나 preferred leader이며, 이는 현재 리더가 누구인지와 무관하고 재배치 도구로 복제본을 옮긴 뒤에도 마찬가지다. 그러므로 복제본을 수동으로 재배치할 때는 첫 번째로 지정한 복제본이 preferred가 된다는 점을 기억하고, 특정 브로커에 리더가 몰리지 않도록 골고루 흩어 놓아야 한다.
핵심 포인트
- preferred leader = 복제본 목록의 첫 번째 복제본(토픽 생성 시의 리더)
- auto.leader.rebalance.enable 기본값은 true
- 수동 재배치 시 첫 번째로 적는 복제본이 preferred가 되므로 브로커별로 분산시켜야 한다
이 모듈과 연관된 문항 4개가 문제 은행에 있습니다.
이 내용으로 문제 풀어보기