KERBE.io

TECH / kafka

KRaft 컨트롤러 쿼럼, voters와 bootstrap.servers 헷갈리지 않기

#Kafka#KRaft#분산시스템

Kafka 4.x 트렁크 소스를 살펴보면 config/kraft/ 예제 디렉터리가 이제 없습니다. 대신 server.properties(combined), controller.properties(컨트롤러 전용), broker.properties(브로커 전용) 세 개로 역할이 나뉘어 있습니다. ZooKeeper 마이그레이션 관련 코드도 핵심 부팅 경로에서는 더 이상 보이지 않습니다. 4.4.0 트렁크는 사실상 순수 KRaft 기준으로 정리된 상태입니다.

voters와 bootstrap.servers는 다른 길이다

QuorumConfig.java를 보면 컨트롤러 쿼럼을 지정하는 방법이 두 가지입니다.

  • controller.quorum.voters: {id}@{host}:{port} 형태로 쿼럼 멤버를 정적으로 나열하는 기존 방식
  • controller.quorum.bootstrap.servers: KIP-853으로 도입된 동적 쿼럼 재구성용 부트스트랩 엔드포인트

두 설정은 상호 배타적인 부트스트랩 경로입니다. controller.properties 예제 파일에서도 controller.quorum.voters는 주석 처리되어 있고, bootstrap.servers가 활성 설정으로 제공됩니다. 정적 방식으로 쿼럼을 구성하면 멤버를 추가하거나 제거할 때 관련 노드를 전부 재기동해야 합니다. 동적 방식은 클러스터를 운영하는 도중에 kafka-metadata-quorum.sh로 투표권자를 add/remove할 수 있어서, 신규 클러스터라면 굳이 정적 방식을 쓸 이유가 없습니다.

combined 노드에서 자주 놓치는 리스너 설정

process.roles=broker,controller로 하나의 프로세스에 두 역할을 합치는 combined mode를 쓸 때, listeners에 CONTROLLER 리스너를 빠뜨리는 실수가 흔합니다. 설정 파일 주석에도 "combined 노드는 최소한 여기에 컨트롤러 리스너를 나열해야 한다"고 명시되어 있는데, 이걸 놓치면 브로커가 자기 자신의 컨트롤러 절반과 통신하지 못해 기동 자체가 실패합니다.

process.roles=broker,controller
node.id=1
controller.quorum.bootstrap.servers=localhost:9093
controller.listener.names=CONTROLLER
listeners=PLAINTEXT://:9092,CONTROLLER://:9093

컨트롤러는 단일 이벤트 루프다

메타데이터 쓰기는 QuorumController가 전담합니다. 이 클래스는 클러스터 등록, 파티션/ISR, 설정, ACL 같은 여러 매니저를 오케스트레이션하지만, 실제로는 단일 스레드 이벤트 루프에서 모든 메타데이터 변경을 직렬화해서 __cluster_metadata 토픽에 append합니다. 데이터 플레인(PRODUCE/FETCH)과는 별개의 병목 지점이라는 뜻입니다. 토픽/파티션을 대량으로 한 번에 생성하거나 설정 변경을 몰아서 하면, 브로커 처리량과 무관하게 컨트롤러 쪽에서 지연이 생길 수 있습니다.

브로커 쪽 요청 처리도 이 구조와 맞물립니다. KafkaApis는 CREATE_TOPICS나 ACL 변경 같은 메타데이터성 요청을 받으면 직접 처리하지 않고 컨트롤러로 포워딩합니다. 그래서 컨트롤러가 죽으면 이런 요청만 실패하고, 이미 존재하는 토픽에 대한 PRODUCE/FETCH는 영향을 받지 않을 수 있습니다. 장애 범위를 판단할 때 이 구분을 해두면 초기 대응이 빨라집니다.

정리

신규 클러스터를 구성한다면 controller.quorum.bootstrap.servers + 동적 쿼럼을 기본으로 잡고, 컨트롤러 노드 수는 과반수 합의가 명확하게 성립하도록 홀수(3 또는 5)로 유지하는 편이 안전합니다. 메타데이터 로그가 쌓이는 디렉터리는 데이터 파티션과 I/O를 나눠서, 저지연 디스크에 별도로 두는 것도 대규모 클러스터에서는 챙길 만한 부분입니다.